diff --git a/.env b/.env new file mode 100644 index 000000000..314bcb119 --- /dev/null +++ b/.env @@ -0,0 +1,16 @@ +BASE_API='https://api.staging.pangea.chat/api/v1' +CHOREO_API = 'https://api.staging.pangea.chat/choreo' +FRONTEND_URL='https://app.staging.pangea.chat' + +SYNAPSE_URL = 'matrix.staging.pangea.chat' +CHOREO_API_KEY = 'e6fa9fa97031ba0c852efe78457922f278a2fbc109752fe18e465337699e9873' +GOOGLE_AUTH_KEY = '466850640825-qegdiq3mpj3h5e0e79ud5hnnq2c22mi3.apps.googleusercontent.com' + +RC_PROJECT = 'a499dc21' +RC_KEY = 'sk_eVGBdPyInaOfJrKlPBgFVnRynqKJB' +RC_GOOGLE_KEY = 'goog_paQMrzFKGzuWZvcMTPkkvIsifJe' +RC_IOS_KEY = 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv' +RC_STRIPE_KEY = 'strp_YWZxWUeEfvagiefDNoofinaRCOl' +RC_OFFERING_NAME = 'test' + +STRIPE_MANAGEMENT_LINK = 'https://billing.stripe.com/p/login/test_9AQaI8d3O9lmaXe5kk' diff --git a/.env.prod b/.env.prod new file mode 100644 index 000000000..7bf172fbe --- /dev/null +++ b/.env.prod @@ -0,0 +1,16 @@ +BASE_API='https://api.pangea.chat/api/v1' +CHOREO_API = 'https://api.pangea.chat/choreo' +FRONTEND_URL='https://app.pangea.chat' + +SYNAPSE_URL = 'matrix.pangea.chat' +CHOREO_API_KEY = '223d863480cbb439fd9f16538c5b56ea09ac375c5aa7ea4c39cc32211117afae' +GOOGLE_AUTH_KEY = '723169076587-tq82s1qrugqphsl9527tng43tbuc7mv1.apps.googleusercontent.com' + +RC_PROJECT = 'a499dc21' +RC_KEY = 'sk_eVGBdPyInaOfJrKlPBgFVnRynqKJB' +RC_GOOGLE_KEY = 'goog_paQMrzFKGzuWZvcMTPkkvIsifJe' +RC_IOS_KEY = 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv' +RC_STRIPE_KEY = 'strp_YWZxWUeEfvagiefDNoofinaRCOl' +RC_OFFERING_NAME = 'default' + +STRIPE_MANAGEMENT_LINK = 'https://billing.stripe.com/p/login/dR6dSkf5p6rBc4EcMM' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0ce56e79e..7565732ad 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ prime .pub/ /build/ +# Gitlab runner locally +/builds/ + # Web related docs/tailwind.css @@ -53,6 +56,7 @@ lib/l10n_old ios/Flutter/.last_build_id ios/Podfile.lock ios/Runner.ipa +scripts/.credentials /windows/out /winuwp/out @@ -60,3 +64,5 @@ ios/Runner.ipa /macos/out .vs olm + +needed-translations.txt diff --git a/.metadata b/.metadata index 9a6ebb3f7..9b7605951 100644 --- a/.metadata +++ b/.metadata @@ -1,7 +1,7 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: revision: "efbf63d9c66b9f6ec30e9ad4611189aa80003d31" @@ -15,9 +15,24 @@ migration: - platform: root create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + - platform: android + create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + - platform: ios + create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 - platform: linux create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + - platform: macos + create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + - platform: web + create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + - platform: windows + create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 + base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31 # User provided section diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..12f26c9a3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,71 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "pangea-chat", + "request": "launch", + "type": "dart", + // "args": [ + // "-d", + // "chrome", + // "--web-port", + // "49632" + // ], + }, + { + "name": "pangea-chat (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "pangea-chat (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "pangea_choreographer", + "cwd": "pangea_packages\\pangea_choreographer", + "request": "launch", + "type": "dart" + }, + { + "name": "pangea_choreographer (profile mode)", + "cwd": "pangea_packages\\pangea_choreographer", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "pangea_choreographer (release mode)", + "cwd": "pangea_packages\\pangea_choreographer", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "pangea_language", + "cwd": "pangea_packages\\pangea_language", + "request": "launch", + "type": "dart" + }, + { + "name": "pangea_language (profile mode)", + "cwd": "pangea_packages\\pangea_language", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "pangea_language (release mode)", + "cwd": "pangea_packages\\pangea_language", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..dd15e28ee --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "dart.previewLsp": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true, + "source.sortMembers": false + }, + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/TRANSLATORS_GUIDE.md b/TRANSLATORS_GUIDE.md new file mode 100644 index 000000000..d5bc4f8fe --- /dev/null +++ b/TRANSLATORS_GUIDE.md @@ -0,0 +1,35 @@ +# Translators Guide + +There are 3 main types of strings to be translated. + +## Simple +``` +Add new friend +``` +They are just plain text and are to be translated in full. + +## Placeholder +``` +{username} changed their avatar +``` +Contains one or more words surrounded by curly brackets "`{}`". Anything outside of the curly brackets is to be translated as normal, but the words in the curly brackets are **NOT** to be translated. In the above example "`{username}`" will be replaced by the users actual username by FluffyChat. + +## Plural + +- {count,plural, =1{**1 more event**} other{{count} **more events**}} + +This is the most complicated string type, the parts in bold are the only parts that need translating in this string. You can identify plural strings by seeing the pattern `{word,plural,` at the start. `=1` and `other` are "selectors" so you can have multiple different translations for different quantities. `other` is the only required selector and will be chosen if the count does not match any other selectors. + +Selector | Matches +---|--- +=0 | a count of exactly 0 +=1 | a count of exactly 1 +=2 | a count of exactly 2 +other | any number unless it matches a more specific rule + +There is also "few" and "many", but they seem to have language specific meaning. + +Also the selectors do not need to match the English version such as your language may not even use different words for when there is more than one of something so: + - {count,plural, other{{count} \}} + +could be a perfectly resonable way to translate. diff --git a/android/app/build.gradle b/android/app/build.gradle index 923f82e0a..4f22c6aa2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,6 +22,7 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'com.google.gms.google-services' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" @@ -43,7 +44,7 @@ android { } defaultConfig { - applicationId "chat.fluffy.fluffychat" + applicationId "com.talktolearn.chat" minSdkVersion 19 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() diff --git a/android/app/google-services.json b/android/app/google-services.json index e33988c40..565c634cb 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -1,40 +1,40 @@ { "project_info": { - "project_number": "865731724731", - "project_id": "fluffychat-ef3e8", - "storage_bucket": "fluffychat-ef3e8.appspot.com" + "project_number": "545984292675", + "project_id": "pangea-chat-936ee", + "storage_bucket": "pangea-chat-936ee.appspot.com" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:865731724731:android:ec427b3b1dcd4a1e64309e", + "mobilesdk_app_id": "1:545984292675:android:d808acce7a80c20bb931f6", "android_client_info": { - "package_name": "chat.fluffy.fluffychat" + "package_name": "com.talktolearn.chat" } }, "oauth_client": [ { - "client_id": "865731724731-od6969v178ul9970elgacpt936v5t7qg.apps.googleusercontent.com", + "client_id": "545984292675-2amsnoan1mt6lec1fld1a7eagu6gej7o.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyBLdZpGSPjcinikB4lAU6awW_h88NG17Sg" + "current_key": "AIzaSyAyWBbl83WXzbVr6txyCmlUsZhpWomQfdg" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "865731724731-od6969v178ul9970elgacpt936v5t7qg.apps.googleusercontent.com", + "client_id": "545984292675-2amsnoan1mt6lec1fld1a7eagu6gej7o.apps.googleusercontent.com", "client_type": 3 }, { - "client_id": "865731724731-ofdr7e6m04murgb1bvchlj9oaos0q5i3.apps.googleusercontent.com", + "client_id": "545984292675-f5p76l3h9sibsonrct7a8l9ca3c69at0.apps.googleusercontent.com", "client_type": 2, "ios_info": { - "bundle_id": "im.fluffychat.app" + "bundle_id": "com.talktolearn.chat" } } ] diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 4a2a1f5d7..be6ce4442 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.talktolearn.chat"> + + + - + @@ -104,16 +108,18 @@ - + - + + diff --git a/android/app/src/main/kotlin/com/talktolearn/chat/FcmPushService.kt b/android/app/src/main/kotlin/com/talktolearn/chat/FcmPushService.kt new file mode 100644 index 000000000..ad301c9f0 --- /dev/null +++ b/android/app/src/main/kotlin/com/talktolearn/chat/FcmPushService.kt @@ -0,0 +1,36 @@ +/*package com.talktolearn.chat + +import com.famedly.fcm_shared_isolate.FcmSharedIsolateService + +import com.talktolearn.chat.MainActivity + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.view.FlutterMain +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.WindowManager + +class FcmPushService : FcmSharedIsolateService() { + override fun getEngine(): FlutterEngine { + return provideEngine(getApplicationContext()) + } + + companion object { + fun provideEngine(context: Context): FlutterEngine { + var engine = MainActivity.engine + if (engine == null) { + engine = MainActivity.provideEngine(context) + engine.getLocalizationPlugin().sendLocalesToFlutter( + context.getResources().getConfiguration()) + engine.getDartExecutor().executeDartEntrypoint( + DartEntrypoint.createDefault()) + } + return engine + } + } +} +*/ \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/talktolearn/chat/MainActivity.kt b/android/app/src/main/kotlin/com/talktolearn/chat/MainActivity.kt new file mode 100644 index 000000000..77d3c6e32 --- /dev/null +++ b/android/app/src/main/kotlin/com/talktolearn/chat/MainActivity.kt @@ -0,0 +1,33 @@ +package com.talktolearn.chat + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +import android.content.Context +import androidx.multidex.MultiDex + +class MainActivity : FlutterActivity() { + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(base) + MultiDex.install(this) + } + + + override fun provideFlutterEngine(context: Context): FlutterEngine? { + return provideEngine(this) + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + // do nothing, because the engine was been configured in provideEngine + } + + companion object { + var engine: FlutterEngine? = null + fun provideEngine(context: Context): FlutterEngine { + val eng = engine ?: FlutterEngine(context, emptyArray(), true, false) + engine = eng + return eng + } + } +} diff --git a/android/app/src/main/kotlin/com/talktolearn/chat/UnifiedPushService.kt b/android/app/src/main/kotlin/com/talktolearn/chat/UnifiedPushService.kt new file mode 100644 index 000000000..3c1696e65 --- /dev/null +++ b/android/app/src/main/kotlin/com/talktolearn/chat/UnifiedPushService.kt @@ -0,0 +1,23 @@ +package com.talktolearn.chat + +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor +import org.unifiedpush.flutter.connector.UnifiedPushReceiver + +import android.content.Context + +class UnifiedPushReceiver : UnifiedPushReceiver() { + override fun getEngine(context: Context): FlutterEngine { + var engine = MainActivity.engine + if (engine == null) { + engine = MainActivity.provideEngine(context) + engine.localizationPlugin.sendLocalesToFlutter( + context.resources.configuration + ) + engine.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + } + return engine + } +} \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index 4a2a1f5d7..be6ce4442 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.talktolearn.chat"> + + diff --git a/l10n.yaml b/l10n.yaml index a714613ee..bb2dd7632 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -3,3 +3,4 @@ template-arb-file: intl_en.arb output-localization-file: l10n.dart output-class: L10n preferred-supported-locales: ["en"] +untranslated-messages-file: needed-translations.txt diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index f078fdf08..3edcc3fef 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,13 +1,20 @@ import 'dart:ui'; +import 'package:fluffychat/pangea/config/environment.dart'; import 'package:matrix/matrix.dart'; abstract class AppConfig { - static String _applicationName = 'FluffyChat'; + // #Pangea + // static String _applicationName = 'FluffyChat'; + static String _applicationName = 'Pangea Chat'; + // #Pangea static String get applicationName => _applicationName; static String? _applicationWelcomeMessage; static String? get applicationWelcomeMessage => _applicationWelcomeMessage; - static String _defaultHomeserver = 'matrix.org'; + // #Pangea + // static String _defaultHomeserver = 'matrix.org'; + static String _defaultHomeserver = Environment.synapsURL; + // #Pangea static String get defaultHomeserver => _defaultHomeserver; static double fontSizeFactor = 1; static const Color chatColor = primaryColor; @@ -15,24 +22,37 @@ abstract class AppConfig { static const double messageFontSize = 15.75; static const bool allowOtherHomeservers = true; static const bool enableRegistration = true; - static const Color primaryColor = Color(0xFF5625BA); - static const Color primaryColorLight = Color(0xFFCCBDEA); + // #Pangea + // static const Color primaryColor = Color(0xFF5625BA); + // static const Color primaryColorLight = Color(0xFFCCBDEA); + static const Color primaryColor = Color(0xFF8560E0); + static const Color primaryColorLight = Color(0xFFDBC9FF); static const Color secondaryColor = Color(0xFF41a2bc); - static String _privacyUrl = - 'https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md'; + static const Color activeToggleColor = Color(0xFF33D057); + // static String _privacyUrl = + // 'https://gitlab.com/famedly/fluffychat/-/blob/main/PRIVACY.md'; + static String _privacyUrl = "https://www.pangeachat.com/privacy"; + //Pangea# static String get privacyUrl => _privacyUrl; static const String enablePushTutorial = 'https://github.com/krille-chan/fluffychat/wiki/Push-Notifications-without-Google-Services'; static const String encryptionTutorial = 'https://github.com/krille-chan/fluffychat/wiki/How-to-use-end-to-end-encryption-in-FluffyChat'; static const String appId = 'im.fluffychat.FluffyChat'; - static const String appOpenUrlScheme = 'im.fluffychat'; + // #Pangea + // static const String appOpenUrlScheme = 'im.fluffychat'; + static const String appOpenUrlScheme = 'matrix.pangea.chat'; static String _webBaseUrl = 'https://fluffychat.im/web'; + // Pangea# static String get webBaseUrl => _webBaseUrl; - static const String sourceCodeUrl = - 'https://github.com/krille-chan/fluffychat'; - static const String supportUrl = - 'https://github.com/krille-chan/fluffychat/issues'; + //#Pangea + static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat'; + // static const String supportUrl = + // 'https://gitlab.com/famedly/fluffychat/issues'; + static const String supportUrl = 'https://www.pangeachat.com/faqs'; + static const String termsOfServiceUrl = + 'https://www.pangeachat.com/terms-of-service'; + //Pangea# static final Uri newIssueUrl = Uri( scheme: 'https', host: 'github.com', @@ -41,7 +61,10 @@ abstract class AppConfig { static const bool enableSentry = true; static const String sentryDns = 'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143'; - static bool renderHtml = true; + //#Pangea + static bool renderHtml = false; + // static bool renderHtml = true; + //Pangea# static bool hideRedactedEvents = false; static bool hideUnknownEvents = true; static bool hideUnimportantStateEvents = true; @@ -49,27 +72,55 @@ abstract class AppConfig { static bool separateChatTypes = false; static bool autoplayImages = true; static bool sendTypingNotifications = true; - static bool sendOnEnter = false; + //#Pangea + static bool sendOnEnter = true; + // static bool sendOnEnter = false; + //Pangea# static bool experimentalVoip = false; static const bool hideTypingUsernames = false; static const bool hideAllStateEvents = false; static const String inviteLinkPrefix = 'https://matrix.to/#/'; static const String deepLinkPrefix = 'im.fluffychat://chat/'; static const String schemePrefix = 'matrix:'; - static const String pushNotificationsChannelId = 'fluffychat_push'; - static const String pushNotificationsChannelName = 'FluffyChat push channel'; + // #Pangea + // static const String pushNotificationsChannelId = 'fluffychat_push'; + // static const String pushNotificationsChannelName = 'FluffyChat push channel'; + // static const String pushNotificationsChannelDescription = + // 'Push notifications for FluffyChat'; + // static const String pushNotificationsAppId = 'chat.fluffy.fluffychat'; + // static const String pushNotificationsGatewayUrl = + // 'https://push.fluffychat.im/_matrix/push/v1/notify'; + // static const String pushNotificationsPusherFormat = 'event_id_only'; + static const String pushNotificationsChannelId = 'pangeachat_push'; + static const String pushNotificationsChannelName = 'Pangea Chat push channel'; static const String pushNotificationsChannelDescription = - 'Push notifications for FluffyChat'; - static const String pushNotificationsAppId = 'chat.fluffy.fluffychat'; + 'Push notifications for Pangea Chat'; + static const String pushNotificationsAppId = 'com.talktolearn.chat'; static const String pushNotificationsGatewayUrl = - 'https://push.fluffychat.im/_matrix/push/v1/notify'; - static const String pushNotificationsPusherFormat = 'event_id_only'; + 'https://sygnal.pangea.chat/_matrix/push/v1/notify'; + static const String? pushNotificationsPusherFormat = null; + // Pangea# static const String emojiFontName = 'Noto Emoji'; static const String emojiFontUrl = 'https://github.com/googlefonts/noto-emoji/'; static const double borderRadius = 16.0; static const double columnWidth = 360.0; + // #Pangea + static String googlePlayMangementUrl = + "https://play.google.com/store/account/subscriptions"; + static String googlePlayHistoryUrl = + "https://play.google.com/store/account/orderhistory"; + static String googlePlayPaymentMethodUrl = + "https://play.google.com/store/paymentmethods"; + static String appleMangementUrl = + "https://apps.apple.com/account/subscriptions"; + static String stripePerMonth = + "https://buy.stripe.com/test_bIY6ssd8z5Uz8ec8ww"; + static String iosPromoCode = + "https://apps.apple.com/redeem?ctx=offercodes&id=1445118630&code="; + // Pangea# + static void loadFromJson(Map json) { if (json['chat_color'] != null) { try { @@ -97,7 +148,11 @@ abstract class AppConfig { _privacyUrl = json['web_base_url']; } if (json['render_html'] is bool) { - renderHtml = json['render_html']; + // #Pangea + // this is interfering with our PangeaRichText functionality, removing it for now + renderHtml = false; + // renderHtml = json['render_html']; + // Pangea# } if (json['hide_redacted_events'] is bool) { hideRedactedEvents = json['hide_redacted_events']; diff --git a/lib/config/firebase_options.dart b/lib/config/firebase_options.dart new file mode 100644 index 000000000..3bacf8c57 --- /dev/null +++ b/lib/config/firebase_options.dart @@ -0,0 +1,91 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyAXK_jobz9YjNmiS1leA-vbGd_a8W-TCGI', + appId: '1:545984292675:web:80f3babc12328eddb931f6', + messagingSenderId: '545984292675', + projectId: 'pangea-chat-936ee', + authDomain: 'pangea-chat-936ee.firebaseapp.com', + databaseURL: 'https://pangea-chat-936ee-default-rtdb.firebaseio.com', + storageBucket: 'pangea-chat-936ee.appspot.com', + measurementId: 'G-FKP13VDEBX', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAyWBbl83WXzbVr6txyCmlUsZhpWomQfdg', + appId: '1:545984292675:android:d808acce7a80c20bb931f6', + messagingSenderId: '545984292675', + projectId: 'pangea-chat-936ee', + databaseURL: 'https://pangea-chat-936ee-default-rtdb.firebaseio.com', + storageBucket: 'pangea-chat-936ee.appspot.com', + androidClientId: + '545984292675-2amsnoan1mt6lec1fld1a7eagu6gej7o.apps.googleusercontent.com' + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyCl8QZd9_PnaqJY2zLHCwlsmSWdq7hnH-U', + appId: '1:545984292675:ios:1226406ecc36e056b931f6', + messagingSenderId: '545984292675', + projectId: 'pangea-chat-936ee', + databaseURL: 'https://pangea-chat-936ee-default-rtdb.firebaseio.com', + storageBucket: 'pangea-chat-936ee.appspot.com', + iosClientId: + '545984292675-f5p76l3h9sibsonrct7a8l9ca3c69at0.apps.googleusercontent.com', + iosBundleId: 'com.talktolearn.chat', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyCl8QZd9_PnaqJY2zLHCwlsmSWdq7hnH-U', + appId: '1:545984292675:ios:1226406ecc36e056b931f6', + messagingSenderId: '545984292675', + projectId: 'pangea-chat-936ee', + databaseURL: 'https://pangea-chat-936ee-default-rtdb.firebaseio.com', + storageBucket: 'pangea-chat-936ee.appspot.com', + iosClientId: + '545984292675-f5p76l3h9sibsonrct7a8l9ca3c69at0.apps.googleusercontent.com', + iosBundleId: 'com.talktolearn.chat', + ); +} diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 2386fb3c9..7fca80f91 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -1,15 +1,9 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; - -import 'package:go_router/go_router.dart'; - import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/add_story/add_story.dart'; import 'package:fluffychat/pages/archive/archive.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; -import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_members/chat_members.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; @@ -30,24 +24,46 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/pages/settings_stories/settings_stories.dart'; import 'package:fluffychat/pages/settings_style/settings_style.dart'; -import 'package:fluffychat/pages/story/story_page.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'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; +import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart'; +import 'package:fluffychat/pangea/pages/sign_up/signup.dart'; +import 'package:fluffychat/pangea/widgets/class/join_with_link.dart'; import 'package:fluffychat/widgets/layouts/empty_page.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/log_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +import '../pangea/pages/analytics/class_analytics/class_analytics.dart'; +import '../pangea/pages/analytics/class_list/class_list.dart'; abstract class AppRoutes { static FutureOr loggedInRedirect( BuildContext context, GoRouterState state, - ) => - Matrix.of(context).client.isLogged() ? '/rooms' : null; + ) { + // #Pangea + // Matrix.of(context).client.isLogged() ? '/rooms' : null; + return PAuthGaurd.loggedInRedirect(context, state); + // Pangea# + } static FutureOr loggedOutRedirect( BuildContext context, GoRouterState state, - ) => - Matrix.of(context).client.isLogged() ? null : '/home'; + ) { + // #Pangea + // Matrix.of(context).client.isLogged() ? null : '/home'; + return PAuthGaurd.loggedOutRedirect(context, state); + // Pangea# + } AppRoutes(); @@ -73,6 +89,16 @@ abstract class AppRoutes { ), redirect: loggedInRedirect, ), + // #Pangea + GoRoute( + path: 'signup', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const SignupPage(), + ), + redirect: loggedInRedirect, + ), + // Pangea# ], ), GoRoute( @@ -100,6 +126,26 @@ abstract class AppRoutes { : child, ), routes: [ + // #Pangea + GoRoute( + path: '/spaces/:roomid', + pageBuilder: (context, state) => defaultPageBuilder( + context, + ChatDetails( + roomId: state.pathParameters['roomid']!, + ), + ), + redirect: loggedOutRedirect, + ), + GoRoute( + path: '/join_with_link', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const JoinClassWithLink(), + ), + redirect: loggedOutRedirect, + ), + // Pangea# GoRoute( path: '/rooms', redirect: loggedOutRedirect, @@ -112,32 +158,68 @@ abstract class AppRoutes { ), ), routes: [ + // #Pangea GoRoute( - path: 'stories/create', + path: 'user_age', pageBuilder: (context, state) => defaultPageBuilder( context, - const AddStoryPage(), + const PUserAge(), ), redirect: loggedOutRedirect, ), GoRoute( - path: 'stories/:roomid', + path: 'mylearning', pageBuilder: (context, state) => defaultPageBuilder( context, - const StoryPage(), + const StudentAnalyticsPage(), + ), + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'analytics', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const AnalyticsClassList(), ), redirect: loggedOutRedirect, routes: [ GoRoute( - path: 'share', + path: ':classid', + redirect: loggedOutRedirect, pageBuilder: (context, state) => defaultPageBuilder( context, - const AddStoryPage(), + const ClassAnalyticsPage(), ), - redirect: loggedOutRedirect, ), ], ), + // GoRoute( + // path: 'stories/create', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // const AddStoryPage(), + // ), + // redirect: loggedOutRedirect, + // ), + // GoRoute( + // path: 'stories/:roomid', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // const StoryPage(), + // ), + // redirect: loggedOutRedirect, + // routes: [ + // GoRoute( + // path: 'share', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // const AddStoryPage(), + // ), + // redirect: loggedOutRedirect, + // ), + // ], + // ), + // Pangea# GoRoute( path: 'archive', pageBuilder: (context, state) => defaultPageBuilder( @@ -167,7 +249,10 @@ abstract class AppRoutes { redirect: loggedOutRedirect, ), GoRoute( - path: 'newgroup', + // #Pangea + // path: 'newgroup', + path: 'newgroup/:spaceid', + // Pangea# pageBuilder: (context, state) => defaultPageBuilder( context, const NewGroup(), @@ -182,6 +267,32 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, ), + // #Pangea + GoRoute( + path: 'newspace/:newexchange', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const NewSpace(), + ), + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'join_exchange/:exchangeid', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const AddExchangeToClass(), + ), + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'partner', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const FindPartner(), + ), + redirect: loggedOutRedirect, + ), + // Pangea# ShellRoute( pageBuilder: (context, state, child) => defaultPageBuilder( context, @@ -244,24 +355,26 @@ abstract class AppRoutes { ], redirect: loggedOutRedirect, ), - GoRoute( - path: 'addaccount', - redirect: loggedOutRedirect, - pageBuilder: (context, state) => defaultPageBuilder( - context, - const HomeserverPicker(), - ), - routes: [ - GoRoute( - path: 'login', - pageBuilder: (context, state) => defaultPageBuilder( - context, - const Login(), - ), - redirect: loggedOutRedirect, - ), - ], - ), + // #Pangea + // GoRoute( + // path: 'addaccount', + // redirect: loggedOutRedirect, + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // const HomeserverPicker(), + // ), + // routes: [ + // GoRoute( + // path: 'login', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // const Login(), + // ), + // redirect: loggedOutRedirect, + // ), + // ], + // ), + // Pangea# GoRoute( path: 'security', redirect: loggedOutRedirect, @@ -296,6 +409,24 @@ abstract class AppRoutes { ), ], ), + // #Pangea + GoRoute( + path: 'learning', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const SettingsLearning(), + ), + redirect: loggedOutRedirect, + ), + GoRoute( + path: 'subscription', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const SubscriptionManagement(), + ), + redirect: loggedOutRedirect, + ), + // Pangea# ], redirect: loggedOutRedirect, ), @@ -309,14 +440,16 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, routes: [ - GoRoute( - path: 'encryption', - pageBuilder: (context, state) => defaultPageBuilder( - context, - const ChatEncryptionSettings(), - ), - redirect: loggedOutRedirect, - ), + // #Pangea + // GoRoute( + // path: 'encryption', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // const ChatEncryptionSettings(), + // ), + // redirect: loggedOutRedirect, + // ), + // Pangea# GoRoute( path: 'invite', pageBuilder: (context, state) => defaultPageBuilder( @@ -354,6 +487,16 @@ abstract class AppRoutes { ), redirect: loggedOutRedirect, ), + // #Pangea + GoRoute( + path: 'class_settings', + pageBuilder: (context, state) => defaultPageBuilder( + context, + const ClassSettingsPage(), + ), + redirect: loggedOutRedirect, + ), + // Pangea# GoRoute( path: 'invite', pageBuilder: (context, state) => defaultPageBuilder( @@ -391,6 +534,17 @@ abstract class AppRoutes { ], redirect: loggedOutRedirect, ), + // GoRoute( + // path: 'tasks', + // pageBuilder: (context, state) => defaultPageBuilder( + // context, + // TasksPage( + // room: Matrix.of(context) + // .client + // .getRoomById(state.pathParameters['roomid']!)!, + // ), + // ), + // ), ], ), ], diff --git a/lib/config/themes.dart b/lib/config/themes.dart index f03c74d4f..3df0be18c 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -1,7 +1,8 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'app_config.dart'; abstract class FluffyThemes { @@ -140,6 +141,11 @@ abstract class FluffyThemes { ), ), ), + // #Pangea + cupertinoOverrideTheme: const CupertinoThemeData( + textTheme: CupertinoTextThemeData(), + ), + // Pangea# ); } } diff --git a/lib/main.dart b/lib/main.dart index 1adfa077c..b5ea0cf2a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,17 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; + import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; @@ -14,6 +19,23 @@ import 'widgets/fluffy_chat_app.dart'; void main() async { Logs().i('Welcome to ${AppConfig.applicationName} <3'); + // #Pangea + await dotenv.load(fileName: Environment.fileName); + + await Future.wait([ + ErrorHandler.initialize(), + PangeaLanguage.initialize(), + GoogleAnalytics.initialize(), + ]); + + /// + /// PangeaLanguage must be initialized before the runApp + /// Then where ever you need language functions simply call PangeaLanguage pangeaLanguage = PangeaLanguage() + /// pangeaLanguage.getList or whatever function you need + /// + await GetStorage.init(); + // Pangea# + // Our background push shared isolate accesses flutter-internal things very early in the startup proccess // To make sure that the parts of flutter needed are started up already, we need to ensure that the // widget bindings are initialized already. diff --git a/lib/pages/add_story/add_story.dart b/lib/pages/add_story/add_story.dart index 323d103b4..7a7421ad9 100644 --- a/lib/pages/add_story/add_story.dart +++ b/lib/pages/add_story/add_story.dart @@ -1,16 +1,8 @@ import 'dart:io'; import 'dart:math'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:matrix/matrix.dart'; -import 'package:video_player/video_player.dart'; - import 'package:fluffychat/pages/add_story/add_story_view.dart'; import 'package:fluffychat/pages/add_story/invite_story_page.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; @@ -19,6 +11,13 @@ import 'package:fluffychat/utils/story_theme_data.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; +import 'package:video_player/video_player.dart'; + import '../../utils/matrix_sdk_extensions/client_stories_extension.dart'; class AddStoryPage extends StatefulWidget { diff --git a/lib/pages/archive/archive_view.dart b/lib/pages/archive/archive_view.dart index d0df690b8..e3d2c29cd 100644 --- a/lib/pages/archive/archive_view.dart +++ b/lib/pages/archive/archive_view.dart @@ -1,11 +1,9 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/archive/archive.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; class ArchiveView extends StatelessWidget { final ArchiveController controller; diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 20128b069..39caa4fe8 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -1,6 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -8,9 +10,6 @@ import 'package:matrix/encryption.dart'; import 'package:matrix/encryption/utils/bootstrap.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import '../../utils/adaptive_bottom_sheet.dart'; import '../key_verification/key_verification_dialog.dart'; diff --git a/lib/pages/chat/add_widget_tile_view.dart b/lib/pages/chat/add_widget_tile_view.dart index 7ce441e84..7cb448858 100644 --- a/lib/pages/chat/add_widget_tile_view.dart +++ b/lib/pages/chat/add_widget_tile_view.dart @@ -1,10 +1,8 @@ +import 'package:fluffychat/pages/chat/add_widget_tile.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pages/chat/add_widget_tile.dart'; - class AddWidgetTileView extends StatelessWidget { final AddWidgetTileState controller; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index e17226fae..77fe8a923 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,15 +1,42 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat_view.dart'; +import 'package:fluffychat/pages/chat/event_info_dialog.dart'; +import 'package:fluffychat/pages/chat/recording_dialog.dart'; +import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/use_type.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/utils/instructions.dart'; +import 'package:fluffychat/pangea/utils/report_message.dart'; +import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.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'; @@ -19,19 +46,6 @@ import 'package:record/record.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat/chat_view.dart'; -import 'package:fluffychat/pages/chat/event_info_dialog.dart'; -import 'package:fluffychat/pages/chat/recording_dialog.dart'; -import 'package:fluffychat/pages/chat_details/chat_details.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -import 'package:fluffychat/utils/error_reporter.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart'; @@ -104,6 +118,10 @@ class ChatPageWithRoom extends StatefulWidget { } class ChatController extends State { + // #Pangea + final PangeaController pangeaController = MatrixState.pangeaController; + late Choreographer choreographer = Choreographer(pangeaController, this); + // Pangea# Room get room => sendingClient.getRoomById(roomId) ?? widget.room; late Client sendingClient; @@ -148,6 +166,9 @@ class ChatController extends State { ).detectFileType, ); } + // #Pangea + if (matrixFiles.isEmpty) return; + // Pangea# await showDialog( context: context, @@ -210,7 +231,10 @@ class ChatController extends State { .firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false); await room.leave(); await waitForSync; - return await client.startDirectChat(userId); + //#Pangea + // return await client.startDirectChat(userId); + return await client.startDirectChat(userId, enableEncryption: false); + //Pangea# }, ); final roomId = success.result; @@ -229,7 +253,10 @@ class ChatController extends State { EmojiPickerType emojiPickerType = EmojiPickerType.keyboard; - void requestHistory() async { + // #Pangea + // void requestHistory() async { + Future requestHistory() async { + // #Pangea if (!timeline!.canRequestHistory) return; Logs().v('Requesting history...'); try { @@ -290,6 +317,10 @@ class ChatController extends State { } } + // #Pangea + bool showPermissionsError = false; + // #Pangea + @override void initState() { scrollController.addListener(_updateScrollController); @@ -297,6 +328,47 @@ class ChatController extends State { _loadDraft(); super.initState(); sendingClient = Matrix.of(context).client; + // #Pangea + if (!mounted) return; + Future.delayed(const Duration(seconds: 1), () async { + if (!mounted) return; + debugPrint( + "chat.dart l1 ${pangeaController.languageController.activeL1Code(roomID: roomId)}", + ); + debugPrint( + "chat.dart l2 ${pangeaController.languageController.activeL2Code(roomID: roomId)}", + ); + if (mounted) { + pangeaController.languageController.showDialogOnEmptyLanguage( + context, + () => Future.delayed( + Duration.zero, + () => setState( + () {}, + ), + ), + ); + } + await Matrix.of(context).client.roomsLoading; + choreographer.setRoomId(roomId); + choreographer.messageOptions.resetSelectedDisplayLang(); + choreographer.stateListener.stream.listen((event) { + debugPrint("chat.dart choreo event $event"); + setState(() {}); + }); + showPermissionsError = !pangeaController.permissionsController + .isToolEnabled(ToolSetting.interactiveTranslator, room) || + !pangeaController.permissionsController + .isToolEnabled(ToolSetting.interactiveGrammar, room); + }); + + Future.delayed( + const Duration(seconds: 5), + () { + if (mounted) setState(() => showPermissionsError = false); + }, + ); + // Pangea# _tryLoadTimeline(); } @@ -351,6 +423,27 @@ class ChatController extends State { onUpdate: updateView, eventContextId: eventContextId, ); + // #Pangea + List? messageEvents = + timeline?.events.where((x) => x.type == 'm.room.message').toList(); + if (messageEvents != null && messageEvents.length < 10) { + int prevNumEvents = timeline!.events.length; + await requestHistory(); + messageEvents = + timeline?.events.where((x) => x.type == 'm.room.message').toList(); + int numRequests = 0; + while (timeline!.events.length > prevNumEvents && + messageEvents!.length < 10 && + numRequests <= 5) { + prevNumEvents = timeline!.events.length; + await requestHistory(); + messageEvents = timeline?.events + .where((x) => x.type == 'm.room.message') + .toList(); + numRequests++; + } + } + // #Pangea } catch (e, s) { Logs().w('Unable to load timeline on event ID $eventContextId', e, s); if (!mounted) return; @@ -408,10 +501,17 @@ class ChatController extends State { timeline?.cancelSubscriptions(); timeline = null; inputFocus.removeListener(_inputFocusListener); + //#Pangea + choreographer.stateListener.close(); + choreographer.dispose(); + //Pangea# super.dispose(); } - TextEditingController sendController = TextEditingController(); + // #Pangea + // TextEditingController sendController = TextEditingController(); + PangeaTextController get sendController => choreographer.textController; + // #Pangea void setSendingClient(Client c) { // first cancel typing with the old sending client @@ -439,7 +539,20 @@ class ChatController extends State { Matrix.of(context).setActiveClient(c); }); - Future send() async { + // #Pangea + // Future send() async { + // Original send function gets the tx id within the matrix lib, + // but for choero, the tx id is generated before the message send. + // Also, adding PangeaMessageData + Future send({ + PangeaRepresentation? originalSent, + PangeaRepresentation? originalWritten, + PangeaMessageTokens? tokensSent, + PangeaMessageTokens? tokensWritten, + ChoreoRecord? choreo, + UseType? useType, + }) async { + // Pangea# if (sendController.text.trim().isEmpty) return; _storeInputTimeoutTimer?.cancel(); final prefs = await SharedPreferences.getInstance(); @@ -463,12 +576,70 @@ class ChatController extends State { } // ignore: unawaited_futures - room.sendTextEvent( + // #Pangea + // room.sendTextEvent( + // sendController.text, + // inReplyTo: replyEvent, + // editEventId: editEvent?.eventId, + // parseCommands: parseCommands, + // ); + room + .pangeaSendTextEvent( sendController.text, inReplyTo: replyEvent, editEventId: editEvent?.eventId, parseCommands: parseCommands, + originalSent: originalSent, + originalWritten: originalWritten, + tokensSent: tokensSent, + tokensWritten: tokensWritten, + choreo: choreo, + useType: useType, + ) + //#Pangea + .then( + (String? msgEventId) { + GoogleAnalytics.sendMessage( + room.id, + room.classCode, + useType ?? UseType.un, + ); + + if (msgEventId == null) { + ErrorHandler.logError( + e: Exception('msgEventId is null'), + s: StackTrace.current, + ); + return; + } + + pangeaController.myAnalytics.handleMessage( + room, + RecentMessageRecord( + eventId: msgEventId, + chatId: room.id, + useType: useType ?? UseType.un, + time: DateTime.now(), + ), + ); + + if (choreo != null && + tokensSent != null && + originalSent?.langCode == + pangeaController.languageController + .activeL2Code(roomID: room.id)) { + pangeaController.myAnalytics.saveConstructsMixed( + [ + // ...choreo.toVocabUse(tokensSent.tokens, room.id, msgEventId), + ...choreo.toGrammarConstructUse(msgEventId, room.id), + ], + originalSent!.langCode, + ); + } + }, + onError: (err, stack) => ErrorHandler.logError(e: err, s: stack), ); + // Pangea# sendController.value = TextEditingValue( text: pendingText, selection: const TextSelection.collapsed(offset: 0), @@ -669,6 +840,11 @@ class ChatController extends State { } void emojiPickerAction() { + // #Pangea + if (choreographer.itController.isOpen) { + return; + } + // Pangea# if (showEmojiPicker) { inputFocus.requestFocus(); } else { @@ -751,16 +927,36 @@ class ChatController extends State { textFields: [DialogTextField(hintText: L10n.of(context)!.reason)], ); if (reason == null || reason.single.isEmpty) return; - final result = await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context).client.reportContent( - event.roomId!, - event.eventId, - reason: reason.single, - score: score, + // #Pangea + try { + await reportMessage( + context, + roomId, + reason.single, + event.senderId, + event.content['body'].toString(), + ); + } catch (err) { + ErrorHandler.logError(e: err, s: StackTrace.current); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.oopsSomethingWentWrong, ), - ); - if (result.error != null) return; + ), + ); + } + // final result = await showFutureLoadingDialog( + // context: context, + // future: () => Matrix.of(context).client.reportContent( + // event.roomId!, + // event.eventId, + // reason: reason.single, + // score: score, + // ), + // ); + // if (result.error != null) return; + // Pangea# setState(() { showEmojiPicker = false; selectedEvents.clear(); @@ -1019,6 +1215,9 @@ class ChatController extends State { void clearSelectedEvents() => setState(() { selectedEvents.clear(); showEmojiPicker = false; + //#Pangea + choreographer.messageOptions.resetSelectedDisplayLang(); + //Pangea# }); void clearSingleSelectedEvent() { @@ -1084,6 +1283,16 @@ class ChatController extends State { } void onSelectMessage(Event event) { + // #Pangea + if (choreographer.itController.isOpen) { + return; + } + pangeaController.instructions.show( + context, + InstructionsEnum.understandingMessages, + event.eventId, + ); + // Pangea# if (!event.redacted) { if (selectedEvents.contains(event)) { setState( @@ -1118,12 +1327,22 @@ class ChatController extends State { return index + 1; } - void onInputBarSubmitted(_) { - send(); + // #Pangea + void onInputBarSubmitted(String _, BuildContext context) { + // void onInputBarSubmitted(_) { + // send(); + choreographer.send(context); + // Pangea# FocusScope.of(context).requestFocus(inputFocus); } - void onAddPopupMenuButtonSelected(String choice) { + //#Pangea + void onAddPopupMenuButtonSelected(String? choice) { + // void onAddPopupMenuButtonSelected(String choice) { + if (choice == null) { + debugger(when: kDebugMode); + } + //Pangea# if (choice == 'file') { sendFileAction(); } @@ -1299,6 +1518,37 @@ class ChatController extends State { editEvent = null; }); + // #Pangea + double? availableSpace; + double? inputRowSize; + bool? lastState; + bool get isRowScrollable { + if (availableSpace == null || inputRowSize == null) { + if (lastState == null) { + lastState = false; + Future.delayed(Duration.zero, () { + setState(() {}); + }); + } + return false; + } + const double offSetValue = 10; + final bool currentState = inputRowSize! > (availableSpace! - offSetValue); + if (!lastState! && currentState) { + Future.delayed(Duration.zero, () { + setState(() {}); + }); + } + if (lastState! && !currentState) { + Future.delayed(Duration.zero, () { + setState(() {}); + }); + } + lastState = currentState; + return currentState; + } + // #Pangea + @override Widget build(BuildContext context) => ChatView(this); } diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index ae659fa06..b8ad6fbda 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -1,11 +1,9 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; - import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; class ChatAppBarTitle extends StatelessWidget { final ChatController controller; diff --git a/lib/pages/chat/chat_emoji_picker.dart b/lib/pages/chat/chat_emoji_picker.dart index aaad97403..863f547ff 100644 --- a/lib/pages/chat/chat_emoji_picker.dart +++ b/lib/pages/chat/chat_emoji_picker.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/config/themes.dart'; import 'chat.dart'; class ChatEmojiPicker extends StatelessWidget { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 85cf557a2..40d17a6d0 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,17 +1,17 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; -import 'package:scroll_to_index/scroll_to_index.dart'; - import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; class ChatEventList extends StatelessWidget { final ChatController controller; @@ -76,6 +76,14 @@ class ChatEventList extends StatelessWidget { ); } + // #Pangea + if (i == 1) { + return controller.room.locked && !controller.room.isRoomAdmin + ? const LockedChatMessage() + : const SizedBox.shrink(); + } + // Pangea# + // Request history button or progress indicator: if (i == controller.timeline!.events.length + 1) { if (controller.timeline!.isRequestingHistory) { @@ -102,11 +110,17 @@ class ChatEventList extends StatelessWidget { } // The message at this index: - final event = controller.timeline!.events[i - 1]; + // #Pangea + // final event = controller.timeline!.events[i - 1]; + final event = controller.timeline!.events[i - 2]; + // Pangea# return AutoScrollTag( key: ValueKey(event.eventId), - index: i - 1, + // #Pangea + // index: i - 1, + index: i - 2, + // Pangea# controller: controller.scrollController, child: event.isVisibleInGui ? Message( @@ -126,7 +140,13 @@ class ChatEventList extends StatelessWidget { onSelect: controller.onSelectMessage, scrollToEventId: (String eventId) => controller.scrollToEventId(eventId), - longPressSelect: controller.selectedEvents.isNotEmpty, + // #Pangea + // longPressSelect: controller.selectedEvents.isEmpty, + selectedDisplayLang: controller + .choreographer.messageOptions.selectedDisplayLang, + immersionMode: controller.choreographer.immersionMode, + definitions: controller.choreographer.definitionsEnabled, + // Pangea# selected: controller.selectedEvents .any((e) => e.eventId == event.eventId), timeline: controller.timeline!, diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 7d2d083a9..659201814 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,15 +1,16 @@ +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/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; import 'chat.dart'; import 'input_bar.dart'; @@ -25,252 +26,302 @@ class ChatInputRow extends StatelessWidget { controller.emojiPickerType == EmojiPickerType.reaction) { return const SizedBox.shrink(); } - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: controller.selectMode - ? [ - SizedBox( - height: 56, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context)!.forward), - ], - ), - ), - ), - controller.selectedEvents.length == 1 - ? controller.selectedEvents.first - .getDisplayEvent(controller.timeline!) - .status - .isSent - ? SizedBox( - height: 56, - child: TextButton( - onPressed: controller.replyAction, - child: Row( - children: [ - Text(L10n.of(context)!.reply), - const Icon(Icons.keyboard_arrow_right), - ], - ), - ), - ) - : SizedBox( - height: 56, - child: TextButton( - onPressed: controller.sendAgainAction, - child: Row( - children: [ - Text(L10n.of(context)!.tryToSendAgain), - const SizedBox(width: 4), - const Icon(Icons.send_outlined, size: 16), - ], - ), - ), - ) - : const SizedBox.shrink(), - ] - : [ - KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.keyA, - }, - onKeysPressed: () => - controller.onAddPopupMenuButtonSelected('file'), - helpLabel: L10n.of(context)!.sendFile, - child: AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - height: 56, - width: controller.inputText.isEmpty ? 56 : 0, - alignment: Alignment.center, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: PopupMenuButton( - icon: const Icon(Icons.add_outlined), - onSelected: controller.onAddPopupMenuButtonSelected, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'file', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment_outlined), - ), - title: Text(L10n.of(context)!.sendFile), - contentPadding: const EdgeInsets.all(0), - ), + // #Pangea + return Column( + children: [ + ITBar( + choreographer: controller.choreographer, + ), + Row( + // crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + // Pangea# + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: controller.selectMode + ? [ + SizedBox( + height: 56, + child: TextButton( + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + const Icon(Icons.keyboard_arrow_left_outlined), + Text(L10n.of(context)!.forward), + ], ), - PopupMenuItem( - value: 'image', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image_outlined), - ), - title: Text(L10n.of(context)!.sendImage), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons.camera_alt_outlined), - ), - title: Text(L10n.of(context)!.openCamera), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera-video', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - child: Icon(Icons.videocam_outlined), - ), - title: Text(L10n.of(context)!.openVideoCamera), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (controller.room - .getImagePacks(ImagePackUsage.sticker) - .isNotEmpty) - PopupMenuItem( - value: 'sticker', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - child: Icon(Icons.emoji_emotions_outlined), - ), - title: Text(L10n.of(context)!.sendSticker), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'location', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.brown, - foregroundColor: Colors.white, - child: Icon(Icons.gps_fixed_outlined), - ), - title: Text(L10n.of(context)!.shareLocation), - contentPadding: const EdgeInsets.all(0), - ), - ), - ], + ), ), - ), - ), - Container( - height: 56, - alignment: Alignment.center, - child: KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.keyE, - }, - onKeysPressed: controller.emojiPickerAction, - helpLabel: L10n.of(context)!.emojis, - child: IconButton( - tooltip: L10n.of(context)!.emojis, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); + controller.selectedEvents.length == 1 + ? controller.selectedEvents.first + .getDisplayEvent(controller.timeline!) + .status + .isSent + ? SizedBox( + height: 56, + child: TextButton( + onPressed: controller.replyAction, + child: Row( + children: [ + Text(L10n.of(context)!.reply), + const Icon(Icons.keyboard_arrow_right), + ], + ), + ), + ) + : SizedBox( + height: 56, + child: TextButton( + onPressed: controller.sendAgainAction, + child: Row( + children: [ + Text(L10n.of(context)!.tryToSendAgain), + const SizedBox(width: 4), + const Icon(Icons.send_outlined, size: 16), + ], + ), + ), + ) + : const SizedBox.shrink(), + ] + : [ + KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.keyA, + }, + onKeysPressed: () => + controller.onAddPopupMenuButtonSelected('file'), + helpLabel: L10n.of(context)!.sendFile, + child: AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + height: 56, + //#Pangea + // width: controller.inputText.isEmpty ? 56 : 0, + width: controller.inputText.isEmpty && + controller.pangeaController.permissionsController + .showChatInputAddButton(controller.roomId) + ? 56 + : 0, + //Pangea# + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: PopupMenuButton( + icon: const Icon(Icons.add_outlined), + onSelected: controller.onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ + //#Pangea + if (controller.pangeaController.permissionsController + .canShareFile(controller.roomId)) + //Pangea# + PopupMenuItem( + value: 'file', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment_outlined), + ), + title: Text(L10n.of(context)!.sendFile), + contentPadding: const EdgeInsets.all(0), + ), + ), + //#Pangea + if (controller.pangeaController.permissionsController + .canSharePhoto(controller.roomId)) + //Pangea# + PopupMenuItem( + value: 'image', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image_outlined), + ), + title: Text(L10n.of(context)!.sendImage), + contentPadding: const EdgeInsets.all(0), + ), + ), + //#Pangea + // if (PlatformInfos.isMobile) + if (PlatformInfos.isMobile && + controller.pangeaController.permissionsController + .canSharePhoto(controller.roomId)) + //Pangea# + PopupMenuItem( + value: 'camera', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons.camera_alt_outlined), + ), + title: Text(L10n.of(context)!.openCamera), + contentPadding: const EdgeInsets.all(0), + ), + ), + //#Pangea + // if (PlatformInfos.isMobile) + if (PlatformInfos.isMobile && + controller.pangeaController.permissionsController + .canShareVideo(controller.roomId)) + //Pangea# + PopupMenuItem( + value: 'camera-video', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + child: Icon(Icons.videocam_outlined), + ), + title: Text(L10n.of(context)!.openVideoCamera), + contentPadding: const EdgeInsets.all(0), + ), + ), + if (controller.room + .getImagePacks(ImagePackUsage.sticker) + .isNotEmpty) + PopupMenuItem( + value: 'sticker', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + child: Icon(Icons.emoji_emotions_outlined), + ), + title: Text(L10n.of(context)!.sendSticker), + contentPadding: const EdgeInsets.all(0), + ), + ), + //#Pangea + // if (PlatformInfos.isMobile) + if (PlatformInfos.isMobile && + controller.pangeaController.permissionsController + .canShareLocation(controller.roomId)) + //Pangea# + PopupMenuItem( + value: 'location', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.brown, + foregroundColor: Colors.white, + child: Icon(Icons.gps_fixed_outlined), + ), + title: Text(L10n.of(context)!.shareLocation), + contentPadding: const EdgeInsets.all(0), + ), + ), + ], + ), + ), + ), + Container( + height: 56, + alignment: Alignment.center, + child: KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.altLeft, + LogicalKeyboardKey.keyE, }, - child: Icon( - controller.showEmojiPicker - ? Icons.keyboard - : Icons.emoji_emotions_outlined, - key: ValueKey(controller.showEmojiPicker), + onKeysPressed: controller.emojiPickerAction, + helpLabel: L10n.of(context)!.emojis, + child: IconButton( + tooltip: L10n.of(context)!.emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.emoji_emotions_outlined, + key: ValueKey(controller.showEmojiPicker), + ), + ), + onPressed: controller.emojiPickerAction, ), ), - onPressed: controller.emojiPickerAction, ), - ), - ), - if (Matrix.of(context).isMultiAccount && - Matrix.of(context).hasComplexBundles && - Matrix.of(context).currentBundle!.length > 1) - Container( - height: 56, - alignment: Alignment.center, - child: _ChatAccountPicker(controller), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: InputBar( - room: controller.room, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: - AppConfig.sendOnEnter ? TextInputAction.send : null, - onSubmitted: controller.onInputBarSubmitted, - onSubmitImage: controller.sendImageFromClipBoard, - focusNode: controller.inputFocus, - controller: controller.sendController, - decoration: InputDecoration( - hintText: L10n.of(context)!.writeAMessage, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, + // #Pangea + // if (Matrix.of(context).isMultiAccount && + // Matrix.of(context).hasComplexBundles && + // Matrix.of(context).currentBundle!.length > 1) + // Container( + // height: 56, + // alignment: Alignment.center, + // child: _ChatAccountPicker(controller), + // ), + // Pangea# + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: InputBar( + room: controller.room, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: + AppConfig.sendOnEnter ? TextInputAction.send : null, + // #Pangea + // onSubmitted: controller.onInputBarSubmitted, + onSubmitted: (String value) => + controller.onInputBarSubmitted(value, context), + // Pangea# + onSubmitImage: controller.sendImageFromClipBoard, + focusNode: controller.inputFocus, + controller: controller.sendController, + decoration: InputDecoration( + hintText: L10n.of(context)!.writeAMessage, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + onChanged: controller.onInputBarChanged, + ), ), - onChanged: controller.onInputBarChanged, ), - ), - ), - if (PlatformInfos.platformCanRecord && - controller.inputText.isEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context)!.voiceMessage, - icon: const Icon(Icons.mic_none_outlined), - onPressed: controller.voiceMessageAction, - ), - ), - if (!PlatformInfos.isMobile || controller.inputText.isNotEmpty) - Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - icon: const Icon(Icons.send_outlined), - onPressed: controller.send, - tooltip: L10n.of(context)!.send, - ), - ), - ], + if (PlatformInfos.platformCanRecord && + controller.inputText.isEmpty) + Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context)!.voiceMessage, + icon: const Icon(Icons.mic_none_outlined), + onPressed: controller.voiceMessageAction, + ), + ), + if (!PlatformInfos.isMobile || + controller.inputText.isNotEmpty) + // #Pangea + ChoreographerSendButton(controller: controller), + // Container( + // height: 56, + // alignment: Alignment.center, + // child: IconButton( + // icon: const Icon(Icons.send_outlined), + // onPressed: controller.send, + // tooltip: L10n.of(context)!.send, + // ), + // ), + // Pangea# + ], + ), + ], ); } } diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index a92d6ce88..d5567022a 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,25 +1,28 @@ -import 'package:flutter/material.dart'; - import 'package:badges/badges.dart'; import 'package:desktop_drop/desktop_drop.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/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; -import 'package:fluffychat/pages/chat/encryption_button.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pages/chat/tombstone_display.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/language_display_toggle.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; + import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; @@ -34,6 +37,9 @@ class ChatView extends StatelessWidget { List _appBarActions(BuildContext context) { if (controller.selectMode) { return [ + // #Pangea + LanguageDisplayToggle(controller: controller), + // Pangea# if (controller.canEditSelectedEvents) IconButton( icon: const Icon(Icons.edit_outlined), @@ -108,33 +114,40 @@ class ChatView extends StatelessWidget { ], ), ]; - } else if (controller.isArchived) { - return [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextButton.icon( - onPressed: controller.forgetRoom, - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, - ), - icon: const Icon(Icons.delete_forever_outlined), - label: Text(L10n.of(context)!.delete), - ), - ), - ]; - } else { - return [ - if (Matrix.of(context).voipPlugin != null && - controller.room.isDirectChat) - IconButton( - onPressed: controller.onPhoneButtonTap, - icon: const Icon(Icons.call_outlined), - tooltip: L10n.of(context)!.placeCall, - ), - EncryptionButton(controller.room), - ChatSettingsPopupMenu(controller.room, true), - ]; } + // #Pangea + // else if (controller.isArchived) { + // return [ + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: TextButton.icon( + // onPressed: controller.forgetRoom, + // style: TextButton.styleFrom( + // foregroundColor: Theme.of(context).colorScheme.error, + // ), + // icon: const Icon(Icons.delete_forever_outlined), + // label: Text(L10n.of(context)!.delete), + // ), + // ), + // ]; + // } + //else { + // return [ + // if (Matrix.of(context).voipPlugin != null && + // controller.room.isDirectChat) + // IconButton( + // onPressed: controller.onPhoneButtonTap, + // icon: const Icon(Icons.call_outlined), + // tooltip: L10n.of(context)!.placeCall, + // ), + // EncryptionButton(controller.room), + // ChatSettingsPopupMenu(controller.room, true), + // ]; + // } + return [ + ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat), + ]; + // Pangea# } @override @@ -183,7 +196,12 @@ class ChatView extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ) : UnreadRoomsBadge( - filter: (r) => r.id != controller.roomId, + filter: (r) => + r.id != controller.roomId + // #Pangea + && + !r.isAnalyticsRoom, + // Pangea# badgePosition: BadgePosition.topEnd(end: 8, top: 4), child: const Center(child: BackButton()), ), @@ -191,17 +209,34 @@ class ChatView extends StatelessWidget { title: ChatAppBarTitle(controller), actions: _appBarActions(context), ), - floatingActionButton: controller.showScrollDownButton && - controller.selectedEvents.isEmpty - ? Padding( - padding: const EdgeInsets.only(bottom: 56.0), - child: FloatingActionButton( - onPressed: controller.scrollDown, - heroTag: null, - mini: true, - child: const Icon(Icons.arrow_downward_outlined), - ), - ) + // #Pangea + // floatingActionButton: controller.showScrollDownButton && + // controller.selectedEvents.isEmpty + floatingActionButton: controller.selectedEvents.isEmpty + ? (controller.showScrollDownButton + // Pangea# + ? Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: controller.scrollDown, + heroTag: null, + mini: true, + child: const Icon(Icons.arrow_downward_outlined), + ), + ) + // #Pangea + : controller.choreographer.errorService.error != null + ? ChoreographerHasErrorButton( + controller.pangeaController, + controller.choreographer.errorService.error!, + ) + : controller.showPermissionsError + ? LanguagePermissionsButtons( + choreographer: controller.choreographer, + roomID: controller.roomId, + ) + : null) + // Pangea# : null, body: DropTarget( onDragDone: controller.onDragDone, @@ -284,95 +319,122 @@ class ChatView extends StatelessWidget { ), if (controller.room.canSendDefaultMessages && controller.room.membership == Membership.join) - Container( - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 2.5, - ), - alignment: Alignment.center, - child: Material( - borderRadius: const BorderRadius.only( - bottomLeft: - Radius.circular(AppConfig.borderRadius), - bottomRight: - Radius.circular(AppConfig.borderRadius), - ), - elevation: 4, - shadowColor: Colors.black.withAlpha(64), - clipBehavior: Clip.hardEdge, - color: Theme.of(context).brightness == - Brightness.light - ? Colors.white - : Colors.black, - child: controller.room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: - const EdgeInsets.all(16), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context)!.leave, - ), - ), - TextButton.icon( - style: TextButton.styleFrom( - padding: - const EdgeInsets.all(16), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: - controller.recreateChat, - label: Text( - L10n.of(context)!.reopenChat, - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], + // #Pangea + ConditionalFlexible( + isScroll: controller.isRowScrollable, + child: ConditionalScroll( + isScroll: controller.isRowScrollable, + child: MeasurableWidget( + onChange: (size, position) { + controller.inputRowSize = size!.height; + }, + child: + // Pangea# + Container( + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular( + AppConfig.borderRadius, + ), + bottomRight: Radius.circular( + AppConfig.borderRadius, + ), ), + elevation: 4, + shadowColor: Colors.black.withAlpha(64), + clipBehavior: Clip.hardEdge, + color: Theme.of(context).brightness == + Brightness.light + ? Colors.white + : Colors.black, + child: controller + .room.isAbandonedDMRoom == + true + ? Row( + mainAxisAlignment: + MainAxisAlignment + .spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: + const EdgeInsets.all( + 16, + ), + foregroundColor: + Theme.of(context) + .colorScheme + .error, + ), + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: + controller.leaveChat, + label: Text( + L10n.of(context)!.leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: + const EdgeInsets.all( + 16, + ), + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: + controller.recreateChat, + label: Text( + L10n.of(context)! + .reopenChat, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), + ), + ), + ), ), ), ], ), + // #Pangea + // if (controller.dragging) + // Container( + // color: Theme.of(context) + // .scaffoldBackgroundColor + // .withOpacity(0.9), + // alignment: Alignment.center, + // child: const Icon( + // Icons.upload_outlined, + // size: 100, + // ), + // ), + // Pangea# ), - if (controller.dragging) - Container( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, - ), - ), ], ), ), @@ -384,3 +446,35 @@ class ChatView extends StatelessWidget { ); } } + +// #Pangea +Widget ConditionalFlexible({required bool isScroll, required Widget child}) { + if (isScroll) { + return Flexible( + flex: 9999999, + child: child, + ); + } + return child; +} + +class ConditionalScroll extends StatelessWidget { + final bool isScroll; + final Widget child; + const ConditionalScroll({ + super.key, + required this.isScroll, + required this.child, + }); + + @override + Widget build(BuildContext context) { + if (isScroll) { + return SingleChildScrollView( + child: child, + ); + } + return child; + } +} +// #Pangea diff --git a/lib/pages/chat/cupertino_widgets_bottom_sheet.dart b/lib/pages/chat/cupertino_widgets_bottom_sheet.dart new file mode 100644 index 000000000..0bc7ac346 --- /dev/null +++ b/lib/pages/chat/cupertino_widgets_bottom_sheet.dart @@ -0,0 +1,50 @@ +import 'package:flutter/cupertino.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/link.dart'; + +import 'edit_widgets_dialog.dart'; + +class CupertinoWidgetsBottomSheet extends StatelessWidget { + final Room room; + + const CupertinoWidgetsBottomSheet({Key? key, required this.room}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CupertinoActionSheet( + title: Text(L10n.of(context)!.matrixWidgets), + actions: [ + ...room.widgets.map( + (widget) => Link( + builder: (context, callback) { + return CupertinoActionSheetAction( + onPressed: callback ?? () {}, + child: Text(widget.name ?? widget.url), + ); + }, + target: LinkTarget.blank, + uri: Uri.parse(widget.url), + ), + ), + CupertinoActionSheetAction( + child: Text(L10n.of(context)!.editWidgets), + onPressed: () { + Navigator.of(context).pop(); + showCupertinoDialog( + context: context, + builder: (context) => EditWidgetsDialog(room: room), + useRootNavigator: false, + ); + }, + ), + CupertinoActionSheetAction( + onPressed: Navigator.of(context).pop, + child: Text(L10n.of(context)!.cancel), + ), + ], + ); + } +} diff --git a/lib/pages/chat/encryption_button.dart b/lib/pages/chat/encryption_button.dart index d5a4481c5..092ad24c7 100644 --- a/lib/pages/chat/encryption_button.dart +++ b/lib/pages/chat/encryption_button.dart @@ -1,5 +1,4 @@ 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'; diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 1b0586d27..4cced05b0 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'dart:io'; +import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:fluffychat/utils/error_reporter.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import '../../../utils/matrix_sdk_extensions/event_extension.dart'; class AudioPlayerWidget extends StatefulWidget { diff --git a/lib/pages/chat/events/cute_events.dart b/lib/pages/chat/events/cute_events.dart index 6b5b0f909..49024750b 100644 --- a/lib/pages/chat/events/cute_events.dart +++ b/lib/pages/chat/events/cute_events.dart @@ -1,12 +1,10 @@ import 'dart:math'; +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; - class CuteContent extends StatefulWidget { final Event event; diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index cd57545a2..9ac0f96c1 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,6 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:flutter_html/flutter_html.dart'; @@ -9,9 +11,6 @@ import 'package:flutter_math_fork/flutter_math.dart'; import 'package:linkify/linkify.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../utils/url_launcher.dart'; class HtmlMessage extends StatelessWidget { diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 6974be5f7..c5ca54afd 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_blurhash/flutter_blurhash.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:matrix/matrix.dart'; class ImageBubble extends StatelessWidget { final Event event; diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 72a637aee..3a173925d 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -1,14 +1,16 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; -import 'package:swipe_to_action/swipe_to_action.dart'; - import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/enum/use_type.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:swipe_to_action/swipe_to_action.dart'; + import '../../../config/app_config.dart'; import 'message_content.dart'; import 'message_reactions.dart'; @@ -28,6 +30,11 @@ class Message extends StatelessWidget { final bool longPressSelect; final bool selected; final Timeline timeline; + // #Pangea + final LanguageModel? selectedDisplayLang; + final bool immersionMode; + final bool definitions; + // Pangea# const Message( this.event, { @@ -41,6 +48,11 @@ class Message extends StatelessWidget { required this.onSwipe, this.selected = false, required this.timeline, + // #Pangea + required this.selectedDisplayLang, + required this.immersionMode, + required this.definitions, + // Pangea# Key? key, }) : super(key: key); @@ -50,6 +62,9 @@ class Message extends StatelessWidget { @override Widget build(BuildContext context) { + // #Pangea + debugPrint('Message.build()'); + // Pangea# if (!{ EventTypes.Message, EventTypes.Sticker, @@ -117,6 +132,15 @@ class Message extends StatelessWidget { : Theme.of(context).colorScheme.primary; } + // #Pangea + final pangeaMessageEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: ownMessage, + selected: selected, + ); + // Pangea# + final row = Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: rowMainAxisAlignment, @@ -191,100 +215,136 @@ class Message extends StatelessWidget { color: noBubble ? Colors.transparent : color, borderRadius: borderRadius, clipBehavior: Clip.antiAlias, - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), - padding: noBubble || noPadding - ? EdgeInsets.zero - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.columnWidth * 1.5, - ), - child: Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (event.relationshipType == - RelationshipTypes.reply) - FutureBuilder( - future: event.getReplyEvent(timeline), - builder: (BuildContext context, snapshot) { - final replyEvent = snapshot.hasData - ? snapshot.data! - : Event( - eventId: event.relationshipEventId!, - content: { - 'msgtype': 'm.text', - 'body': '...', - }, - senderId: event.senderId, - type: 'm.room.message', - room: event.room, - status: EventStatus.sent, - originServerTs: DateTime.now(), - ); - return InkWell( - onTap: () { - if (scrollToEventId != null) { - scrollToEventId!(replyEvent.eventId); - } - }, - child: AbsorbPointer( - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 4.0, - ), - child: ReplyContent( - replyEvent, - ownMessage: ownMessage, - timeline: timeline, - ), - ), - ), - ); - }, - ), - MessageContent( - displayEvent, - textColor: textColor, - onInfoTab: onInfoTab, + // #Pangea + child: CompositedTransformTarget( + link: MatrixState.pAnyState + .layerLinkAndKey(event.eventId) + .link, + child: Container( + key: MatrixState.pAnyState + .layerLinkAndKey(event.eventId) + .key, + // Pangea# + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), - if (event.hasAggregatedEvents( - timeline, - RelationshipTypes.edit, - )) - Padding( - padding: const EdgeInsets.only( - top: 4.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.edit_outlined, - color: textColor.withAlpha(164), - size: 14, - ), - Text( - ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', - style: TextStyle( - color: textColor.withAlpha(164), - fontSize: 12, + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 1.5, + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (event.relationshipType == + RelationshipTypes.reply) + FutureBuilder( + future: event.getReplyEvent(timeline), + builder: (BuildContext context, snapshot) { + final replyEvent = snapshot.hasData + ? snapshot.data! + : Event( + eventId: event.relationshipEventId!, + content: { + 'msgtype': 'm.text', + 'body': '...', + }, + senderId: event.senderId, + type: 'm.room.message', + room: event.room, + status: EventStatus.sent, + originServerTs: DateTime.now(), + ); + return InkWell( + onTap: () { + if (scrollToEventId != null) { + scrollToEventId!(replyEvent.eventId); + } + }, + child: AbsorbPointer( + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 4.0, + ), + child: ReplyContent( + replyEvent, + ownMessage: ownMessage, + timeline: timeline, + ), + ), ), - ), - ], + ); + }, ), + MessageContent( + displayEvent, + textColor: textColor, + onInfoTab: onInfoTab, + // #Pangea + selected: selected, + pangeaMessageEvent: pangeaMessageEvent, + selectedDisplayLang: selectedDisplayLang, + immersionMode: immersionMode, + definitions: definitions, + // Pangea# ), - ], - ), - ], + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + ) + // #Pangea + || + (pangeaMessageEvent.showUseType) + // Pangea# + ) + Padding( + padding: const EdgeInsets.only( + top: 4.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // #Pangea + if (pangeaMessageEvent.showUseType) ...[ + pangeaMessageEvent.useType.iconView( + context, + textColor.withAlpha(164), + ), + const SizedBox(width: 4) + ], + if (event.hasAggregatedEvents( + timeline, + RelationshipTypes.edit, + )) ...[ + // Pangea# + Icon( + Icons.edit_outlined, + color: textColor.withAlpha(164), + size: 14, + ), + Text( + ' - ${displayEvent.originServerTs.localizedTimeShort(context)}', + style: TextStyle( + color: textColor.withAlpha(164), + fontSize: 12, + ), + ), + ], + ], + ), + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 2be0ba826..f79031663 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,19 +1,19 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/chat/events/video_player.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; + import '../../../config/app_config.dart'; import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; -import '../../bootstrap/bootstrap_dialog.dart'; import 'audio_player.dart'; import 'cute_events.dart'; import 'html_message.dart'; @@ -26,12 +26,29 @@ class MessageContent extends StatelessWidget { final Event event; final Color textColor; final void Function(Event)? onInfoTab; + // #Pangea + final bool selected; + final PangeaMessageEvent pangeaMessageEvent; + //question: are there any performance benefits to using booleans + //here rather than passing the choreographer? pangea rich text, a widget + //further down in the chain is also using pangeaController so its not constant + final LanguageModel? selectedDisplayLang; + final bool immersionMode; + final bool definitions; + // Pangea# const MessageContent( this.event, { this.onInfoTab, Key? key, required this.textColor, + // #Pangea + required this.selected, + required this.pangeaMessageEvent, + required this.selectedDisplayLang, + required this.immersionMode, + required this.definitions, + // Pangea# }) : super(key: key); void _verifyOrRequestKey(BuildContext context) async { @@ -50,13 +67,15 @@ class MessageContent extends StatelessWidget { ); return; } - final client = Matrix.of(context).client; - if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { - final success = await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - if (success != true) return; - } + // #Pangea + // final client = Matrix.of(context).client; + // if (client.isUnknownSession && client.encryption!.crossSigning.enabled) { + // final success = await BootstrapDialog( + // client: Matrix.of(context).client, + // ).show(context); + // if (success != true) return; + // } + // Pangea# event.requestKey(); final sender = event.senderFromMemoryOrFallback; await showAdaptiveBottomSheet( @@ -231,12 +250,41 @@ class MessageContent extends StatelessWidget { final bigEmotes = event.onlyEmotes && event.numberEmotes > 0 && event.numberEmotes <= 10; + // #Pangea + final messageTextStyle = TextStyle( + color: textColor, + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: event.redacted ? TextDecoration.lineThrough : null, + height: 1.3, + ); + if (pangeaMessageEvent.showRichText) { + return PangeaRichText( + existingStyle: messageTextStyle, + selected: selected, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: immersionMode, + definitions: definitions, + selectedDisplayLang: selectedDisplayLang, + ); + } + //Pangea# return FutureBuilder( future: event.calcLocalizedBody( MatrixLocals(L10n.of(context)!), hideReply: true, ), builder: (context, snapshot) { + // #Pangea + if (!snapshot.hasData) { + return Text( + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ), + style: messageTextStyle, + ); + } + // Pangea# return Linkify( text: snapshot.data ?? event.calcLocalizedBodyFallback( diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 42dcdff97..77e72183c 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,8 +1,6 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class MessageDownloadContent extends StatelessWidget { final Event event; diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index 110553b9e..71b30554e 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -1,15 +1,13 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart' show IterableExtension; -import 'package:future_loading_dialog/future_loading_dialog.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; class MessageReactions extends StatelessWidget { final Event event; diff --git a/lib/pages/chat/events/verification_request_content.dart b/lib/pages/chat/events/verification_request_content.dart index a7271125c..13274678c 100644 --- a/lib/pages/chat/events/verification_request_content.dart +++ b/lib/pages/chat/events/verification_request_content.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 3cfd1e703..b6a28591d 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:chewie/chewie.dart'; +import 'package:fluffychat/pages/chat/events/image_bubble.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - -import 'package:chewie/chewie.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -11,9 +13,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:universal_html/html.dart' as html; import 'package:video_player/video_player.dart'; -import 'package:fluffychat/pages/chat/events/image_bubble.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import '../../../utils/error_reporter.dart'; class EventVideoPlayer extends StatefulWidget { diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index cdff8e210..83ab417b8 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -1,19 +1,15 @@ +import 'package:emojis/emoji.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:emojis/emoji.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:matrix/matrix.dart'; import 'package:pasteboard/pasteboard.dart'; import 'package:slugify/slugify.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; -import '../../widgets/avatar.dart'; import '../../widgets/matrix.dart'; -import 'command_hints.dart'; class InputBar extends StatelessWidget { final Room room; @@ -24,7 +20,10 @@ class InputBar extends StatelessWidget { final ValueChanged? onSubmitted; final ValueChanged? onSubmitImage; final FocusNode? focusNode; - final TextEditingController? controller; + // #Pangea + // final TextEditingController? controller; + final PangeaTextController? controller; + // Pangea# final InputDecoration? decoration; final ValueChanged? onChanged; final bool? autofocus; @@ -48,14 +47,19 @@ class InputBar extends StatelessWidget { }) : super(key: key); List> getSuggestions(String text) { - if (controller!.selection.baseOffset != - controller!.selection.extentOffset || - controller!.selection.baseOffset < 0) { - return []; // no entries if there is selected text - } + // #Pangea + final List> ret = >[]; + // if (controller!.selection.baseOffset != + // controller!.selection.extentOffset || + // controller!.selection.baseOffset < 0) { + // return []; // no entries if there is selected text + // } + // Pangea# final searchText = controller!.text.substring(0, controller!.selection.baseOffset); - final List> ret = >[]; + // #Pangea + // final List> ret = >[]; + // Pangea# const maxResults = 30; final commandMatch = RegExp(r'^/(\w*)$').firstMatch(searchText); @@ -221,104 +225,106 @@ class InputBar extends StatelessWidget { Map suggestion, Client? client, ) { - const size = 30.0; - const padding = EdgeInsets.all(4.0); - if (suggestion['type'] == 'command') { - final command = suggestion['name']!; - final hint = commandHint(L10n.of(context)!, command); - return Tooltip( - message: hint, - waitDuration: const Duration(days: 1), // don't show on hover - child: Container( - padding: padding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '/$command', - style: const TextStyle(fontFamily: 'monospace'), - ), - Text( - hint, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - ); - } - if (suggestion['type'] == 'emoji') { - final label = suggestion['label']!; - return Tooltip( - message: label, - waitDuration: const Duration(days: 1), // don't show on hover - child: Container( - padding: padding, - child: Text(label, style: const TextStyle(fontFamily: 'monospace')), - ), - ); - } - if (suggestion['type'] == 'emote') { - return Container( - padding: padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - MxcImage( - // ensure proper ordering ... - key: ValueKey(suggestion['name']), - uri: suggestion['mxc'] is String - ? Uri.parse(suggestion['mxc'] ?? '') - : null, - width: size, - height: size, - ), - const SizedBox(width: 6), - Text(suggestion['name']!), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Opacity( - opacity: suggestion['pack_avatar_url'] != null ? 0.8 : 0.5, - child: suggestion['pack_avatar_url'] != null - ? Avatar( - mxContent: Uri.tryParse( - suggestion.tryGet('pack_avatar_url') ?? '', - ), - name: suggestion.tryGet('pack_display_name'), - size: size * 0.9, - client: client, - ) - : Text(suggestion['pack_display_name']!), - ), - ), - ), - ], - ), - ); - } - if (suggestion['type'] == 'user' || suggestion['type'] == 'room') { - final url = Uri.parse(suggestion['avatar_url'] ?? ''); - return Container( - padding: padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Avatar( - mxContent: url, - name: suggestion.tryGet('displayname') ?? - suggestion.tryGet('mxid'), - size: size, - client: client, - ), - const SizedBox(width: 6), - Text(suggestion['displayname'] ?? suggestion['mxid']!), - ], - ), - ); - } + // #Pangea + // const size = 30.0; + // const padding = EdgeInsets.all(4.0); + // if (suggestion['type'] == 'command') { + // final command = suggestion['name']!; + // final hint = commandHint(L10n.of(context)!, command); + // return Tooltip( + // message: hint, + // waitDuration: const Duration(days: 1), // don't show on hover + // child: Container( + // padding: padding, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // '/$command', + // style: const TextStyle(fontFamily: 'monospace'), + // ), + // Text( + // hint, + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: Theme.of(context).textTheme.bodySmall, + // ), + // ], + // ), + // ), + // ); + // } + // if (suggestion['type'] == 'emoji') { + // final label = suggestion['label']!; + // return Tooltip( + // message: label, + // waitDuration: const Duration(days: 1), // don't show on hover + // child: Container( + // padding: padding, + // child: Text(label, style: const TextStyle(fontFamily: 'monospace')), + // ), + // ); + // } + // if (suggestion['type'] == 'emote') { + // return Container( + // padding: padding, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // MxcImage( + // // ensure proper ordering ... + // key: ValueKey(suggestion['name']), + // uri: suggestion['mxc'] is String + // ? Uri.parse(suggestion['mxc'] ?? '') + // : null, + // width: size, + // height: size, + // ), + // const SizedBox(width: 6), + // Text(suggestion['name']!), + // Expanded( + // child: Align( + // alignment: Alignment.centerRight, + // child: Opacity( + // opacity: suggestion['pack_avatar_url'] != null ? 0.8 : 0.5, + // child: suggestion['pack_avatar_url'] != null + // ? Avatar( + // mxContent: Uri.tryParse( + // suggestion.tryGet('pack_avatar_url') ?? '', + // ), + // name: suggestion.tryGet('pack_display_name'), + // size: size * 0.9, + // client: client, + // ) + // : Text(suggestion['pack_display_name']!), + // ), + // ), + // ), + // ], + // ), + // ); + // } + // if (suggestion['type'] == 'user' || suggestion['type'] == 'room') { + // final url = Uri.parse(suggestion['avatar_url'] ?? ''); + // return Container( + // padding: padding, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Avatar( + // mxContent: url, + // name: suggestion.tryGet('displayname') ?? + // suggestion.tryGet('mxid'), + // size: size, + // client: client, + // ), + // const SizedBox(width: 6), + // Text(suggestion['displayname'] ?? suggestion['mxid']!), + // ], + // ), + // ); + // } + // Pangea# return const SizedBox.shrink(); } @@ -445,45 +451,61 @@ class InputBar extends StatelessWidget { }, ), }, - child: TypeAheadField>( - direction: AxisDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - keepSuggestionsOnSuggestionSelected: true, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - textFieldConfiguration: TextFieldConfiguration( - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - controller: controller, - decoration: decoration!, - focusNode: focusNode, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); - }, - textCapitalization: TextCapitalization.sentences, + // #Pangea + child: CompositedTransformTarget( + link: controller!.choreographer.inputLayerLinkAndKey.link, + // Pangea# + child: TypeAheadField>( + direction: AxisDirection.up, + hideOnEmpty: true, + hideOnLoading: true, + keepSuggestionsOnSuggestionSelected: true, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + // #Pangea + key: controller!.choreographer.inputLayerLinkAndKey.key, + // Pangea# + textFieldConfiguration: TextFieldConfiguration( + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + onSubmitted: (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + }, + // #Pangea + onTap: () { + controller!.onInputTap( + context, + fNode: focusNode!, + ); + }, + // Pangea# + controller: controller, + decoration: decoration!, + focusNode: focusNode, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + textCapitalization: TextCapitalization.sentences, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (c, s) => + buildSuggestion(c, s, Matrix.of(context).client), + onSuggestionSelected: (Map suggestion) => + insertSuggestion(context, suggestion), + errorBuilder: (BuildContext context, Object? error) => + const SizedBox.shrink(), + loadingBuilder: (BuildContext context) => const SizedBox.shrink(), + // fix loading briefly flickering a dark box + noItemsFoundBuilder: (BuildContext context) => const SizedBox + .shrink(), // fix loading briefly showing no suggestions ), - suggestionsCallback: getSuggestions, - itemBuilder: (c, s) => - buildSuggestion(c, s, Matrix.of(context).client), - onSuggestionSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => - const SizedBox.shrink(), - loadingBuilder: (BuildContext context) => const SizedBox.shrink(), - // fix loading briefly flickering a dark box - noItemsFoundBuilder: (BuildContext context) => const SizedBox - .shrink(), // fix loading briefly showing no suggestions ), ), ); diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart index ae81b46e4..eec216b59 100644 --- a/lib/pages/chat/pinned_events.dart +++ b/lib/pages/chat/pinned_events.dart @@ -1,16 +1,14 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; class PinnedEvents extends StatelessWidget { final ChatController controller; diff --git a/lib/pages/chat/reactions_picker.dart b/lib/pages/chat/reactions_picker.dart index 4ae5505fd..c94a22357 100644 --- a/lib/pages/chat/reactions_picker.dart +++ b/lib/pages/chat/reactions_picker.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:emoji_proposal/emoji_proposal.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_emojis.dart'; import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + import '../../config/themes.dart'; class ReactionsPicker extends StatelessWidget { diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart index 86d6cbb61..e84ab8202 100644 --- a/lib/pages/chat/recording_dialog.dart +++ b/lib/pages/chat/recording_dialog.dart @@ -1,15 +1,14 @@ import 'dart:async'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'events/audio_player.dart'; class RecordingDialog extends StatefulWidget { @@ -46,7 +45,16 @@ class RecordingDialogState extends State { return; } await WakelockPlus.enable(); + + // We try to pick Opus where supported, since that is a codec optimized + // for speech as well as what the voice messages MSC uses. + final audioCodec = + (await _audioRecorder.isEncoderSupported(AudioEncoder.opus)) + ? AudioEncoder.opus + : AudioEncoder.aacLc; + await _audioRecorder.start( + encoder: audioCodec, path: _recordedPath, bitRate: bitRate, samplingRate: samplingRate, diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 11b61db79..e019cef1c 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,11 +1,10 @@ +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/size_string.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/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/size_string.dart'; import '../../utils/resize_image.dart'; class SendFileDialog extends StatefulWidget { diff --git a/lib/pages/chat/widgets_bottom_sheet.dart b/lib/pages/chat/widgets_bottom_sheet.dart new file mode 100644 index 000000000..7a52929ce --- /dev/null +++ b/lib/pages/chat/widgets_bottom_sheet.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/link.dart'; + +import 'edit_widgets_dialog.dart'; + +class WidgetsBottomSheet extends StatelessWidget { + final Room room; + + const WidgetsBottomSheet({Key? key, required this.room}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + itemBuilder: (context, index) { + if (index == room.widgets.length) { + return ListTile( + leading: const Icon(Icons.edit), + title: Text(L10n.of(context)!.editWidgets), + onTap: () { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: (context) => EditWidgetsDialog(room: room), + useRootNavigator: false, + ); + }, + ); + } + final widget = room.widgets[index]; + return Link( + builder: (context, callback) { + return ListTile( + title: Text(widget.name ?? widget.url), + subtitle: Text(widget.type), + onTap: callback, + ); + }, + target: LinkTarget.blank, + uri: Uri.parse(widget.url), + ); + }, + itemCount: room.widgets.length + 1, + ); + } +} diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index b9da5c654..aa658d992 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -1,9 +1,17 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; +import 'package:fluffychat/pages/settings/settings.dart'; +import 'package:fluffychat/pangea/utils/set_class_name.dart'; +import 'package:fluffychat/pangea/utils/set_class_topic.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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'; @@ -11,13 +19,6 @@ import 'package:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart' as matrix; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_view.dart'; -import 'package:fluffychat/pages/settings/settings.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - enum AliasActions { copy, delete, setCanonical } class ChatDetails extends StatefulWidget { @@ -40,34 +41,42 @@ class ChatDetailsController extends State { String? get roomId => widget.roomId; - void setDisplaynameAction() async { - final room = Matrix.of(context).client.getRoomById(roomId!)!; - final input = await showTextInputDialog( - context: context, - title: L10n.of(context)!.changeTheNameOfTheGroup, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [ - DialogTextField( - initialText: room.getLocalizedDisplayname( - MatrixLocals( - L10n.of(context)!, - ), - ), - ), - ], - ); - if (input == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => room.setName(input.single), - ); - if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)), - ); - } - } + // #Pangea + final GlobalKey addToSpaceKey = GlobalKey(); + + bool displayAddStudentOptions = false; + void toggleAddStudentOptions() => + setState(() => displayAddStudentOptions = !displayAddStudentOptions); + void setDisplaynameAction() => setClassDisplayname(context, roomId); + // void setDisplaynameAction() async { + // final room = Matrix.of(context).client.getRoomById(roomId!)!; + // final input = await showTextInputDialog( + // context: context, + // title: L10n.of(context)!.changeTheNameOfTheGroup, + // okLabel: L10n.of(context)!.ok, + // cancelLabel: L10n.of(context)!.cancel, + // textFields: [ + // DialogTextField( + // initialText: room.getLocalizedDisplayname( + // MatrixLocals( + // L10n.of(context)!, + // ), + // ), + // ), + // ], + // ); + // if (input == null) return; + // final success = await showFutureLoadingDialog( + // context: context, + // future: () => room.setName(input.single), + // ); + // if (success.error == null) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)), + // ); + // } + // } + // Pangea# void editAliases() async { final room = Matrix.of(context).client.getRoomById(roomId!); @@ -103,6 +112,9 @@ class ChatDetailsController extends State { return setAliasAction(); } final select = await showConfirmationDialog( + // #Pangea + useRootNavigator: false, + // Pangea# context: context, title: L10n.of(context)!.editRoomAliases, actions: [ @@ -175,6 +187,9 @@ class ChatDetailsController extends State { final domain = room.client.userID!.domain; final input = await showTextInputDialog( + // #Pangea + useRootNavigator: false, + // Pangea# context: context, title: L10n.of(context)!.setInvitationLink, okLabel: L10n.of(context)!.ok, @@ -198,32 +213,35 @@ class ChatDetailsController extends State { void setTopicAction() async { final room = Matrix.of(context).client.getRoomById(roomId!)!; - final input = await showTextInputDialog( - context: context, - title: L10n.of(context)!.setChatDescription, - okLabel: L10n.of(context)!.ok, - cancelLabel: L10n.of(context)!.cancel, - textFields: [ - DialogTextField( - hintText: L10n.of(context)!.noChatDescriptionYet, - initialText: room.topic, - minLines: 4, - maxLines: 8, - ), - ], - ); - if (input == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => room.setDescription(input.single), - ); - if (success.error == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(L10n.of(context)!.chatDescriptionHasBeenChanged), - ), - ); - } + // #Pangea + setClassTopic(room, context); + // final input = await showTextInputDialog( + // context: context, + // title: L10n.of(context)!.setChatDescription, + // okLabel: L10n.of(context)!.ok, + // cancelLabel: L10n.of(context)!.cancel, + // textFields: [ + // DialogTextField( + // hintText: L10n.of(context)!.noChatDescriptionYet, + // initialText: room.topic, + // minLines: 4, + // maxLines: 8, + // ), + // ], + // ); + // if (input == null) return; + // final success = await showFutureLoadingDialog( + // context: context, + // future: () => room.setDescription(input.single), + // ); + // if (success.error == null) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(L10n.of(context)!.chatDescriptionHasBeenChanged), + // ), + // ); + // } + // Pangea# } void setGuestAccess() async { @@ -400,4 +418,10 @@ class ChatDetailsController extends State { @override Widget build(BuildContext context) => ChatDetailsView(this); + + // #Pangea + bool showEditNameIcon = false; + void hoverEditNameIcon(bool hovering) => + setState(() => showEditNameIcon = !showEditNameIcon); + // Pangea# } diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index b7fd77bc9..7a8beaf2f 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -1,20 +1,28 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_details/participant_list_item.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/pages/class_settings/class_name_header.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_description_button.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/class_name_button.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; +import 'package:fluffychat/pangea/utils/lock_room.dart'; +import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/pangea/widgets/space/class_settings.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import '../../utils/url_launcher.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'; class ChatDetailsView extends StatelessWidget { final ChatDetailsController controller; @@ -57,18 +65,27 @@ class ChatDetailsView extends StatelessWidget { leading: const Center(child: BackButton()), elevation: Theme.of(context).appBarTheme.elevation, actions: [ - if (room.canonicalAlias.isNotEmpty) - IconButton( - tooltip: L10n.of(context)!.share, - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - AppConfig.inviteLinkPrefix + room.canonicalAlias, - context, - ), - ), - ChatSettingsPopupMenu(room, false), + // #Pangea + // if (room.canonicalAlias.isNotEmpty) + // IconButton( + // tooltip: L10n.of(context)!.share, + // icon: Icon(Icons.adaptive.share_outlined), + // onPressed: () => FluffyShare.share( + // AppConfig.inviteLinkPrefix + room.canonicalAlias, + // context, + // ), + // ), + if (!room.isSpace) + // Pangea# + ChatSettingsPopupMenu(room, false), ], - title: Text(L10n.of(context)!.chatDetails), + // #Pangea + title: ClassNameHeader( + controller: controller, + room: room, + ), + // title: Text(L10n.of(context)!.chatDetails), + // Pangea# backgroundColor: Theme.of(context).appBarTheme.backgroundColor, ), @@ -207,162 +224,216 @@ class ChatDetailsView extends StatelessWidget { height: 1, color: Theme.of(context).dividerColor, ), - if (!room.canChangeStateEvent(EventTypes.RoomTopic)) + // #Pangea + if (room.canSendEvent('m.room.name')) + ClassNameButton( + room: room, + controller: controller, + ), + if (room.canSendEvent('m.room.topic')) + ClassDescriptionButton( + room: room, + controller: controller, + ), + if ((room.isPangeaClass || room.isExchange) && + room.isRoomAdmin) ListTile( title: Text( - L10n.of(context)!.chatDescription, + L10n.of(context)!.classAnalytics, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.bold, ), ), - ) - else - Padding( - padding: const EdgeInsets.all(16.0), - child: OutlinedButton.icon( - onPressed: controller.setTopicAction, - label: Text(L10n.of(context)!.setChatDescription), - icon: const Icon(Icons.edit_outlined), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: SelectableLinkify( - text: room.topic.isEmpty - ? L10n.of(context)!.noChatDescriptionYet - : room.topic, - options: const LinkifyOptions(humanize: false), - linkStyle: - const TextStyle(color: Colors.blueAccent), - style: TextStyle( - fontSize: 14, - fontStyle: room.topic.isEmpty - ? FontStyle.italic - : FontStyle.normal, - color: - Theme.of(context).textTheme.bodyMedium!.color, - decorationColor: - Theme.of(context).textTheme.bodyMedium!.color, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), - ), - const SizedBox(height: 16), - Divider( - height: 1, - color: Theme.of(context).dividerColor, - ), - if (room.joinRules == JoinRules.public) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.link_outlined), - ), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.editAliases, - title: Text(L10n.of(context)!.editRoomAliases), - subtitle: Text( - (room.canonicalAlias.isNotEmpty) - ? room.canonicalAlias - : L10n.of(context)!.none, - ), - ), - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon( - Icons.insert_emoticon_outlined, - ), - ), - title: Text(L10n.of(context)!.emoteSettings), - subtitle: Text(L10n.of(context)!.setCustomEmotes), - onTap: controller.goToEmoteSettings, - trailing: const Icon(Icons.chevron_right_outlined), - ), - if (!room.isDirectChat) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.shield_outlined), - ), - title: Text( - L10n.of(context)!.whoIsAllowedToJoinThisGroup, - ), - trailing: room.canChangeJoinRules - ? const Icon(Icons.chevron_right_outlined) - : null, - subtitle: Text( - room.joinRules?.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ) ?? - L10n.of(context)!.none, - ), - onTap: room.canChangeJoinRules - ? controller.setJoinRules - : null, - ), - if (!room.isDirectChat) - ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: iconColor, - child: const Icon(Icons.visibility_outlined), - ), - trailing: room.canChangeHistoryVisibility - ? const Icon(Icons.chevron_right_outlined) - : null, - title: Text( - L10n.of(context)!.visibilityOfTheChatHistory, - ), - subtitle: Text( - room.historyVisibility?.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ) ?? - L10n.of(context)!.none, - ), - onTap: room.canChangeHistoryVisibility - ? controller.setHistoryVisibility - : null, - ), - if (room.joinRules == JoinRules.public) - ListTile( leading: CircleAvatar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, foregroundColor: iconColor, child: const Icon( - Icons.person_add_alt_1_outlined, + Icons.analytics_outlined, ), ), - trailing: room.canChangeGuestAccess - ? const Icon(Icons.chevron_right_outlined) - : null, - title: Text( - L10n.of(context)!.areGuestsAllowedToJoin, + onTap: () => context.go( + '/rooms/analytics/${room.id}', ), - subtitle: Text( - room.guestAccess.getLocalizedString( - MatrixLocals(L10n.of(context)!), - ), - ), - onTap: room.canChangeGuestAccess - ? controller.setGuestAccess - : null, ), - if (!room.isDirectChat) + if (room.classSettings != null && room.isRoomAdmin) + ClassSettings( + roomId: controller.roomId, + startOpen: false, + ), + if (room.pangeaRoomRules != null && room.isRoomAdmin) + RoomRulesEditor( + roomId: controller.roomId, + startOpen: false, + ), + // if (!room.canChangeStateEvent(EventTypes.RoomTopic)) + // ListTile( + // title: Text( + // L10n.of(context)!.chatDescription, + // style: TextStyle( + // color: Theme.of(context).colorScheme.secondary, + // fontWeight: FontWeight.bold, + // ), + // ), + // ) + // else + // Padding( + // padding: const EdgeInsets.all(16.0), + // child: OutlinedButton.icon( + // onPressed: controller.setTopicAction, + // label: Text(L10n.of(context)!.setChatDescription), + // icon: const Icon(Icons.edit_outlined), + // ), + // ), + // Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 16.0, + // ), + // child: SelectableLinkify( + // text: room.topic.isEmpty + // ? L10n.of(context)!.noChatDescriptionYet + // : room.topic, + // options: const LinkifyOptions(humanize: false), + // linkStyle: + // const TextStyle(color: Colors.blueAccent), + // style: TextStyle( + // fontSize: 14, + // fontStyle: room.topic.isEmpty + // ? FontStyle.italic + // : FontStyle.normal, + // color: + // Theme.of(context).textTheme.bodyMedium!.color, + // decorationColor: + // Theme.of(context).textTheme.bodyMedium!.color, + // ), + // onOpen: (url) => + // UrlLauncher(context, url.url).launchUrl(), + // ), + // ), + // const SizedBox(height: 16), + // Divider( + // height: 1, + // color: Theme.of(context).dividerColor, + // ), + // if (room.joinRules == JoinRules.public) + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon(Icons.link_outlined), + // ), + // trailing: const Icon(Icons.chevron_right_outlined), + // onTap: controller.editAliases, + // title: Text(L10n.of(context)!.editRoomAliases), + // subtitle: Text( + // (room.canonicalAlias.isNotEmpty) + // ? room.canonicalAlias + // : L10n.of(context)!.none, + // ), + // ), + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon( + // Icons.insert_emoticon_outlined, + // ), + // ), + // title: Text(L10n.of(context)!.emoteSettings), + // subtitle: Text(L10n.of(context)!.setCustomEmotes), + // onTap: controller.goToEmoteSettings, + // trailing: const Icon(Icons.chevron_right_outlined), + // ), + // if (!room.isDirectChat) + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon(Icons.shield_outlined), + // ), + // title: Text( + // L10n.of(context)!.whoIsAllowedToJoinThisGroup, + // ), + // trailing: room.canChangeJoinRules + // ? const Icon(Icons.chevron_right_outlined) + // : null, + // subtitle: Text( + // room.joinRules?.getLocalizedString( + // MatrixLocals(L10n.of(context)!), + // ) ?? + // L10n.of(context)!.none, + // ), + // onTap: room.canChangeJoinRules + // ? controller.setJoinRules + // : null, + // ), + // if (!room.isDirectChat) + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon(Icons.visibility_outlined), + // ), + // trailing: room.canChangeHistoryVisibility + // ? const Icon(Icons.chevron_right_outlined) + // : null, + // title: Text( + // L10n.of(context)!.visibilityOfTheChatHistory, + // ), + // subtitle: Text( + // room.historyVisibility?.getLocalizedString( + // MatrixLocals(L10n.of(context)!), + // ) ?? + // L10n.of(context)!.none, + // ), + // onTap: room.canChangeHistoryVisibility + // ? controller.setHistoryVisibility + // : null, + // ), + // if (room.joinRules == JoinRules.public) + // ListTile( + // leading: CircleAvatar( + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // foregroundColor: iconColor, + // child: const Icon( + // Icons.person_add_alt_1_outlined, + // ), + // ), + // trailing: room.canChangeGuestAccess + // ? const Icon(Icons.chevron_right_outlined) + // : null, + // title: Text( + // L10n.of(context)!.areGuestsAllowedToJoin, + // ), + // subtitle: Text( + // room.guestAccess.getLocalizedString( + // MatrixLocals(L10n.of(context)!), + // ), + // ), + // onTap: room.canChangeGuestAccess + // ? controller.setGuestAccess + // : null, + // ), + // if (!room.isDirectChat) + if (!room.isDirectChat && !room.isSpace) + // Pangea# ListTile( - title: Text(L10n.of(context)!.chatPermissions), + // #Pangea + // title: Text(L10n.of(context)!.chatPermissions), + title: Text( + L10n.of(context)!.editChatPermissions, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + // Pangea# subtitle: Text( L10n.of(context)!.whoCanPerformWhichAction, ), @@ -374,7 +445,9 @@ class ChatDetailsView extends StatelessWidget { Icons.edit_attributes_outlined, ), ), - trailing: const Icon(Icons.chevron_right_outlined), + // #Pangea + // trailing: const Icon(Icons.chevron_right_outlined), + // Pangea# onTap: () => context .push('/rooms/${room.id}/details/permissions'), ), @@ -382,6 +455,63 @@ class ChatDetailsView extends StatelessWidget { height: 1, color: Theme.of(context).dividerColor, ), + // #Pangea + if (room.canInvite) + ListTile( + title: Text( + room.isSpace + ? L10n.of(context)!.inviteUsersFromPangea + : L10n.of(context)!.inviteStudentByUserName, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: + Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon( + Icons.add, + ), + ), + onTap: () => context.go('/rooms/${room.id}/invite'), + ), + if (room.showClassEditOptions && room.isSpace) + SpaceDetailsToggleAddStudentsTile( + controller: controller, + ), + if (controller.displayAddStudentOptions && + room.showClassEditOptions) + ClassInvitationButtons(roomId: controller.roomId!), + if (!room.isPangeaClass) + AddToSpaceToggles( + roomId: room.id, + key: controller.addToSpaceKey, + startOpen: false, + mode: room.isExchange + ? AddToClassMode.exchange + : AddToClassMode.chat, + ), + const Divider(height: 1), + if (room.isRoomAdmin) + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: room.isSpace + ? Text(L10n.of(context)!.lockSpace) + : Text(L10n.of(context)!.lockChat), + value: room.locked, + onChanged: (value) => showFutureLoadingDialog( + context: context, + future: () => toggleLockRoom( + room, + Matrix.of(context).client, + ), + ), + ), + const Divider(height: 1), + // Pangea# ListTile( title: Text( L10n.of(context)!.countParticipants( @@ -393,18 +523,20 @@ class ChatDetailsView extends StatelessWidget { ), ), ), - if (!room.isDirectChat && room.canInvite) - ListTile( - title: Text(L10n.of(context)!.inviteContact), - leading: CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.white, - radius: Avatar.defaultSize / 2, - child: const Icon(Icons.add_outlined), - ), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: () => context.go('/rooms/${room.id}/invite'), - ), + // #Pangea + // if (!room.isDirectChat && room.canInvite) + // ListTile( + // title: Text(L10n.of(context)!.inviteContact), + // leading: CircleAvatar( + // backgroundColor: Theme.of(context).primaryColor, + // foregroundColor: Colors.white, + // radius: Avatar.defaultSize / 2, + // child: const Icon(Icons.add_outlined), + // ), + // trailing: const Icon(Icons.chevron_right_outlined), + // onTap: () => context.go('/rooms/${room.id}/invite'), + // ), + // Pangea# ], ) : i < members.length + 1 diff --git a/lib/pages/chat_details/participant_list_item.dart b/lib/pages/chat_details/participant_list_item.dart index 11848cb68..a0325b7c8 100644 --- a/lib/pages/chat_details/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item.dart @@ -1,16 +1,16 @@ +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import '../../widgets/avatar.dart'; import '../user_bottom_sheet/user_bottom_sheet.dart'; class ParticipantListItem extends StatelessWidget { final User user; - const ParticipantListItem(this.user, {Key? key}) : super(key: key); + const ParticipantListItem(this.user, {super.key}); @override Widget build(BuildContext context) { @@ -26,16 +26,33 @@ class ParticipantListItem extends StatelessWidget { ? L10n.of(context)!.moderator : ''; + // #Pangea + if (user.id == BotName.byEnvironment) { + return const SizedBox(); + } + // Pangea# + return Opacity( - opacity: user.membership == Membership.join ? 1 : 0.5, + //#Pangea + // opacity: user.membership == Membership.join? 1 : 0.5, + opacity: + user.membership == Membership.join && user.id != BotName.byEnvironment + ? 1 + : 0.5, + //Pangea# child: ListTile( - onTap: () => showAdaptiveBottomSheet( - context: context, - builder: (c) => UserBottomSheet( - user: user, - outerContext: context, - ), - ), + //#Pangea + // onTap: () => showAdaptiveBottomSheet( + onTap: user.id == BotName.byEnvironment + ? null + : () => showAdaptiveBottomSheet( + //Pangea# + context: context, + builder: (c) => UserBottomSheet( + user: user, + outerContext: context, + ), + ), title: Row( children: [ Expanded( @@ -52,18 +69,23 @@ class ParticipantListItem extends StatelessWidget { ), margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + // #Pangea + // color: Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).secondaryHeaderColor, borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), + // border: Border.all( + // color: Theme.of(context).colorScheme.primary, + // ), + // Pangea# ), child: Text( permissionBatch, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary, - ), + // #Pangea + // style: TextStyle( + // fontSize: 14, + // color: Theme.of(context).colorScheme.primary, + // ), + // Pangea# ), ), membershipBatch[user.membership]!.isEmpty diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart index b8df84c80..a23376c75 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart @@ -1,14 +1,13 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings_view.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/encryption.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings_view.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../key_verification/key_verification_dialog.dart'; class ChatEncryptionSettings extends StatefulWidget { diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index fc78f30ae..83788f3f1 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -1,21 +1,17 @@ -import 'package:flutter/cupertino.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; +import 'package:fluffychat/utils/beautify_string_extension.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.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:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; -import 'package:fluffychat/utils/beautify_string_extension.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; - class ChatEncryptionSettingsView extends StatelessWidget { final ChatEncryptionSettingsController controller; - const ChatEncryptionSettingsView(this.controller, {Key? key}) - : super(key: key); + const ChatEncryptionSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -54,11 +50,13 @@ class ChatEncryptionSettingsView extends StatelessWidget { value: room.encrypted, onChanged: controller.enableEncryption, ), - Icon( - CupertinoIcons.lock_shield, - size: 128, - color: Theme.of(context).colorScheme.onInverseSurface, - ), + // #Pangea + // Icon( + // CupertinoIcons.lock_shield, + // size: 128, + // color: Theme.of(context).colorScheme.onInverseSurface, + // ), + // Pangea# const Divider(), if (room.isDirectChat) Padding( diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index b972847fd..50ddf987a 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -1,11 +1,27 @@ import 'dart:async'; import 'dart:io'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; +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/pages/settings_security/settings_security.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/utils/famedlysdk_store.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/tor_stub.dart' + if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.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'; @@ -13,25 +29,12 @@ import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.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/pages/settings_security/settings_security.dart'; -import 'package:fluffychat/utils/famedlysdk_store.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import '../../../utils/account_bundles.dart'; import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart'; import '../../utils/url_launcher.dart'; import '../../utils/voip/callkeep_manager.dart'; import '../../widgets/fluffy_chat_app.dart'; import '../../widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; - -import 'package:fluffychat/utils/tor_stub.dart' - if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; enum SelectMode { normal, @@ -61,10 +64,10 @@ class ChatList extends StatefulWidget { final String? activeChat; const ChatList({ - Key? key, + super.key, this.displayNavigationRail = false, required this.activeChat, - }) : super(key: key); + }); @override ChatListController createState() => ChatListController(); @@ -87,6 +90,9 @@ class ChatListController extends State void resetActiveSpaceId() { setState(() { activeSpaceId = null; + //#Pangea + context.go("/rooms"); + //Pangea# }); } @@ -129,6 +135,9 @@ class ChatListController extends State void onDestinationSelected(int? i) { setState(() { + // #Pangea + debugPrint('onDestinationSelected $i'); + // Pangea# activeFilter = getActiveFilterByDestination(i); }); } @@ -140,23 +149,63 @@ class ChatListController extends State bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) { switch (activeFilter) { case ActiveFilter.allChats: - return (room) => !room.isSpace && !room.isStoryRoom; + return (room) => + !room.isSpace && + !room.isStoryRoom + // #Pangea + && + !room.isAnalyticsRoom; + // Pangea# case ActiveFilter.groups: return (room) => - !room.isSpace && !room.isDirectChat && !room.isStoryRoom; + !room.isSpace && + !room.isDirectChat && + !room.isStoryRoom + // #Pangea + && + !room.isAnalyticsRoom; + // Pangea# case ActiveFilter.messages: return (room) => - !room.isSpace && room.isDirectChat && !room.isStoryRoom; + !room.isSpace && + room.isDirectChat && + !room.isStoryRoom + // #Pangea + && + !room.isAnalyticsRoom; + // Pangea# case ActiveFilter.spaces: return (r) => r.isSpace; } } List get filteredRooms => Matrix.of(context) - .client - .rooms - .where(getRoomFilterByActiveFilter(activeFilter)) - .toList(); + .client + .rooms + .where( + getRoomFilterByActiveFilter(activeFilter), + ) + // #Pangea + .sorted((roomA, roomB) { + // put rooms with unread messages at the top of the list + if (roomA.membership == Membership.invite && + roomB.membership != Membership.invite) { + return -1; + } + if (roomA.membership != Membership.invite && + roomB.membership == Membership.invite) { + return 1; + } + + final bool aUnread = roomA.notificationCount > 0 || roomA.markedUnread; + final bool bUnread = roomB.notificationCount > 0 || roomB.markedUnread; + if (aUnread && !bUnread) return -1; + if (!aUnread && bUnread) return 1; + + return 0; + }) + // Pangea# + .toList(); bool isSearchMode = false; Future? publicRoomsResponse; @@ -167,6 +216,9 @@ class ChatListController extends State bool isSearching = false; static const String _serverStoreNamespace = 'im.fluffychat.search.server'; + //#Pangea + final PangeaController pangeaController = MatrixState.pangeaController; + //Pangea# void setServer() async { final newServer = await showTextInputDialog( @@ -372,6 +424,11 @@ class ChatListController extends State } } + //#Pangea + StreamSubscription? classStream; + StreamSubscription? _invitedSpaceSubscription; + //Pangea# + @override void initState() { _initReceiveSharingIntent(); @@ -394,6 +451,40 @@ class ChatListController extends State _checkTorBrowser(); + //#Pangea + classStream = pangeaController.classController.stateStream.listen((event) { + if (event["activeSpaceId"] != null && mounted) { + setActiveSpace(event["activeSpaceId"]); + } + }); + + _invitedSpaceSubscription = pangeaController + .matrixState.client.onSync.stream + .where((event) => event.rooms?.invite != null) + .listen((event) { + for (final inviteEntry in event.rooms!.invite!.entries) { + if (inviteEntry.value.inviteState == null) continue; + final bool isSpace = inviteEntry.value.inviteState!.any( + (event) => + 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, + ); + if (space != null) { + chatListHandleSpaceTap( + context, + this, + space, + ); + } + } + }); + //Pangea# + super.initState(); } @@ -402,6 +493,10 @@ class ChatListController extends State _intentDataStreamSubscription?.cancel(); _intentFileStreamSubscription?.cancel(); _intentUriStreamSubscription?.cancel(); + //#Pangea + classStream?.cancel(); + _invitedSpaceSubscription?.cancel(); + //Pangea# scrollController.removeListener(_onScroll); super.dispose(); } @@ -471,6 +566,7 @@ class ChatListController extends State title: L10n.of(context)!.areYouSure, okLabel: L10n.of(context)!.yes, cancelLabel: L10n.of(context)!.cancel, + message: L10n.of(context)!.archiveRoomDescription, ) == OkCancelResult.ok; if (!confirmed) return; @@ -526,7 +622,17 @@ class ChatListController extends State actions: Matrix.of(context) .client .rooms - .where((r) => r.isSpace) + .where( + (r) => + r.isSpace + // #Pangea + && + selectedRoomIds + .map((id) => Matrix.of(context).client.getRoomById(id)) + .where((e) => !(e?.isPangeaClass ?? true)) + .every((e) => r.canIAddSpaceChild(e)), + //Pangea# + ) .map( (space) => AlertDialogAction( key: space.id, @@ -583,16 +689,35 @@ class ChatListController extends State await client.accountDataLoading; if (client.prevBatch == null) { await client.onSync.stream.first; + // #Pangea + pangeaController.startChatWithBotIfNotPresent(); + //Pangea# // Display first login bootstrap if enabled - if (client.encryption?.keyManager.enabled == true) { - if (await client.encryption?.keyManager.isCached() == false || - await client.encryption?.crossSigning.isCached() == false || - client.isUnknownSession && !mounted) { - await BootstrapDialog(client: client).show(context); - } - } + // #Pangea + // if (client.encryption?.keyManager.enabled == true) { + // if (await client.encryption?.keyManager.isCached() == false || + // await client.encryption?.crossSigning.isCached() == false || + // client.isUnknownSession && !mounted) { + // await BootstrapDialog(client: client).show(context); + // } + // } + // Pangea# } + + // #Pangea + if (mounted) { + GoogleAnalytics.analyticsUserUpdate(client.userID); + await pangeaController.subscriptionController.initialize(); + pangeaController.afterSyncAndFirstLoginInitialization(context); + await pangeaController.inviteBotToExistingSpaces(); + } else { + ErrorHandler.logError( + m: "didn't run afterSyncAndFirstLoginInitialization because not mounted", + ); + // debugger(when: kDebugMode); + } + // Pangea# if (!mounted) return; setState(() { waitForFirstSync = true; diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 69b438e8c..d10ff0545 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -1,22 +1,19 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - import 'package:animations/animations.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.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/space_view.dart'; -import 'package:fluffychat/pages/chat_list/stories_header.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + import '../../config/themes.dart'; import '../../widgets/connection_status_header.dart'; import '../../widgets/matrix.dart'; @@ -25,7 +22,7 @@ import 'chat_list_header.dart'; class ChatListViewBody extends StatelessWidget { final ChatListController controller; - const ChatListViewBody(this.controller, {Key? key}) : super(key: key); + const ChatListViewBody(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -71,11 +68,13 @@ class ChatListViewBody extends StatelessWidget { ); } final rooms = controller.filteredRooms; - final displayStoriesHeader = { - ActiveFilter.allChats, - ActiveFilter.messages, - }.contains(controller.activeFilter) && - client.storiesRooms.isNotEmpty; + // Pangea# + // final displayStoriesHeader = { + // ActiveFilter.allChats, + // ActiveFilter.messages, + // }.contains(controller.activeFilter) && + // client.storiesRooms.isNotEmpty; + // Pangea# return SafeArea( child: CustomScrollView( controller: controller.scrollController, @@ -163,11 +162,13 @@ class ChatListViewBody extends StatelessWidget { icon: const Icon(Icons.camera_alt_outlined), ), ], - if (displayStoriesHeader) - StoriesHeader( - key: const Key('stories_header'), - filter: controller.searchController.text, - ), + // #Pangea + // if (displayStoriesHeader) + // StoriesHeader( + // key: const Key('stories_header'), + // filter: controller.searchController.text, + // ), + // Pangea# const ConnectionStatusHeader(), AnimatedContainer( height: controller.isTorBrowser ? 64 : 0, @@ -196,13 +197,31 @@ class ChatListViewBody extends StatelessWidget { !controller.isSearchMode) ...[ Padding( padding: const EdgeInsets.all(32.0), - child: Icon( - CupertinoIcons.chat_bubble_2, - size: 128, - color: - Theme.of(context).colorScheme.onInverseSurface, + // #Pangea + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'private_chat_wallpaper.png', + height: 256, + ), + ], + ), + // child: Icon( + // CupertinoIcons.chat_bubble_2, + // size: 128, + // color: + // Theme.of(context).colorScheme.onInverseSurface, + // ), + // Pangea# + ), + // #Pangea + Center( + child: ChatListBodyStartText( + controller: controller, ), ), + // Pangea# ], ], ), @@ -313,8 +332,7 @@ class _SearchItem extends StatelessWidget { required this.title, this.avatar, required this.onPressed, - Key? key, - }) : super(key: key); + }); @override Widget build(BuildContext context) => InkWell( diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index f9675a8dd..261e86be9 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,16 +1,13 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; -import '../../widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { final ChatListController controller; - const ChatListHeader({Key? key, required this.controller}) : super(key: key); + const ChatListHeader({super.key, required this.controller}); @override Widget build(BuildContext context) { @@ -43,80 +40,85 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { controller.selectedRoomIds.length.toString(), key: const ValueKey(SelectMode.select), ) - : TextField( - controller: controller.searchController, - focusNode: controller.searchFocusNode, - textInputAction: TextInputAction.search, - onChanged: controller.onSearchEnter, - decoration: InputDecoration( - fillColor: Theme.of(context).colorScheme.secondaryContainer, - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - hintText: L10n.of(context)!.search, - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: controller.isSearchMode - ? IconButton( - tooltip: L10n.of(context)!.cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelSearch, - color: Theme.of(context).colorScheme.onBackground, - ) - : IconButton( - onPressed: controller.startSearch, - icon: Icon( - Icons.search_outlined, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - suffixIcon: controller.isSearchMode - ? controller.isSearching - ? const Padding( - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 12, - ), - child: SizedBox.square( - dimension: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ), - ) - : TextButton.icon( - onPressed: controller.setServer, - style: TextButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(99), - ), - textStyle: const TextStyle(fontSize: 12), - ), - icon: const Icon(Icons.edit_outlined, size: 16), - label: Text( - controller.searchServer ?? - Matrix.of(context) - .client - .homeserver! - .host, - maxLines: 2, - ), - ) - : SizedBox( - width: 0, - child: ClientChooserButton(controller), - ), - ), - ), + // #Pangea + : ClientChooserButton(controller), + // : TextField( + // controller: controller.searchController, + // focusNode: controller.searchFocusNode, + // textInputAction: TextInputAction.search, + // onChanged: controller.onSearchEnter, + // decoration: InputDecoration( + // fillColor: Theme.of(context).colorScheme.secondaryContainer, + // border: UnderlineInputBorder( + // borderSide: BorderSide.none, + // borderRadius: BorderRadius.circular(99), + // ), + // hintText: L10n.of(context)!.search, + // floatingLabelBehavior: FloatingLabelBehavior.never, + // prefixIcon: controller.isSearchMode + // ? IconButton( + // tooltip: L10n.of(context)!.cancel, + // icon: const Icon(Icons.close_outlined), + // onPressed: controller.cancelSearch, + // color: Theme.of(context).colorScheme.onBackground, + // ) + // : IconButton( + // onPressed: controller.startSearch, + // icon: Icon( + // Icons.search_outlined, + // color: Theme.of(context).colorScheme.onBackground, + // ), + // ), + // suffixIcon: controller.isSearchMode + // ? controller.isSearching + // ? const Padding( + // padding: EdgeInsets.symmetric( + // vertical: 10.0, + // horizontal: 12, + // ), + // child: SizedBox.square( + // dimension: 24, + // child: CircularProgressIndicator.adaptive( + // strokeWidth: 2, + // ), + // ), + // ) + // : TextButton.icon( + // onPressed: controller.setServer, + // style: TextButton.styleFrom( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(99), + // ), + // textStyle: const TextStyle(fontSize: 12), + // ), + // icon: const Icon(Icons.edit_outlined, size: 16), + // label: Text( + // controller.searchServer ?? + // Matrix.of(context) + // .client + // .homeserver! + // .host, + // maxLines: 2, + // ), + // ) + // : SizedBox( + // width: 0, + // child: ClientChooserButton(controller), + // ), + // ), + // ), + // Pangea# actions: selectMode == SelectMode.share ? [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: ClientChooserButton(controller), - ), + // #Pangea + // Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 16.0, + // vertical: 8.0, + // ), + // child: ClientChooserButton(controller), + // ), + // Pangea# ] : selectMode == SelectMode.select ? [ @@ -154,7 +156,10 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { onPressed: controller.toggleMuted, ), IconButton( - icon: const Icon(Icons.delete_outlined), + // #Pangea + // icon: const Icon(Icons.delete_outlined), + icon: const Icon(Icons.archive_outlined), + // Pangea# tooltip: L10n.of(context)!.archive, onPressed: controller.archiveAction, ), diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 9475b8a48..3fd5c7422 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,14 +1,15 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/get_chat_list_item_subtitle.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/room_status_extension.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/room_status_extension.dart'; import '../../config/themes.dart'; import '../../utils/date_time_extension.dart'; import '../../widgets/avatar.dart'; @@ -23,6 +24,7 @@ class ChatListItem extends StatelessWidget { final bool selected; final void Function()? onTap; final void Function()? onLongPress; + final void Function()? onForget; const ChatListItem( this.room, { @@ -30,8 +32,9 @@ class ChatListItem extends StatelessWidget { this.selected = false, this.onTap, this.onLongPress, - Key? key, - }) : super(key: key); + this.onForget, + super.key, + }); void clickAction(BuildContext context) async { if (onTap != null) return onTap!(); @@ -132,6 +135,7 @@ class ChatListItem extends StatelessWidget { title: L10n.of(context)!.areYouSure, okLabel: L10n.of(context)!.yes, cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.archiveRoomDescription, ); if (confirmed == OkCancelResult.cancel) return; await showFutureLoadingDialog( @@ -189,6 +193,9 @@ class ChatListItem extends StatelessWidget { mxContent: room.avatar, name: displayname, onTap: onLongPress, + //#Pangea + littleIcon: room.roomTypeIcon, + // Pangea# ), title: Row( children: [ @@ -274,17 +281,26 @@ class ChatListItem extends StatelessWidget { softWrap: false, ) : FutureBuilder( - future: room.lastEvent?.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: !room.isDirectChat || - room.directChatMatrixID != - room.lastEvent?.senderId, - ) ?? - Future.value(L10n.of(context)!.emptyChat), + // #Pangea + // future: room.lastEvent?.calcLocalizedBody( + // MatrixLocals(L10n.of(context)!), + // hideReply: true, + // hideEdit: true, + // plaintextBody: true, + // removeMarkdown: true, + // withSenderNamePrefix: !room.isDirectChat || + // room.directChatMatrixID != + // room.lastEvent?.senderId, + // ) ?? + // Future.value(L10n.of(context)!.emptyChat), + future: room.lastEvent != null + ? GetChatListItemSubtitle().getSubtitle( + context, + room.lastEvent, + MatrixState.pangeaController, + ) + : Future.value(L10n.of(context)!.emptyChat), + // Pangea# builder: (context, snapshot) { return Text( room.membership == Membership.invite @@ -361,6 +377,12 @@ class ChatListItem extends StatelessWidget { ], ), onTap: () => clickAction(context), + trailing: onForget == null + ? null + : IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: onForget, + ), ), ), ); diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index b60371746..a12611107 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -1,18 +1,17 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:badges/badges.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/navi_rail_item.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + import '../../widgets/matrix.dart'; import 'chat_list_body.dart'; import 'start_chat_fab.dart'; @@ -20,7 +19,7 @@ import 'start_chat_fab.dart'; class ChatListView extends StatelessWidget { final ChatListController controller; - const ChatListView(this.controller, {Key? key}) : super(key: key); + const ChatListView(this.controller, {super.key}); List getNavigationDestinations(BuildContext context) { final badgePosition = BadgePosition.topEnd(top: -12, end: -8); @@ -39,7 +38,10 @@ class ChatListView extends StatelessWidget { controller.getRoomFilterByActiveFilter(ActiveFilter.messages), child: const Icon(Icons.forum), ), - label: L10n.of(context)!.messages, + //#Pangea + // label: L10n.of(context)!.messages, + label: L10n.of(context)!.directChats, + //Pangea# ), NavigationDestination( icon: UnreadRoomsBadge( @@ -68,14 +70,24 @@ class ChatListView extends StatelessWidget { controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), child: const Icon(Icons.forum), ), - label: L10n.of(context)!.chats, + // #Pangea + // label: L10n.of(context)!.chats, + label: L10n.of(context)!.allChats, + // Pangea# ), if (controller.spaces.isNotEmpty) - const NavigationDestination( - icon: Icon(Icons.workspaces_outlined), - selectedIcon: Icon(Icons.workspaces), - label: 'Spaces', + // #Pangea + // const NavigationDestination( + // icon: Icon(Icons.workspaces_outlined), + // selectedIcon: Icon(Icons.workspaces), + // label: 'Spaces', + // ), + NavigationDestination( + icon: const Icon(Icons.workspaces_outlined), + selectedIcon: const Icon(Icons.workspaces), + label: L10n.of(context)!.allSpaces, ), + // Pangea# ]; } @@ -115,14 +127,17 @@ class ChatListView extends StatelessWidget { builder: (context) { final allSpaces = client.rooms.where((room) => room.isSpace); - final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) - .toList(); + // #Pangea + // final rootSpaces = allSpaces + // .where( + // (space) => !allSpaces.any( + // (parentSpace) => parentSpace.spaceChildren + // .any((child) => child.roomId == space.id), + // ), + // ) + // .toList(); + final rootSpaces = allSpaces.toList(); + // Pangea# final destinations = getNavigationDestinations(context); return SizedBox( @@ -144,13 +159,25 @@ class ChatListView extends StatelessWidget { final isSelected = controller.activeFilter == ActiveFilter.spaces && rootSpaces[i].id == controller.activeSpaceId; + //#Pangea + final Room? room = Matrix.of(context) + .client + .getRoomById(rootSpaces[i].id); + // Pangea# return NaviRailItem( toolTip: rootSpaces[i].getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ), isSelected: isSelected, - onTap: () => - controller.setActiveSpace(rootSpaces[i].id), + // #Pangea + // onTap: () => + // controller.setActiveSpace(rootSpaces[i].id), + onTap: () => chatListHandleSpaceTap( + context, + controller, + rootSpaces[i], + ), + // Pangea# icon: Avatar( mxContent: rootSpaces[i].avatar, name: rootSpaces[i].getLocalizedDisplayname( @@ -158,6 +185,9 @@ class ChatListView extends StatelessWidget { ), size: 32, fontSize: 12, + // #Pangea + littleIcon: room?.roomTypeIcon, + // Pangea# ), ); }, @@ -193,22 +223,32 @@ class ChatListView extends StatelessWidget { destinations: getNavigationDestinations(context), ) : null, - floatingActionButton: KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.keyN, - }, - onKeysPressed: () => context.go('/rooms/newprivatechat'), - helpLabel: L10n.of(context)!.newChat, - child: selectMode == SelectMode.normal && - !controller.isSearchMode - ? StartChatFloatingActionButton( - activeFilter: controller.activeFilter, - roomsIsEmpty: false, - scrolledToTop: controller.scrolledToTop, - ) - : const SizedBox.shrink(), - ), + // #Pangea + // floatingActionButton: KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.controlLeft, + // LogicalKeyboardKey.keyN, + // }, + // onKeysPressed: () => context.go('/rooms/newprivatechat'), + // helpLabel: L10n.of(context)!.newChat, + // child: selectMode == SelectMode.normal && + // !controller.isSearchMode + // ? StartChatFloatingActionButton( + // activeFilter: controller.activeFilter, + // roomsIsEmpty: false, + // scrolledToTop: controller.scrolledToTop, + // ) + // : const SizedBox.shrink(), + // ), + floatingActionButton: selectMode == SelectMode.normal + ? StartChatFloatingActionButton( + activeFilter: controller.activeFilter, + roomsIsEmpty: false, + scrolledToTop: controller.scrolledToTop, + controller: controller, + ) + : null, + // Pangea# ), ), ), diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 2791a4a0c..6253b74cd 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,80 +1,165 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/utils/class_code.dart'; +import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart'; +import 'package:fluffychat/pangea/utils/logout.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/fluffy_share.dart'; import 'chat_list.dart'; class ClientChooserButton extends StatelessWidget { final ChatListController controller; - const ClientChooserButton(this.controller, {Key? key}) : super(key: key); + const ClientChooserButton(this.controller, {super.key}); List> _bundleMenuItems(BuildContext context) { final matrix = Matrix.of(context); - final bundles = matrix.accountBundles.keys.toList() - ..sort( - (a, b) => a!.isValidMatrixId == b!.isValidMatrixId - ? 0 - : a.isValidMatrixId && !b.isValidMatrixId - ? -1 - : 1, - ); + // #Pangea + // final bundles = matrix.accountBundles.keys.toList() + // ..sort( + // (a, b) => a!.isValidMatrixId == b!.isValidMatrixId + // ? 0 + // : a.isValidMatrixId && !b.isValidMatrixId + // ? -1 + // : 1, + // ); + // Pangea# return >[ + // #Pangea + // PopupMenuItem( + // value: SettingsAction.newStory, + // child: Row( + // children: [ + // const Icon(Icons.camera_outlined), + // const SizedBox(width: 18), + // Text(L10n.of(context)!.yourStory), + // ], + // ), + // ), + // PopupMenuItem( + // value: SettingsAction.newGroup, + // child: Row( + // children: [ + // const Icon(Icons.group_add_outlined), + // const SizedBox(width: 18), + // Text(L10n.of(context)!.createGroup), + // ], + // ), + // ), PopupMenuItem( - value: SettingsAction.newStory, + value: SettingsAction.joinWithClassCode, child: Row( children: [ - const Icon(Icons.camera_outlined), + const Icon(Icons.join_full_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.yourStory), + Expanded(child: Text(L10n.of(context)!.joinWithClassCode)), ], ), ), PopupMenuItem( - value: SettingsAction.newGroup, + enabled: matrix.client.classesAndExchangesImTeaching.isNotEmpty, + value: SettingsAction.classAnalytics, child: Row( children: [ - const Icon(Icons.group_add_outlined), + const Icon(Icons.analytics_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.createGroup), + Expanded(child: Text(L10n.of(context)!.classAnalytics)), ], ), ), PopupMenuItem( - value: SettingsAction.newSpace, + enabled: matrix.client.classesImIn.isNotEmpty, + value: SettingsAction.myAnalytics, child: Row( children: [ - const Icon(Icons.workspaces_outlined), + const Icon(Icons.analytics_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.createNewSpace), + Expanded(child: Text(L10n.of(context)!.myLearning)), ], ), ), PopupMenuItem( - value: SettingsAction.invite, + value: SettingsAction.newClass, child: Row( children: [ - Icon(Icons.adaptive.share_outlined), + const Icon(Icons.school), const SizedBox(width: 18), - Text(L10n.of(context)!.inviteContact), + Expanded(child: Text(L10n.of(context)!.createNewClass)), ], ), ), + // PopupMenuItem( + // value: SettingsAction.newSpace, + // child: Row( + // children: [ + // const Icon(Icons.workspaces_outlined), + // const SizedBox(width: 18), + // Text(L10n.of(context)!.createNewSpace), + // ], + // ), + // ), + PopupMenuItem( + value: SettingsAction.newExchange, + child: Row( + children: [ + const Icon(Icons.connecting_airports), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.newExchange)), + ], + ), + ), + PopupMenuItem( + value: SettingsAction.findAClass, + enabled: false, + child: Row( + children: [ + const Icon(Icons.class_outlined), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.findAClass)), + ], + ), + ), + if (controller.pangeaController.permissionsController.isUser18()) + PopupMenuItem( + value: SettingsAction.findAConversationPartner, + child: Row( + children: [ + const Icon(Icons.add_circle_outline), + const SizedBox(width: 18), + Expanded(child: Text(L10n.of(context)!.findALanguagePartner)), + ], + ), + ), + // PopupMenuItem( + // value: SettingsAction.invite, + // child: Row( + // children: [ + // Icon(Icons.adaptive.share_outlined), + // const SizedBox(width: 18), + // Text(L10n.of(context)!.inviteContact), + // ], + // ), + // ), + // Pangea# PopupMenuItem( value: SettingsAction.archive, child: Row( children: [ const Icon(Icons.archive_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.archive), + // #Pangea + // Text(L10n.of(context)!.archive), + Expanded(child: Text(L10n.of(context)!.archive)), + // Pangea# ], ), ), @@ -84,87 +169,99 @@ class ClientChooserButton extends StatelessWidget { children: [ const Icon(Icons.settings_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.settings), + // #Pangea + // Text(L10n.of(context)!.settings), + Expanded(child: Text(L10n.of(context)!.settings)), + // Pangea# ], ), ), - const PopupMenuItem( - value: null, - child: Divider(height: 1), - ), - for (final bundle in bundles) ...[ - if (matrix.accountBundles[bundle]!.length != 1 || - matrix.accountBundles[bundle]!.single!.userID != bundle) - PopupMenuItem( - value: null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - bundle!, - style: TextStyle( - color: Theme.of(context).textTheme.titleMedium!.color, - fontSize: 14, - ), - ), - const Divider(height: 1), - ], - ), - ), - ...matrix.accountBundles[bundle]! - .map( - (client) => PopupMenuItem( - value: client, - child: FutureBuilder( - // analyzer does not understand this type cast for error - // handling - // - // ignore: unnecessary_cast - future: (client!.fetchOwnProfile() as Future) - .onError((e, s) => null), - builder: (context, snapshot) => Row( - children: [ - Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - client.userID!.localpart, - size: 32, - fontSize: 12, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - snapshot.data?.displayName ?? - client.userID!.localpart!, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => controller.editBundlesForAccount( - client.userID, - bundle, - ), - ), - ], - ), - ), - ), - ) - .toList(), - ], + // #Pangea + // const PopupMenuItem( + // value: null, + // child: Divider(height: 1), + // ), + // for (final bundle in bundles) ...[ + // if (matrix.accountBundles[bundle]!.length != 1 || + // matrix.accountBundles[bundle]!.single!.userID != bundle) + // PopupMenuItem( + // value: null, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // bundle!, + // style: TextStyle( + // color: Theme.of(context).textTheme.titleMedium!.color, + // fontSize: 14, + // ), + // ), + // const Divider(height: 1), + // ], + // ), + // ), + // ...matrix.accountBundles[bundle]!.map( + // (client) => PopupMenuItem( + // value: client, + // child: FutureBuilder( + // // analyzer does not understand this type cast for error + // // handling + // // + // // ignore: unnecessary_cast + // future: (client!.fetchOwnProfile() as Future) + // .onError((e, s) => null), + // builder: (context, snapshot) => Row( + // children: [ + // Avatar( + // mxContent: snapshot.data?.avatarUrl, + // name: + // snapshot.data?.displayName ?? client.userID!.localpart, + // size: 32, + // fontSize: 12, + // ), + // const SizedBox(width: 12), + // Expanded( + // child: Text( + // snapshot.data?.displayName ?? client.userID!.localpart!, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // const SizedBox(width: 12), + // IconButton( + // icon: const Icon(Icons.edit_outlined), + // onPressed: () => controller.editBundlesForAccount( + // client.userID, + // bundle, + // ), + // ), + // ], + // ), + // ), + // ), + // ), + // ], + // PopupMenuItem( + // value: SettingsAction.addAccount, + // child: Row( + // children: [ + // const Icon(Icons.person_add_outlined), + // const SizedBox(width: 18), + // Text(L10n.of(context)!.addAccount), + // ], + // ), + // ), PopupMenuItem( - value: SettingsAction.addAccount, + value: SettingsAction.logout, child: Row( children: [ - const Icon(Icons.person_add_outlined), + const Icon(Icons.logout_outlined), const SizedBox(width: 18), - Text(L10n.of(context)!.addAccount), + Expanded(child: Text(L10n.of(context)!.logout)), ], ), ), + // Pangea# ]; } @@ -214,17 +311,32 @@ class ClientChooserButton extends StatelessWidget { PopupMenuButton( onSelected: (o) => _clientSelected(o, context), itemBuilder: _bundleMenuItems, + // #Pangea child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(99), - child: Avatar( - mxContent: snapshot.data?.avatarUrl, - name: snapshot.data?.displayName ?? - matrix.client.userID!.localpart, - size: 32, - fontSize: 12, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + clipBehavior: Clip.hardEdge, + child: ListTile( + tileColor: Theme.of(context).scaffoldBackgroundColor, + hoverColor: Theme.of(context).colorScheme.onSurfaceVariant, + leading: const Icon(Icons.settings_outlined), + title: Text(L10n.of(context)!.mainMenu), ), ), + // child: Material( + // color: Colors.transparent, + // borderRadius: BorderRadius.circular(99), + // child: Avatar( + // mxContent: snapshot.data?.avatarUrl, + // name: snapshot.data?.displayName ?? + // matrix.client.userID!.localpart, + // size: 32, + // fontSize: 12, + // ), + // ), + // Pangea# ), ], ), @@ -252,26 +364,28 @@ class ClientChooserButton extends StatelessWidget { controller.setActiveBundle(object); } else if (object is SettingsAction) { switch (object) { - case SettingsAction.addAccount: - final consent = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context)!.addAccount, - message: L10n.of(context)!.enableMultiAccounts, - okLabel: L10n.of(context)!.next, - cancelLabel: L10n.of(context)!.cancel, - ); - if (consent != OkCancelResult.ok) return; - context.go('/rooms/settings/addaccount'); - break; - case SettingsAction.newStory: - context.go('/rooms/stories/create'); - break; - case SettingsAction.newGroup: - context.go('/rooms/newgroup'); - break; - case SettingsAction.newSpace: - context.go('/rooms/newspace'); - break; + // #Pangea + // case SettingsAction.addAccount: + // final consent = await showOkCancelAlertDialog( + // context: context, + // title: L10n.of(context)!.addAccount, + // message: L10n.of(context)!.enableMultiAccounts, + // okLabel: L10n.of(context)!.next, + // cancelLabel: L10n.of(context)!.cancel, + // ); + // if (consent != OkCancelResult.ok) return; + // context.go('/rooms/settings/addaccount'); + // break; + // case SettingsAction.newStory: + // context.go('/rooms/stories/create'); + // break; + // case SettingsAction.newGroup: + // context.go('/rooms/newgroup'); + // break; + // case SettingsAction.newSpace: + // context.go('/rooms/newspace'); + // break; + // Pangea# case SettingsAction.invite: FluffyShare.shareInviteLink(context); break; @@ -281,6 +395,39 @@ class ClientChooserButton extends StatelessWidget { case SettingsAction.archive: context.go('/rooms/archive'); break; + // #Pangea + case SettingsAction.newClass: + context.go('/rooms/newspace'); + break; + case SettingsAction.newExchange: + context.go('/rooms/newspace/exchange'); + break; + case SettingsAction.joinWithClassCode: + ClassCodeUtil.joinWithClassCodeDialog( + context, + controller.pangeaController, + null, + ); + break; + case SettingsAction.findAConversationPartner: + findConversationPartnerDialog( + context, + controller.pangeaController, + ); + break; + case SettingsAction.classAnalytics: + context.go('/rooms/analytics'); + break; + case SettingsAction.myAnalytics: + context.go('/rooms/mylearning'); + break; + case SettingsAction.findAClass: + debugger(when: kDebugMode, message: "left to implement"); + break; + case SettingsAction.logout: + pLogoutAction(context); + break; + // Pangea# } } } @@ -356,11 +503,23 @@ class ClientChooserButton extends StatelessWidget { } enum SettingsAction { - addAccount, - newStory, - newGroup, - newSpace, + // #Pangea + // addAccount, + // newStory, + // newGroup, + // newSpace, + // Pangea# invite, settings, archive, + // #Pangea + joinWithClassCode, + classAnalytics, + myAnalytics, + findAClass, + findAConversationPartner, + logout, + newClass, + newExchange + // Pangea# } diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index 7eef55468..52bc5d214 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -1,6 +1,6 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/config/app_config.dart'; import '../../config/themes.dart'; class NaviRailItem extends StatefulWidget { @@ -16,8 +16,8 @@ class NaviRailItem extends StatefulWidget { required this.onTap, required this.icon, this.selectedIcon, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _NaviRailItemState(); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 8bbb63691..d67b92421 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -1,17 +1,22 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.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/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/archive_space.dart'; +import 'package:fluffychat/pangea/utils/chat_list_handle_space_tap.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/avatar.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_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/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/avatar.dart'; import '../../utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; import 'chat_list_header.dart'; @@ -21,9 +26,9 @@ class SpaceView extends StatefulWidget { final ScrollController scrollController; const SpaceView( this.controller, { - Key? key, + super.key, required this.scrollController, - }) : super(key: key); + }); @override State createState() => _SpaceViewState(); @@ -35,9 +40,15 @@ class _SpaceViewState extends State { String? prevBatch; void _refresh() { - setState(() { - _requests.remove(widget.controller.activeSpaceId); - }); + // #Pangea + if (mounted) { + // Pangea# + setState(() { + _requests.remove(widget.controller.activeSpaceId); + }); + // #Pangea + } + // Pangea# } Future getFuture(String activeSpaceId) => @@ -73,7 +84,10 @@ class _SpaceViewState extends State { } if (spaceChild.roomType == 'm.space') { if (spaceChild.roomId == widget.controller.activeSpaceId) { - context.go('/rooms/${spaceChild.roomId}'); + // #Pangea + // context.go('/rooms/${spaceChild.roomId}'); + context.push('/spaces/${spaceChild.roomId}'); + // Pangea# } else { widget.controller.setActiveSpace(spaceChild.roomId); } @@ -110,11 +124,31 @@ class _SpaceViewState extends State { label: L10n.of(context)!.removeFromSpace, icon: Icons.delete_sweep_outlined, ), + // #Pangea + if (room != null && + room.ownPowerLevel >= ClassDefaultValues.powerLevelOfAdmin) + SheetAction( + key: SpaceChildContextAction.addToSpace, + label: L10n.of(context)!.addToSpace, + icon: Icons.workspaces_outlined, + ), + if (room != null && room.isRoomAdmin) + SheetAction( + key: SpaceChildContextAction.archive, + label: room.isSpace + ? L10n.of(context)!.archiveSpace + : L10n.of(context)!.archive, + icon: Icons.architecture_outlined, + ), + // Pangea# if (room != null) SheetAction( key: SpaceChildContextAction.leave, label: L10n.of(context)!.leave, - icon: Icons.delete_outlined, + // #Pangea + // icon: Icons.delete_outlined, + icon: Icons.arrow_forward, + // Pangea# isDestructiveAction: true, ), ], @@ -137,9 +171,49 @@ class _SpaceViewState extends State { future: () => activeSpace!.removeSpaceChild(spaceChild!.roomId), ); break; + // #Pangea + case SpaceChildContextAction.archive: + widget.controller.cancelAction(); + widget.controller.toggleSelection(room!.id); + room.isSpace + ? await showFutureLoadingDialog( + context: context, + future: () async { + await archiveSpace( + room, + Matrix.of(context).client, + ); + widget.controller.selectedRoomIds.clear(); + }, + ) + : await widget.controller.archiveAction(); + _refresh(); + break; + case SpaceChildContextAction.addToSpace: + widget.controller.cancelAction(); + widget.controller.toggleSelection(room!.id); + await widget.controller.addToSpace(); + break; + // Pangea# } } + // #Pangea + StreamSubscription? _roomSubscription; + + @override + void initState() { + super.initState(); + _refresh(); + } + + @override + void dispose() { + super.dispose(); + _roomSubscription?.cancel(); + } + // Pangea# + @override Widget build(BuildContext context) { final client = Matrix.of(context).client; @@ -147,12 +221,14 @@ class _SpaceViewState extends State { final allSpaces = client.rooms.where((room) => room.isSpace); if (activeSpaceId == null) { final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) + // #Pangea + // .where( + // (space) => !allSpaces.any( + // (parentSpace) => parentSpace.spaceChildren + // .any((child) => child.roomId == space.id), + // ), + // ) + // Pangea# .toList(); return CustomScrollView( @@ -172,17 +248,34 @@ class _SpaceViewState extends State { leading: Avatar( mxContent: rootSpace.avatar, name: displayname, + // #Pangea + littleIcon: rootSpace.roomTypeIcon, + // Pangea# ), title: Text( displayname, maxLines: 1, overflow: TextOverflow.ellipsis, ), + // #Pangea subtitle: Text( - L10n.of(context)! - .numChats(rootSpace.spaceChildren.length.toString()), + rootSpace.membership == Membership.join + ? L10n.of(context)!.numChats( + rootSpace.spaceChildren.length.toString(), + ) + : L10n.of(context)!.youreInvited, ), - onTap: () => widget.controller.setActiveSpace(rootSpace.id), + onTap: () => chatListHandleSpaceTap( + context, + widget.controller, + rootSpaces[i], + ), + // subtitle: Text( + // L10n.of(context)! + // .numChats(rootSpace.spaceChildren.length.toString()), + // ), + // onTap: () => widget.controller.setActiveSpace(rootSpace.id), + // Pangea# onLongPress: () => _onSpaceChildContextMenu(null, rootSpace), trailing: const Icon(Icons.chevron_right_outlined), @@ -195,6 +288,25 @@ class _SpaceViewState extends State { ], ); } + + // #Pangea + _roomSubscription = client.onSync.stream + .where((event) => event.rooms?.join?.isNotEmpty ?? false) + .listen((event) { + if (mounted) { + final List joinedRoomIds = event.rooms!.join!.keys.toList(); + final joinedRoomFutures = joinedRoomIds.map( + (joinedRoomId) => client.waitForRoomInSync( + joinedRoomId, + join: true, + ), + ); + Future.wait(joinedRoomFutures).then((_) { + _refresh(); + }); + } + }); + // Pangea# return FutureBuilder( future: getFuture(activeSpaceId), builder: (context, snapshot) { @@ -232,7 +344,48 @@ class _SpaceViewState extends State { (space) => space.spaceChildren.any((child) => child.roomId == activeSpaceId), ); - final spaceChildren = response.rooms; + // #Pangea + // final spaceChildren = response.rooms; + List spaceChildren = response.rooms; + 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.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; + }); + // Pangea# + final canLoadMore = response.nextBatch != null; return WillPopScope( onWillPop: () async { @@ -318,15 +471,24 @@ class _SpaceViewState extends State { .withAlpha(128), trailing: const Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Icon(Icons.edit_outlined), + // #Pangea + // child: Icon(Icons.edit_outlined), + child: Icon(Icons.settings_outlined), + // Pangea# ), - onTap: () => _onJoinSpaceChild(spaceChild), + // #Pangea + // onTap: () => _onJoinSpaceChild(spaceChild), + onTap: () => context.go('/spaces/${spaceChild.roomId}'), + // Pangea# ); } return ListTile( leading: Avatar( mxContent: spaceChild.avatarUrl, name: spaceChild.name, + //#Pangea + littleIcon: room?.roomTypeIcon, + //Pangea# ), title: Row( children: [ @@ -353,7 +515,16 @@ class _SpaceViewState extends State { ], ], ), - onTap: () => _onJoinSpaceChild(spaceChild), + //#Pangea + // onTap: () => _onJoinSpaceChild(spaceChild), + onTap: room?.isSpace ?? false + ? () => chatListHandleSpaceTap( + context, + widget.controller, + room!, + ) + : () => _onJoinSpaceChild(spaceChild), + //Pangea# onLongPress: () => _onSpaceChildContextMenu(spaceChild, room), subtitle: Text( @@ -386,4 +557,9 @@ enum SpaceChildContextAction { join, leave, removeFromSpace, + // #Pangea + // deleteChat, + archive, + addToSpace + // Pangea# } diff --git a/lib/pages/chat_list/start_chat_fab.dart b/lib/pages/chat_list/start_chat_fab.dart index f702ad69d..059181e09 100644 --- a/lib/pages/chat_list/start_chat_fab.dart +++ b/lib/pages/chat_list/start_chat_fab.dart @@ -1,31 +1,48 @@ -import 'package:flutter/material.dart'; +import 'dart:core'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; -import '../../config/themes.dart'; -import 'chat_list.dart'; - class StartChatFloatingActionButton extends StatelessWidget { final ActiveFilter activeFilter; final ValueNotifier scrolledToTop; final bool roomsIsEmpty; + // #Pangea + final ChatListController controller; + // Pangea# const StartChatFloatingActionButton({ - Key? key, + super.key, required this.activeFilter, required this.scrolledToTop, required this.roomsIsEmpty, - }) : super(key: key); + // #Pangea + required this.controller, + // Pangea# + }); void _onPressed(BuildContext context) { + //#Pangea + if (controller.activeSpaceId != null) { + context.go('/rooms/newgroup/${controller.activeSpaceId}'); + return; + } + //Pangea# switch (activeFilter) { case ActiveFilter.allChats: case ActiveFilter.messages: - context.go('/rooms/newprivatechat'); - break; + // #Pangea + // context.go('/rooms/newprivatechat'); + // break; + // Pangea# case ActiveFilter.groups: - context.go('/rooms/newgroup'); + // #Pangea + // context.go('/rooms/newgroup'); + context.go('/rooms/newgroup/${controller.activeSpaceId}'); + // Pangea# break; case ActiveFilter.spaces: context.go('/rooms/newspace'); @@ -34,10 +51,17 @@ class StartChatFloatingActionButton extends StatelessWidget { } IconData get icon { + // #Pangea + if (controller.activeSpaceId != null) { + return Icons.group_add_outlined; + } + // Pangea# switch (activeFilter) { case ActiveFilter.allChats: case ActiveFilter.messages: - return Icons.add_outlined; + // #Pangea + // return Icons.add_outlined; + // Pangea# case ActiveFilter.groups: return Icons.group_add_outlined; case ActiveFilter.spaces: @@ -46,6 +70,11 @@ class StartChatFloatingActionButton extends StatelessWidget { } String getLabel(BuildContext context) { + // #Pangea + if (controller.activeSpaceId != null) { + return L10n.of(context)!.newGroup; + } + // Pangea# switch (activeFilter) { case ActiveFilter.allChats: case ActiveFilter.messages: diff --git a/lib/pages/chat_list/stories_header.dart b/lib/pages/chat_list/stories_header.dart index ab6caabbc..c655cf2e1 100644 --- a/lib/pages/chat_list/stories_header.dart +++ b/lib/pages/chat_list/stories_header.dart @@ -1,17 +1,16 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.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/config/app_config.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + import '../../config/themes.dart'; enum ContextualRoomAction { @@ -23,7 +22,7 @@ enum ContextualRoomAction { class StoriesHeader extends StatelessWidget { final String filter; - const StoriesHeader({required this.filter, Key? key}) : super(key: key); + const StoriesHeader({required this.filter, super.key}); void _addToStoryAction(BuildContext context) => context.go('/rooms/stories/create'); @@ -186,8 +185,7 @@ class _StoryButton extends StatefulWidget { this.hasPosts = true, this.unread = false, this.onLongPressed, - Key? key, - }) : super(key: key); + }); @override State<_StoryButton> createState() => _StoryButtonState(); diff --git a/lib/pages/chat_list/test.dart b/lib/pages/chat_list/test.dart new file mode 100644 index 000000000..22c34d193 --- /dev/null +++ b/lib/pages/chat_list/test.dart @@ -0,0 +1,49 @@ + + // void resetActiveSpaceId() { + // setState(() { + // activeSpaceId = null; + // }); + // } + + // void setActiveSpace(String? spaceId) { + // setState(() { + // activeSpaceId = spaceId; + // activeFilter = ActiveFilter.spaces; + // }); + // } + + // int get selectedIndex { + // switch (activeFilter) { + // case ActiveFilter.allChats: + // case ActiveFilter.messages: + // return 0; + // case ActiveFilter.groups: + // return 1; + // case ActiveFilter.spaces: + // return AppConfig.separateChatTypes ? 2 : 1; + // } + // } + + // ActiveFilter getActiveFilterByDestination(int? i) { + // switch (i) { + // case 1: + // if (AppConfig.separateChatTypes) { + // return ActiveFilter.groups; + // } + // return ActiveFilter.spaces; + // case 2: + // return ActiveFilter.spaces; + // case 0: + // default: + // if (AppConfig.separateChatTypes) { + // return ActiveFilter.messages; + // } + // return ActiveFilter.allChats; + // } + // } + + // void onDestinationSelected(int? i) { + // setState(() { + // activeFilter = getActiveFilterByDestination(i); + // }); + // } \ No newline at end of file diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart index 394206f17..c5ba36493 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart @@ -1,17 +1,15 @@ import 'dart:developer'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings_view.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/permission_slider_dialog.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_permissions_settings/chat_permissions_settings_view.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/permission_slider_dialog.dart'; - class ChatPermissionsSettings extends StatefulWidget { const ChatPermissionsSettings({Key? key}) : super(key: key); diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart index fd10b9af0..700152d93 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings_view.dart @@ -1,12 +1,10 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; import 'package:fluffychat/pages/chat_permissions_settings/permission_list_tile.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.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'; class ChatPermissionsSettingsView extends StatelessWidget { final ChatPermissionsSettingsController controller; diff --git a/lib/pages/chat_permissions_settings/permission_list_tile.dart b/lib/pages/chat_permissions_settings/permission_list_tile.dart index 0ac68f44c..3b577ac36 100644 --- a/lib/pages/chat_permissions_settings/permission_list_tile.dart +++ b/lib/pages/chat_permissions_settings/permission_list_tile.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index 59e9e6583..0153fc35c 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -1,15 +1,14 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:fluffychat/pages/device_settings/device_settings_view.dart'; +import 'package:fluffychat/pages/key_verification/key_verification_dialog.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/device_settings/device_settings_view.dart'; -import 'package:fluffychat/pages/key_verification/key_verification_dialog.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; class DevicesSettings extends StatefulWidget { @@ -34,6 +33,9 @@ class DevicesSettingsController extends State { void removeDevicesAction(List devices) async { if (await showOkCancelAlertDialog( + // #Pangea + useRootNavigator: false, + // Pangea# context: context, title: L10n.of(context)!.areYouSure, okLabel: L10n.of(context)!.yes, @@ -68,6 +70,9 @@ class DevicesSettingsController extends State { void renameDeviceAction(Device device) async { final displayName = await showTextInputDialog( + // #Pangea + useRootNavigator: false, + // Pangea# context: context, title: L10n.of(context)!.changeDeviceName, okLabel: L10n.of(context)!.ok, diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index b03f0dc7b..b87217b4f 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pages/device_settings/device_settings.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'user_device_list_item.dart'; class DevicesSettingsView extends StatelessWidget { diff --git a/lib/pages/dialer/dialer.dart b/lib/pages/dialer/dialer.dart index 1af65ce9d..ab2462b3a 100644 --- a/lib/pages/dialer/dialer.dart +++ b/lib/pages/dialer/dialer.dart @@ -19,9 +19,11 @@ import 'dart:async'; import 'dart:math'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; @@ -30,18 +32,14 @@ import 'package:matrix/matrix.dart'; import 'package:vibration/vibration.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar.dart'; import 'pip/pip_view.dart'; class _StreamView extends StatelessWidget { const _StreamView( this.wrappedStream, { - Key? key, this.mainView = false, required this.matrixClient, - }) : super(key: key); + }); final WrappedMediaStream wrappedStream; final Client matrixClient; @@ -128,8 +126,8 @@ class Calling extends StatefulWidget { required this.client, required this.callId, this.onClear, - Key? key, - }) : super(key: key); + super.key, + }); @override MyCallingPage createState() => MyCallingPage(); diff --git a/lib/pages/homeserver_picker/homeserver_app_bar.dart b/lib/pages/homeserver_picker/homeserver_app_bar.dart index d1fd7149e..88797ca68 100644 --- a/lib/pages/homeserver_picker/homeserver_app_bar.dart +++ b/lib/pages/homeserver_picker/homeserver_app_bar.dart @@ -1,18 +1,16 @@ +import 'package:fluffychat/config/app_config.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'homeserver_bottom_sheet.dart'; import 'homeserver_picker.dart'; class HomeserverAppBar extends StatelessWidget { final HomeserverPickerController controller; - const HomeserverAppBar({Key? key, required this.controller}) - : super(key: key); + const HomeserverAppBar({super.key, required this.controller}); @override Widget build(BuildContext context) { diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index a5df23a51..5a2ac62d8 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -1,11 +1,19 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/tor_stub.dart' + if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -14,18 +22,10 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/tor_stub.dart' - if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; - class HomeserverPicker extends StatefulWidget { - const HomeserverPicker({Key? key}) : super(key: key); + const HomeserverPicker({super.key}); @override HomeserverPickerController createState() => HomeserverPickerController(); @@ -113,12 +113,20 @@ class HomeserverPickerController extends State { Map? _rawLoginTypes; - void ssoLoginAction(String id) async { + // #Pangea + // void ssoLoginAction(String id) async { + void ssoLoginAction(IdentityProvider provider) async { + final id = provider.id!; + //Pangea# final redirectUrl = kIsWeb - ? '${html.window.origin!}/web/auth.html' - : isDefaultPlatform - ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login' - : 'http://localhost:3001//login'; + // #Pangea + // ? '${html.window.origin!}/web/auth.html' + // : isDefaultPlatform + // ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login' + // : 'http://localhost:3001//login'; + ? '${html.window.origin!}/auth.html' + : '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'; + //Pangea# final url = '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}'; final urlScheme = isDefaultPlatform @@ -131,7 +139,10 @@ class HomeserverPickerController extends State { final token = Uri.parse(result).queryParameters['loginToken']; if (token?.isEmpty ?? false) return; - await showFutureLoadingDialog( + // #Pangea + final loginRes = await showFutureLoadingDialog( + // await showFutureLoadingDialog( + // Pangea# context: context, future: () => Matrix.of(context).getLoginClient().login( LoginType.mLoginToken, @@ -139,6 +150,11 @@ class HomeserverPickerController extends State { initialDeviceDisplayName: PlatformInfos.clientName, ), ); + //Pangea# + if (loginRes.result != null) { + GoogleAnalytics.login(provider.name!, loginRes.result!.userId); + } + //Pangea# } List? get identityProviders { @@ -183,6 +199,9 @@ class HomeserverPickerController extends State { Matrix.of(context).initMatrix(); } catch (e, s) { Logs().e('Future error:', e, s); + // #Pangea + ErrorHandler.logError(e: e, s: s); + // Pangea# } }, ); diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 3cd9d713e..94cce0698 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -1,71 +1,81 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/pages/connect/p_sso_button.dart'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; +import 'package:fluffychat/pangea/widgets/signup/signup_buttons.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import '../../config/themes.dart'; -import '../../widgets/mxc_image.dart'; -import 'homeserver_app_bar.dart'; import 'homeserver_picker.dart'; class HomeserverPickerView extends StatelessWidget { final HomeserverPickerController controller; - const HomeserverPickerView(this.controller, {Key? key}) : super(key: key); + const HomeserverPickerView(this.controller, {super.key}); @override Widget build(BuildContext context) { final identityProviders = controller.identityProviders; final errorText = controller.error; return LoginScaffold( - enforceMobileMode: Matrix.of(context).client.isLogged(), + // #Pangea + // enforceMobileMode: Matrix.of(context).client.isLogged(), + // appBar: AppBar( + // titleSpacing: 12, + // automaticallyImplyLeading: false, + // title: HomeserverAppBar(controller: controller), + // ), appBar: AppBar( - titleSpacing: 12, - automaticallyImplyLeading: false, - title: HomeserverAppBar(controller: controller), + centerTitle: true, + title: Text( + AppConfig.applicationName, + ), ), + // Pangea# body: SafeArea( child: Column( children: [ // display a prominent banner to import session for TOR browser // users. This feature is just some UX sugar as TOR users are // usually forced to logout as TOR browser is non-persistent - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(8)), - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.hydrateTor), - subtitle: Text(L10n.of(context)!.hydrateTorLong), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.restoreBackup, - ), - ), - ), + // #Pangea + // AnimatedContainer( + // height: controller.isTorBrowser ? 64 : 0, + // duration: FluffyThemes.animationDuration, + // curve: FluffyThemes.animationCurve, + // clipBehavior: Clip.hardEdge, + // decoration: const BoxDecoration(), + // child: Material( + // clipBehavior: Clip.hardEdge, + // borderRadius: + // const BorderRadius.vertical(bottom: Radius.circular(8)), + // color: Theme.of(context).colorScheme.surface, + // child: ListTile( + // leading: const Icon(Icons.vpn_key), + // title: Text(L10n.of(context)!.hydrateTor), + // subtitle: Text(L10n.of(context)!.hydrateTorLong), + // trailing: const Icon(Icons.chevron_right_outlined), + // onTap: controller.restoreBackup, + // ), + // ), + // ), + // Pangea# Expanded( child: controller.isLoading ? const Center(child: CircularProgressIndicator.adaptive()) : ListView( children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FluffyThemes.isColumnMode(context) - ? Image.asset( - 'assets/info-logo.png', - height: 96, - ) - : Image.asset('assets/banner_transparent.png'), - ), + // #Pangea + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 8.0), + // child: FluffyThemes.isColumnMode(context) + // ? Image.asset( + // 'assets/info-logo.png', + // height: 96, + // ) + // : Image.asset('assets/banner_transparent.png'), + // ), + // Pangea# const SizedBox(height: 12), if (errorText != null) ...[ const Center( @@ -86,73 +96,116 @@ class HomeserverPickerView extends StatelessWidget { ), ), ), - Center( - child: Text( - L10n.of(context)! - .pleaseTryAgainLaterOrChooseDifferentServer, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: 12, - ), - ), - ), + // #Pangea + // Center( + // child: Text( + // L10n.of(context)! + // .pleaseTryAgainLaterOrChooseDifferentServer, + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Theme.of(context).colorScheme.error, + // fontSize: 12, + // ), + // ), + // ), + // Pangea# const SizedBox(height: 12), ], + // #Pangea + const SignupButtons(), + // Pangea# if (identityProviders != null) ...[ ...identityProviders.map( - (provider) => _LoginButton( - icon: provider.icon == null - ? const Icon(Icons.open_in_new_outlined) - : Material( - color: Colors.white, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - clipBehavior: Clip.hardEdge, - child: MxcImage( - placeholder: (_) => - const Icon(Icons.web_outlined), - uri: Uri.parse(provider.icon!), - width: 24, - height: 24, - ), - ), - label: L10n.of(context)!.signInWith( - provider.name ?? - provider.brand ?? - L10n.of(context)!.singlesignon, + // #Pangea + (provider) => Padding( + padding: const EdgeInsets.all(12.0), + child: Hero( + tag: + "ssobutton ${provider.id ?? provider.name}", + child: PangeaSsoButton( + identityProvider: provider, + onPressed: () => + controller.ssoLoginAction(provider), + ), ), - onPressed: () => - controller.ssoLoginAction(provider.id!), ), + // (provider) => _LoginButton( + // icon: provider.icon == null + // ? const Icon(Icons.open_in_new_outlined) + // : Material( + // color: Colors.white, + // borderRadius: BorderRadius.circular( + // AppConfig.borderRadius, + // ), + // clipBehavior: Clip.hardEdge, + // child: MxcImage( + // placeholder: (_) => + // const Icon(Icons.web_outlined), + // uri: Uri.parse(provider.icon!), + // width: 24, + // height: 24, + // ), + // ), + // label: L10n.of(context)!.signInWith( + // provider.name ?? + // provider.brand ?? + // L10n.of(context)!.singlesignon, + // ), + // onPressed: () => + // controller.ssoLoginAction(provider.id!), + // Pangea# ), ], if (controller.supportsPasswordLogin) - _LoginButton( - onPressed: controller.login, - icon: const Icon(Icons.login_outlined), - label: L10n.of(context)!.signInWithPassword, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Center( - child: SizedBox( - width: 256, - child: TextButton( - style: TextButton.styleFrom( - padding: - const EdgeInsets.symmetric(vertical: 12), - ), - onPressed: controller.restoreBackup, - child: Text( - L10n.of(context)!.hydrate, - textAlign: TextAlign.center, + // #Pangea + Padding( + padding: const EdgeInsets.all(12.0), + child: Hero( + tag: 'signinButton', + child: ElevatedButton( + onPressed: controller.login, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const PangeaLogoSvg(width: 20), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: Text( + "${L10n.of(context)!.loginOrSignup} Pangea Chat", + ), + ), + ], ), ), ), ), - ), + // _LoginButton( + // onPressed: controller.login, + // icon: const Icon(Icons.login_outlined), + // label: L10n.of(context)!.signInWithPassword, + // ), + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 12), + // child: Center( + // child: SizedBox( + // width: 256, + // child: TextButton( + // style: TextButton.styleFrom( + // padding: + // const EdgeInsets.symmetric(vertical: 12), + // ), + // onPressed: controller.restoreBackup, + // child: Text( + // L10n.of(context)!.hydrate, + // textAlign: TextAlign.center, + // ), + // ), + // ), + // ), + // ), + // Pangea# ], ), ), @@ -172,8 +225,7 @@ class _LoginButton extends StatelessWidget { required this.icon, required this.label, required this.onPressed, - Key? key, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index ac4505c40..73a52c4c7 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - -import 'package:go_router/go_router.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + import '../../utils/matrix_sdk_extensions/event_extension.dart'; class ImageViewer extends StatefulWidget { diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 3bd6166c7..9d9c74295 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'image_viewer.dart'; class ImageViewerView extends StatelessWidget { diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 74d301630..5ac12f9a8 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -1,23 +1,30 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/invitation_selection/invitation_selection_view.dart'; +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/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/invitation_selection/invitation_selection_view.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; +//#Pangea +enum InvitationSelectionMode { admin, member } +//Pangea# + class InvitationSelection extends StatefulWidget { final String roomId; const InvitationSelection({ - Key? key, + super.key, required this.roomId, - }) : super(key: key); + }); @override InvitationSelectionController createState() => @@ -49,9 +56,74 @@ class InvitationSelectionController extends State { b.calcDisplayname().toLowerCase(), ), ); - return contacts; + //#Pangea + // return contacts; + return contacts.where((u) => u.id != BotName.byEnvironment).toList(); + //Pangea# } + //#Pangea + // add all students (already local) from spaceParents who aren't already in room to eligibleStudents + // use room.members to get all users in room + bool _initialized = false; + Future> eligibleStudents( + BuildContext context, + String text, + ) async { + if (!_initialized) { + _initialized = true; + await requestParentSpaceParticipants(); + } + + final eligibleStudents = []; + final spaceParents = room.pangeaSpaceParents; + final userId = Matrix.of(context).client.userID; + for (final Room space in spaceParents) { + eligibleStudents.addAll( + space.getParticipants().where( + (spaceUser) => + spaceUser.id != BotName.byEnvironment && + spaceUser.id != "@support:staging.pangea.chat" && + spaceUser.id != userId && + (text.isEmpty || + (spaceUser.displayName + ?.toLowerCase() + .contains(text.toLowerCase()) ?? + false) || + spaceUser.id.toLowerCase().contains(text.toLowerCase())), + ), + ); + } + return eligibleStudents; + } + + Future + eligibleStudentsAsSearchUserDirectoryResponse( + BuildContext context, + String text, + ) async { + return SearchUserDirectoryResponse( + results: (await eligibleStudents(context, text)) + .map( + (e) => Profile( + userId: e.id, + avatarUrl: e.avatarUrl, + displayName: e.displayName, + ), + ) + .toList(), + limited: false, + ); + } + + List studentsInRoom(BuildContext context) => room + .getParticipants() + .where( + (u) => [Membership.join, Membership.invite].contains(u.membership), + ) + .toList(); + //Pangea# + void inviteAction(BuildContext context, String id, String displayname) async { final room = Matrix.of(context).client.getRoomById(roomId!)!; if (OkCancelResult.ok != @@ -71,7 +143,29 @@ class InvitationSelectionController extends State { } final success = await showFutureLoadingDialog( context: context, - future: () => room.invite(id), + //#Pangea + // future: () => room.invite(id), + future: () => Future.wait([ + room.invite(id), + room.setPower(id, ClassDefaultValues.powerLevelOfAdmin), + if (room.isSpace) + ...room.spaceChildren + .map( + (e) => roomId != null + ? Matrix.of(context).client.getRoomById(e.roomId!) + : null, + ) + .where((element) => element != null) + .cast() + .map( + (e) => Future.wait([ + e.invite(id), + e.setPower(id, ClassDefaultValues.powerLevelOfAdmin), + ]), + ), + ]), + onError: (e) => ErrorHandler.logError(e: e, s: StackTrace.current), + // Pangea# ); if (success.error == null) { ScaffoldMessenger.of(context).showSnackBar( @@ -102,7 +196,15 @@ class InvitationSelectionController extends State { final matrix = Matrix.of(context); SearchUserDirectoryResponse response; try { - response = await matrix.client.searchUserDirectory(text, limit: 10); + //#Pangea + // response = await matrix.client.searchUserDirectory(text, limit: 10); + response = await (mode == InvitationSelectionMode.admin + ? matrix.client.searchUserDirectory(text, limit: 10) + : eligibleStudentsAsSearchUserDirectoryResponse( + context, + text, + )); + //Pangea# } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text((e).toLocalizedString(context))), @@ -121,9 +223,82 @@ class InvitationSelectionController extends State { ], ); } + //#Pangea + final participants = Matrix.of(context) + .client + .getRoomById(roomId!)! + .getParticipants() + .where( + (user) => + [Membership.join, Membership.invite].contains(user.membership), + ) + .toList(); + foundProfiles.removeWhere( + (profile) => + participants.indexWhere((u) => u.id == profile.userId) != -1 && + BotName.byEnvironment != profile.userId, + ); + //Pangea# }); } + //#Pangea + Room? _room; + Room get room => _room ??= Matrix.of(context).client.getRoomById(roomId!)!; + + // request participants for all parent spaces + Future requestParentSpaceParticipants() async { + final spaceParents = room.pangeaSpaceParents; + await Future.wait([ + ...spaceParents.map((r) async { + await r.requestParticipants(); + }), + room.requestParticipants(), + ]); + } + + InvitationSelectionMode mode = InvitationSelectionMode.member; + + StreamSubscription? _spaceSubscription; + @override + void initState() { + Future.delayed( + Duration.zero, + () => setState( + () => mode = room.isSpace + ? InvitationSelectionMode.admin + : InvitationSelectionMode.member, + ), + ); + _spaceSubscription = Matrix.of(context) + .client + .onSync + .stream + .where( + (event) => + event.rooms?.join?.keys.any( + (ithRoomId) => room.pangeaSpaceParents + .map((e) => e.id) + .contains(ithRoomId), + ) ?? + false, + ) + .listen( + (SyncUpdate syncUpdate) async { + await requestParentSpaceParticipants(); + setState(() {}); + }, + ); + super.initState(); + } + + @override + void dispose() { + _spaceSubscription?.cancel(); + super.dispose(); + } + //Pangea# + @override Widget build(BuildContext context) => InvitationSelectionView(this); } diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index 202e4b378..4b6321fb2 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -1,17 +1,15 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.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'; class InvitationSelectionView extends StatelessWidget { final InvitationSelectionController controller; - const InvitationSelectionView(this.controller, {Key? key}) : super(key: key); + const InvitationSelectionView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -28,7 +26,9 @@ class InvitationSelectionView extends StatelessWidget { ); } - final groupName = room.name.isEmpty ? L10n.of(context)!.group : room.name; + // #Pangea + // final groupName = room.name.isEmpty ? L10n.of(context)!.group : room.name; + // Pangea# return Scaffold( appBar: AppBar( leading: const Center(child: BackButton()), @@ -43,7 +43,12 @@ class InvitationSelectionView extends StatelessWidget { child: TextField( textInputAction: TextInputAction.search, decoration: InputDecoration( - hintText: L10n.of(context)!.inviteContactToGroup(groupName), + // #Pangea + hintText: controller.mode == InvitationSelectionMode.admin + ? L10n.of(context)!.inviteUsersFromPangea + : L10n.of(context)!.inviteStudentByUserName, + // hintText: L10n.of(context)!.inviteContactToGroup(groupName), + // Pangea# prefixIcon: controller.loading ? const Padding( padding: EdgeInsets.symmetric( @@ -92,7 +97,12 @@ class InvitationSelectionView extends StatelessWidget { ), ) : FutureBuilder>( - future: controller.getContacts(context), + // #Pangea + future: controller.mode == InvitationSelectionMode.admin + ? controller.getContacts(context) + : controller.eligibleStudents(context, ""), + // future: controller.getContacts(context), + // Pangea# builder: (BuildContext context, snapshot) { if (!snapshot.hasData) { return const Center( @@ -142,13 +152,12 @@ class _InviteContactListTile extends StatelessWidget { final void Function() onTap; const _InviteContactListTile({ - Key? key, required this.userId, required this.displayname, required this.avatarUrl, required this.isMember, required this.onTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index 7d9633ccd..4bb8c1572 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -1,18 +1,16 @@ import 'dart:convert'; import 'dart:ui'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -import 'package:fluffychat/widgets/avatar.dart'; - class KeyVerificationDialog extends StatefulWidget { Future show(BuildContext context) => showAdaptiveBottomSheet( context: context, diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index d4d5d1f26..23edaff28 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -1,19 +1,20 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/utils/localized_exception_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/utils/localized_exception_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/platform_infos.dart'; import 'login_view.dart'; class Login extends StatefulWidget { - const Login({Key? key}) : super(key: key); + const Login({super.key}); @override LoginController createState() => LoginController(); @@ -27,6 +28,27 @@ class LoginController extends State { bool loading = false; bool showPassword = false; + // #Pangea + final PangeaController pangeaController = MatrixState.pangeaController; + @override + void initState() { + // TODO: implement initState + super.initState(); + loading = true; + pangeaController.checkHomeServerAction().then((value) { + setState(() { + loading = false; + }); + }).catchError((e) { + final String err = e as String; + setState(() { + loading = false; + passwordError = err.toLocalizedString(context); + }); + }); + } + // Pangea# + void toggleShowPassword() => setState(() => showPassword = !loading && !showPassword); @@ -67,7 +89,10 @@ class LoginController extends State { } else { identifier = AuthenticationUserIdentifier(user: username); } - await matrix.getLoginClient().login( + // #Pangea + // await matrix.getLoginClient().login( + final loginRes = await matrix.getLoginClient().login( + // Pangea# LoginType.mLoginPassword, identifier: identifier, // To stay compatible with older server versions @@ -78,6 +103,9 @@ class LoginController extends State { password: passwordController.text, initialDeviceDisplayName: PlatformInfos.clientName, ); + // #Pangea + GoogleAnalytics.login("pangea", loginRes.userId); + // Pangea# } on MatrixException catch (exception) { setState(() => passwordError = exception.errorMessage); return setState(() => loading = false); diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index 50fa1e383..9ce89a4fc 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -1,33 +1,36 @@ +import 'package:fluffychat/pangea/utils/password_forgotten.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'login.dart'; class LoginView extends StatelessWidget { final LoginController controller; - const LoginView(this.controller, {Key? key}) : super(key: key); + const LoginView(this.controller, {super.key}); @override Widget build(BuildContext context) { return LoginScaffold( - enforceMobileMode: Matrix.of(context).client.isLogged(), + // #Pangea + // enforceMobileMode: Matrix.of(context).client.isLogged(), + // Pangea# appBar: AppBar( leading: controller.loading ? null : const BackButton(), automaticallyImplyLeading: !controller.loading, centerTitle: true, - title: Text( - L10n.of(context)!.logInTo( - Matrix.of(context) - .getLoginClient() - .homeserver - .toString() - .replaceFirst('https://', ''), - ), - ), + // #Pangea + // title: Text( + // L10n.of(context)!.logInTo( + // Matrix.of(context) + // .getLoginClient() + // .homeserver + // .toString() + // .replaceFirst('https://', ''), + // ), + // ), + // Pangea# ), body: Builder( builder: (context) { @@ -51,6 +54,12 @@ class LoginView extends StatelessWidget { errorText: controller.usernameError, errorStyle: const TextStyle(color: Colors.orange), hintText: L10n.of(context)!.emailOrUsername, + // #Pangea + fillColor: Theme.of(context) + .colorScheme + .background + .withOpacity(0.75), + // Pangea# ), ), ), @@ -69,16 +78,38 @@ class LoginView extends StatelessWidget { prefixIcon: const Icon(Icons.lock_outlined), errorText: controller.passwordError, errorStyle: const TextStyle(color: Colors.orange), - suffixIcon: IconButton( - onPressed: controller.toggleShowPassword, - icon: Icon( - controller.showPassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - color: Colors.black, + // #Pangea + // prevent enter key from clicking show password button + suffixIcon: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: controller.toggleShowPassword, + child: Icon( + controller.showPassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: Colors.black, + ), ), ), + // suffixIcon: IconButton( + // onPressed: controller.toggleShowPassword, + // icon: Icon( + // controller.showPassword + // ? Icons.visibility_off_outlined + // : Icons.visibility_outlined, + // color: Colors.black, + // ), + // ), + // Pangea# hintText: L10n.of(context)!.password, + // #Pangea + fillColor: Theme.of(context) + .colorScheme + .background + .withOpacity(0.75), + // Pangea# ), ), ), @@ -86,28 +117,40 @@ class LoginView extends StatelessWidget { tag: 'signinButton', child: Padding( padding: const EdgeInsets.all(12.0), - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: - Theme.of(context).colorScheme.onPrimary, - ), - onPressed: controller.loading ? null : controller.login, - icon: const Icon(Icons.login_outlined), - label: controller.loading + // #Pangea + child: ElevatedButton( + onPressed: + controller.loading ? null : () => controller.login(), + child: controller.loading ? const LinearProgressIndicator() : Text(L10n.of(context)!.login), ), + // child: ElevatedButton.icon( + // style: ElevatedButton.styleFrom( + // backgroundColor: Theme.of(context).colorScheme.primary, + // foregroundColor: + // Theme.of(context).colorScheme.onPrimary, + // ), + // onPressed: controller.loading ? null : controller.login, + // icon: const Icon(Icons.login_outlined), + // label: controller.loading + // ? const LinearProgressIndicator() + // : Text(L10n.of(context)!.login), + // ), + // Pangea$ ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( children: [ - Expanded( + const Expanded( child: Divider( thickness: 1, - color: Theme.of(context).dividerColor, + // #Pangea + color: Colors.white, + // color: Theme.of(context).dividerColor, + // Pangea# ), ), Padding( @@ -117,10 +160,13 @@ class LoginView extends StatelessWidget { style: const TextStyle(fontSize: 18), ), ), - Expanded( + const Expanded( child: Divider( thickness: 1, - color: Theme.of(context).dividerColor, + // #Pangea + color: Colors.white, + // color: Theme.of(context).dividerColor, + // Pangea# ), ), ], @@ -128,17 +174,27 @@ class LoginView extends StatelessWidget { ), Padding( padding: const EdgeInsets.all(12.0), - child: ElevatedButton.icon( + // #Pangea + child: ElevatedButton( onPressed: controller.loading ? () {} - : controller.passwordForgotten, - style: ElevatedButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, - backgroundColor: Theme.of(context).colorScheme.onError, - ), - icon: const Icon(Icons.safety_check_outlined), - label: Text(L10n.of(context)!.passwordForgotten), + : controller.pangeaPasswordForgotten, + style: + ElevatedButton.styleFrom(foregroundColor: Colors.red), + child: Text(L10n.of(context)!.passwordForgotten), ), + // child: ElevatedButton.icon( + // onPressed: controller.loading + // ? () {} + // : controller.passwordForgotten, + // style: ElevatedButton.styleFrom( + // foregroundColor: Theme.of(context).colorScheme.error, + // backgroundColor: Theme.of(context).colorScheme.onError, + // ), + // icon: const Icon(Icons.safety_check_outlined), + // label: Text(L10n.of(context)!.passwordForgotten), + // ), + // Pangea# ), ], ), diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index 70ea22eff..1a1ab6c9e 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -1,14 +1,23 @@ -import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'package:fluffychat/pages/new_group/new_group_view.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/chat_topic_model.dart'; +import 'package:fluffychat/pangea/models/lemma.dart'; +import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; -import 'package:fluffychat/pages/new_group/new_group_view.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - class NewGroup extends StatefulWidget { - const NewGroup({Key? key}) : super(key: key); + const NewGroup({super.key}); @override NewGroupController createState() => NewGroupController(); @@ -18,29 +27,89 @@ class NewGroupController extends State { TextEditingController controller = TextEditingController(); bool publicGroup = false; + //#Pangea + PangeaController pangeaController = MatrixState.pangeaController; + final GlobalKey addToSpaceKey = GlobalKey(); + + ChatTopic chatTopic = ChatTopic.empty; + + void setVocab(List vocab) => setState(() => chatTopic.vocab = vocab); + String? get activeSpaceId => + GoRouterState.of(context).pathParameters['spaceid']; + // Pangea# + void setPublicGroup(bool b) => setState(() => publicGroup = b); void submitAction([_]) async { + // #Pangea + if (controller.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.emptyChatNameWarning), + ), + ); + return; + } + // Pangea# final client = Matrix.of(context).client; final roomID = await showFutureLoadingDialog( context: context, future: () async { final roomId = await client.createGroupChat( - visibility: - publicGroup ? sdk.Visibility.public : sdk.Visibility.private, - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, + // #Pangea + // visibility: + // publicGroup ? sdk.Visibility.public : sdk.Visibility.private, + // preset: publicGroup + // ? sdk.CreateRoomPreset.publicChat + // : sdk.CreateRoomPreset.privateChat, + preset: sdk.CreateRoomPreset.publicChat, groupName: controller.text.isNotEmpty ? controller.text : null, + powerLevelContentOverride: + await ClassChatPowerLevels.powerLevelOverrideForClassChat( + context, + addToSpaceKey.currentState!.parents + .map((suggestionStatus) => suggestionStatus.room) + .toList(), + ), + // Pangea# ); return roomId; }, + // #Pangea + onError: (exception) { + ErrorHandler.logError(e: exception, s: StackTrace.current); + return exception.toString(); + }, + // Pangea# ); if (roomID.error == null) { + //#Pangea + GoogleAnalytics.createChat(roomID.result!); + await addToSpaceKey.currentState!.addSpaces(roomID.result!); + //Pangea# context.go('/rooms/${roomID.result!}/invite'); + //#Pangea + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(e: roomID.error, s: StackTrace.current); } + //Pangea# } + //#Pangea + @override + void initState() { + Future.delayed(Duration.zero, () { + chatTopic.langCode = + pangeaController.languageController.activeL2Code(roomID: null) ?? + pangeaController.pLanguageStore.targetOptions.first.langCode; + setState(() {}); + }); + + super.initState(); + } + //Pangea# + @override Widget build(BuildContext context) => NewGroupView(this); } diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart index 7b8dd7b1c..cadb47633 100644 --- a/lib/pages/new_group/new_group_view.dart +++ b/lib/pages/new_group/new_group_view.dart @@ -1,14 +1,14 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pages/new_group/new_group.dart'; +import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class NewGroupView extends StatelessWidget { final NewGroupController controller; - const NewGroupView(this.controller, {Key? key}) : super(key: key); + const NewGroupView(this.controller, {super.key}); @override Widget build(BuildContext context) { @@ -29,24 +29,38 @@ class NewGroupView extends StatelessWidget { textInputAction: TextInputAction.go, onSubmitted: controller.submitAction, decoration: InputDecoration( - labelText: L10n.of(context)!.optionalGroupName, + // #Pangea + labelText: L10n.of(context)!.enterAGroupName, + // labelText: L10n.of(context)!.optionalGroupName, prefixIcon: const Icon(Icons.people_outlined), - hintText: L10n.of(context)!.enterAGroupName, + // hintText: L10n.of(context)!.enterAGroupName, + // Pangea# ), ), ), - SwitchListTile.adaptive( - secondary: const Icon(Icons.public_outlined), - title: Text(L10n.of(context)!.groupIsPublic), - value: controller.publicGroup, - onChanged: controller.setPublicGroup, + // #Pangea + // SwitchListTile.adaptive( + // secondary: const Icon(Icons.public_outlined), + // title: Text(L10n.of(context)!.groupIsPublic), + // value: controller.publicGroup, + // onChanged: controller.setPublicGroup, + // ), + // SwitchListTile.adaptive( + // secondary: const Icon(Icons.lock_outlined), + // title: Text(L10n.of(context)!.enableEncryption), + // value: !controller.publicGroup, + // onChanged: null, + // ), + AddToSpaceToggles( + key: controller.addToSpaceKey, + startOpen: false, + activeSpaceId: controller.activeSpaceId, + mode: AddToClassMode.chat, ), - SwitchListTile.adaptive( - secondary: const Icon(Icons.lock_outlined), - title: Text(L10n.of(context)!.enableEncryption), - value: !controller.publicGroup, - onChanged: null, + const SizedBox( + height: 50, ), + // Pangea# ], ), ), diff --git a/lib/pages/new_private_chat/new_private_chat.dart b/lib/pages/new_private_chat/new_private_chat.dart index 179d4aab1..5b5fca28a 100644 --- a/lib/pages/new_private_chat/new_private_chat.dart +++ b/lib/pages/new_private_chat/new_private_chat.dart @@ -1,10 +1,4 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/new_private_chat/new_private_chat_view.dart'; import 'package:fluffychat/pages/new_private_chat/qr_scanner_modal.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; @@ -12,6 +6,10 @@ import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; class NewPrivateChat extends StatefulWidget { const NewPrivateChat({Key? key}) : super(key: key); diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index 4672633b6..529987c48 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -1,15 +1,11 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; -import 'package:qr_flutter/qr_flutter.dart'; - import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; class NewPrivateChatView extends StatelessWidget { final NewPrivateChatController controller; @@ -59,12 +55,14 @@ class NewPrivateChatView extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - QrImageView( - data: - 'https://matrix.to/#/${Matrix.of(context).client.userID}', - version: QrVersions.auto, - size: qrCodeSize, - ), + //#Pangea - commenting this out because it's not super important and is throwing an error + // QrImageView( + // data: + // 'https://matrix.to/#/${Matrix.of(context).client.userID}', + // version: QrVersions.auto, + // size: qrCodeSize, + // ), + // Pangea# TextButton.icon( style: TextButton.styleFrom( fixedSize: diff --git a/lib/pages/new_space/new_space.dart b/lib/pages/new_space/new_space.dart index 9d4a8272a..2ebf3ac5e 100644 --- a/lib/pages/new_space/new_space.dart +++ b/lib/pages/new_space/new_space.dart @@ -1,15 +1,28 @@ -import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'package:fluffychat/pages/new_space/new_space_view.dart'; +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/utils/class_chat_power_levels.dart'; +import 'package:fluffychat/pangea/utils/class_code.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/pangea/widgets/space/class_settings.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart' as sdk; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/new_space/new_space_view.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - class NewSpace extends StatefulWidget { - const NewSpace({Key? key}) : super(key: key); + const NewSpace({super.key}); @override NewSpaceController createState() => NewSpaceController(); @@ -17,30 +30,205 @@ class NewSpace extends StatefulWidget { class NewSpaceController extends State { TextEditingController controller = TextEditingController(); - bool publicGroup = false; + // #Pangea + // bool publicGroup = false; + bool publicGroup = true; + final GlobalKey rulesEditorKey = GlobalKey(); + final GlobalKey addToSpaceKey = GlobalKey(); + final GlobalKey classSettingsKey = + GlobalKey(); + //Pangea# void setPublicGroup(bool b) => setState(() => publicGroup = b); - void submitAction([_]) async { - final matrix = Matrix.of(context); - final roomID = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.createRoom( - preset: publicGroup - ? sdk.CreateRoomPreset.publicChat - : sdk.CreateRoomPreset.privateChat, - creationContent: {'type': RoomCreationTypes.mSpace}, - visibility: publicGroup ? sdk.Visibility.public : null, - roomAliasName: publicGroup && controller.text.isNotEmpty - ? controller.text.trim().toLowerCase().replaceAll(' ', '_') - : null, - name: controller.text.isNotEmpty ? controller.text : null, + // #Pangea + bool newClassMode = true; + + //in initState, set newClassMode to true if parameter "newClass" is true + //use Vrouter + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + newClassMode = + GoRouterState.of(context).pathParameters['newexchange'] != 'exchange'; + setState(() {}); + }); + } + + List get initialState { + final events = []; + + events.add( + StateEvent( + type: EventTypes.RoomPowerLevels, + stateKey: '', + content: { + 'events': { + PangeaEventTypes.studentAnalyticsSummary: 0, + EventTypes.spaceChild: 0, + }, + 'users_default': 0, + 'users': { + Matrix.of(context).client.userID: + ClassDefaultValues.powerLevelOfAdmin, + }, + }, ), ); - if (roomID.error == null) { - context.go('/rooms/${roomID.result!}'); + + if (rulesEditorKey.currentState?.rules != null) { + events.add(rulesEditorKey.currentState!.rules.toStateEvent); + } else { + debugger(when: kDebugMode); } + if (classSettingsKey.currentState != null) { + events.add(classSettingsKey.currentState!.classSettings.toStateEvent); + } else { + debugger(when: kDebugMode && newClassMode); + } + + return events; } + //Pangea# + + void submitAction([_]) async { + final matrix = Matrix.of(context); + // #Pangea + if (rulesEditorKey.currentState == null) { + debugger(when: kDebugMode); + return; + } + if (classSettingsKey.currentState != null && + classSettingsKey.currentState!.sameLanguages) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.noIdenticalLanguages), + ), + ); + return; + } + if (controller.text.isEmpty) { + final String warning = newClassMode + ? L10n.of(context)!.emptyClassNameWarning + : L10n.of(context)!.emptyExchangeNameWarning; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(warning)), + ); + return; + } + if (newClassMode) { + final int? languageLevel = + classSettingsKey.currentState!.classSettings.languageLevel; + if (languageLevel == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context)!.languageLevelWarning)), + ); + return; + } + } + //Pangea# + final roomID = await showFutureLoadingDialog( + context: context, + // #Pangea + // future: () async => matrix.client.createRoom( + future: () async { + final roomId = await matrix.client.createRoom( + initialState: initialState, + //Pangea# + preset: publicGroup + ? sdk.CreateRoomPreset.publicChat + : sdk.CreateRoomPreset.privateChat, + creationContent: {'type': RoomCreationTypes.mSpace}, + visibility: publicGroup ? sdk.Visibility.public : null, + // #Pangea + // roomAliasName: publicGroup && controller.text.isNotEmpty + // ? controller.text.trim().toLowerCase().replaceAll(' ', '_') + // : null, + powerLevelContentOverride: addToSpaceKey.currentState != null + ? await ClassChatPowerLevels.powerLevelOverrideForClassChat( + context, + addToSpaceKey.currentState!.parents + .map((suggestionStatus) => suggestionStatus.room) + .toList(), + ) + : null, + roomAliasName: ClassCodeUtil.generateClassCode(), + // Pangea# + name: controller.text.isNotEmpty ? controller.text : null, + ); + // #Pangea + Room? room = Matrix.of(context).client.getRoomById(roomId); + + final List> futures = [ + Matrix.of(context).client.waitForRoomInSync(roomId, join: true), + ]; + if (addToSpaceKey.currentState != null) { + futures.add(addToSpaceKey.currentState!.addSpaces(roomId)); + } + await Future.wait(futures); + + room = Matrix.of(context).client.getRoomById(roomId); + + final newChatRoomId = await Matrix.of(context).client.createGroupChat( + enableEncryption: false, + preset: sdk.CreateRoomPreset.publicChat, + groupName: + '${controller.text}: ${L10n.of(context)!.classWelcomeChat}', + ); + GoogleAnalytics.createChat(newChatRoomId); + + room!.setSpaceChild(newChatRoomId, suggested: true); + newClassMode + ? GoogleAnalytics.addParent( + newChatRoomId, + room.classCode, + ) + : GoogleAnalytics.addChatToExchange( + newChatRoomId, + room.classCode, + ); + + GoogleAnalytics.createClass(room.name, room.classCode); + try { + await room.invite(BotName.byEnvironment); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${room.id}", + ); + } + + return roomId; + }, + title: L10n.of(context)!.creatingSpacePleaseWait, + onError: (exception) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: exception); + return exception.toString(); + }, + // Pangea# + ); + + // #Pangea + if (roomID.result == null) return; + MatrixState.pangeaController.classController + .setActiveSpaceIdInChatListController(roomID.result!); + + if (roomID.error == null) { + context.go('/spaces/${roomID.result!}'); + } + // if (roomID.error == null) { + // context.go('/rooms/${roomID.result!}'); + // } + // Pangea# + } + + // #Pangea + //toggle newClassMode + void toggleClassMode(bool newValue) { + setState(() => newClassMode = newValue); + } + // Pangea# @override Widget build(BuildContext context) => NewSpaceView(this); diff --git a/lib/pages/new_space/new_space_view.dart b/lib/pages/new_space/new_space_view.dart index ce4f967c9..e16a6d76a 100644 --- a/lib/pages/new_space/new_space_view.dart +++ b/lib/pages/new_space/new_space_view.dart @@ -1,20 +1,55 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart'; +import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/pangea/widgets/space/class_settings.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:flutter/material.dart'; - +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'new_space.dart'; class NewSpaceView extends StatelessWidget { final NewSpaceController controller; - const NewSpaceView(this.controller, {Key? key}) : super(key: key); + const NewSpaceView(this.controller, {super.key}); @override Widget build(BuildContext context) { + // #Pangea + final activeColor = Theme.of(context).brightness == Brightness.dark + ? AppConfig.primaryColorLight + : AppConfig.primaryColor; + // Pangea# return Scaffold( appBar: AppBar( - title: Text(L10n.of(context)!.createNewSpace), + // #Pangea + centerTitle: true, + title: Text( + controller.newClassMode + ? L10n.of(context)!.createNewClass + : L10n.of(context)!.newExchange, + ), + actions: [ + IconButton( + icon: const Icon(Icons.class_outlined), + selectedIcon: const Icon(Icons.class_), + color: controller.newClassMode ? activeColor : null, + isSelected: controller.newClassMode, + onPressed: () => controller.toggleClassMode(true), + ), + IconButton( + icon: const Icon(Icons.connecting_airports), + selectedIcon: const Icon(Icons.connecting_airports), + color: !controller.newClassMode ? activeColor : null, + isSelected: !controller.newClassMode, + onPressed: () => controller.toggleClassMode(false), + ), + ], + // title: Text(L10n.of(context)!.createNewSpace), + // Pangea# ), body: MaxWidthBody( child: Column( @@ -23,6 +58,10 @@ class NewSpaceView extends StatelessWidget { Padding( padding: const EdgeInsets.all(12.0), child: TextField( + // #Pangea + maxLength: ClassDefaultValues.maxClassName, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + // Pangea# controller: controller.controller, autofocus: true, autocorrect: false, @@ -35,18 +74,40 @@ class NewSpaceView extends StatelessWidget { ), ), ), - 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), + // #Pangea + if (controller.newClassMode) + ClassSettings( + key: controller.classSettingsKey, + roomId: null, + startOpen: true, ), - subtitle: Text(L10n.of(context)!.newSpaceDescription), + if (!controller.newClassMode) + AddToSpaceToggles( + key: controller.addToSpaceKey, + startOpen: false, + mode: !controller.newClassMode + ? AddToClassMode.exchange + : AddToClassMode.chat, + ), + RoomRulesEditor( + key: controller.rulesEditorKey, + roomId: null, + startOpen: false, ), + 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# ], ), ), diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index b4b9c2f1f..a23dd0655 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,23 +1,22 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/pangea/utils/logout.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/app_lock.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:image_picker/image_picker.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import '../../widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; import 'settings_view.dart'; class Settings extends StatefulWidget { - const Settings({Key? key}) : super(key: key); + const Settings({super.key}); @override SettingsController createState() => SettingsController(); @@ -61,23 +60,26 @@ class SettingsController extends State { void logoutAction() async { final noBackup = showChatBackupBanner == true; - if (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSureYouWantToLogout, - message: L10n.of(context)!.noBackupWarning, - isDestructiveAction: noBackup, - okLabel: L10n.of(context)!.logout, - cancelLabel: L10n.of(context)!.cancel, - ) == - OkCancelResult.cancel) { - return; - } - final matrix = Matrix.of(context); - await showFutureLoadingDialog( - context: context, - future: () => matrix.client.logout(), - ); + // #Pangea + pLogoutAction(context, isDestructiveAction: noBackup); + // if (await showOkCancelAlertDialog( + // useRootNavigator: false, + // context: context, + // title: L10n.of(context)!.areYouSureYouWantToLogout, + // message: L10n.of(context)!.noBackupWarning, + // isDestructiveAction: noBackup, + // okLabel: L10n.of(context)!.logout, + // cancelLabel: L10n.of(context)!.cancel, + // ) == + // OkCancelResult.cancel) { + // return; + // } + // final matrix = Matrix.of(context); + // await showFutureLoadingDialog( + // context: context, + // future: () => matrix.client.logout(), + // ); + // Pangea# } void setAvatarAction() async { @@ -160,7 +162,9 @@ class SettingsController extends State { @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) => checkBootstrap()); + // #Pangea + // WidgetsBinding.instance.addPostFrameCallback((_) => checkBootstrap()); + // Pangea# super.initState(); } @@ -189,19 +193,21 @@ class SettingsController extends State { bool? showChatBackupBanner; void firstRunBootstrapAction([_]) async { - if (showChatBackupBanner != true) { - showOkAlertDialog( - context: context, - title: L10n.of(context)!.chatBackup, - message: L10n.of(context)!.onlineKeyBackupEnabled, - okLabel: L10n.of(context)!.close, - ); - return; - } - await BootstrapDialog( - client: Matrix.of(context).client, - ).show(context); - checkBootstrap(); + // #Pangea + // if (showChatBackupBanner != true) { + // showOkAlertDialog( + // context: context, + // title: L10n.of(context)!.chatBackup, + // message: L10n.of(context)!.onlineKeyBackupEnabled, + // okLabel: L10n.of(context)!.close, + // ); + // return; + // } + // await BootstrapDialog( + // client: Matrix.of(context).client, + // ).show(context); + // checkBootstrap(); + // Pangea# } @override diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index c7277e023..e7714c863 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -1,25 +1,26 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'settings.dart'; class SettingsView extends StatelessWidget { final SettingsController controller; - const SettingsView(this.controller, {Key? key}) : super(key: key); + const SettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { - final showChatBackupBanner = controller.showChatBackupBanner; + // #Pangea + // final showChatBackupBanner = controller.showChatBackupBanner; + // Pangea# return Scaffold( appBar: AppBar( leading: Center( @@ -137,21 +138,23 @@ class SettingsView extends StatelessWidget { }, ), const Divider(thickness: 1), - if (showChatBackupBanner == null) - ListTile( - leading: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.chatBackup), - trailing: const CircularProgressIndicator.adaptive(), - ) - else - SwitchListTile.adaptive( - controlAffinity: ListTileControlAffinity.trailing, - value: controller.showChatBackupBanner == false, - secondary: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.chatBackup), - onChanged: controller.firstRunBootstrapAction, - ), - const Divider(thickness: 1), + // #Pangea + // if (showChatBackupBanner == null) + // ListTile( + // leading: const Icon(Icons.backup_outlined), + // title: Text(L10n.of(context)!.chatBackup), + // trailing: const CircularProgressIndicator.adaptive(), + // ) + // else + // SwitchListTile.adaptive( + // controlAffinity: ListTileControlAffinity.trailing, + // value: controller.showChatBackupBanner == false, + // secondary: const Icon(Icons.backup_outlined), + // title: Text(L10n.of(context)!.chatBackup), + // onChanged: controller.firstRunBootstrapAction, + // ), + // const Divider(thickness: 1), + // Pangea# ListTile( leading: const Icon(Icons.format_paint_outlined), title: Text(L10n.of(context)!.changeTheme), @@ -176,6 +179,24 @@ class SettingsView extends StatelessWidget { onTap: () => context.go('/rooms/settings/chat'), 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), + onTap: () => context.go('/rooms/settings/subscription'), + trailing: const Icon( + Icons.chevron_right_outlined, + ), + ), + // Pangea# ListTile( leading: const Icon(Icons.shield_outlined), title: Text(L10n.of(context)!.security), @@ -195,12 +216,25 @@ class SettingsView extends StatelessWidget { onTap: () => launchUrlString(AppConfig.privacyUrl), trailing: const Icon(Icons.open_in_new_outlined), ), + // #Pangea + // ListTile( + // leading: const Icon(Icons.info_outline_rounded), + // title: Text(L10n.of(context)!.about), + // onTap: () => PlatformInfos.showDialog(context), + // trailing: const Icon(Icons.chevron_right_outlined), + // ), ListTile( - leading: const Icon(Icons.info_outline_rounded), - title: Text(L10n.of(context)!.about), - onTap: () => PlatformInfos.showDialog(context), - trailing: const Icon(Icons.chevron_right_outlined), + leading: const Icon(Icons.shield_outlined), + title: Text(L10n.of(context)!.termsAndConditions), + onTap: () => launchUrlString(AppConfig.termsOfServiceUrl), + trailing: const Icon(Icons.open_in_new_outlined), ), + if (Environment.isStaging) + ListTile( + leading: const Icon(Icons.bug_report_outlined), + title: Text(L10n.of(context)!.connectedToStaging), + ), + // Pangea# ], ), ), diff --git a/lib/pages/settings_3pid/settings_3pid_view.dart b/lib/pages/settings_3pid/settings_3pid_view.dart index 245077d9c..2a88f7711 100644 --- a/lib/pages/settings_3pid/settings_3pid_view.dart +++ b/lib/pages/settings_3pid/settings_3pid_view.dart @@ -1,12 +1,9 @@ +import 'package:fluffychat/pages/settings_3pid/settings_3pid.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'; -import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - class Settings3PidView extends StatelessWidget { final Settings3PidController controller; @@ -27,73 +24,76 @@ class Settings3PidView extends StatelessWidget { ), ], ), - body: MaxWidthBody( - child: FutureBuilder?>( - future: controller.request, - builder: ( - BuildContext context, - AsyncSnapshot?> snapshot, - ) { - if (snapshot.hasError) { - return Center( - child: Text( - snapshot.error.toString(), - textAlign: TextAlign.center, - ), - ); - } - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ); - } - final identifier = snapshot.data!; - return Column( - children: [ - ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - foregroundColor: - identifier.isEmpty ? Colors.orange : Colors.grey, - child: Icon( - identifier.isEmpty - ? Icons.warning_outlined - : Icons.info_outlined, - ), - ), - title: Text( - identifier.isEmpty - ? L10n.of(context)!.noPasswordRecoveryDescription - : L10n.of(context)! - .withTheseAddressesRecoveryDescription, - ), - ), - const Divider(height: 1), - Expanded( - child: ListView.builder( - itemCount: identifier.length, - itemBuilder: (BuildContext context, int i) => ListTile( - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: Colors.grey, - child: Icon(identifier[i].iconData), - ), - title: Text(identifier[i].address), - trailing: IconButton( - tooltip: L10n.of(context)!.delete, - icon: const Icon(Icons.delete_forever_outlined), - color: Colors.red, - onPressed: () => controller.delete3Pid(identifier[i]), - ), - ), - ), - ), - ], + body: + // #Pangea + // MaxWidthBody( + // child: + // Pangea# + FutureBuilder?>( + future: controller.request, + builder: ( + BuildContext context, + AsyncSnapshot?> snapshot, + ) { + if (snapshot.hasError) { + return Center( + child: Text( + snapshot.error.toString(), + textAlign: TextAlign.center, + ), ); - }, - ), + } + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ); + } + final identifier = snapshot.data!; + return Column( + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: + identifier.isEmpty ? Colors.orange : Colors.grey, + child: Icon( + identifier.isEmpty + ? Icons.warning_outlined + : Icons.info_outlined, + ), + ), + title: Text( + identifier.isEmpty + ? L10n.of(context)!.noPasswordRecoveryDescription + : L10n.of(context)!.withTheseAddressesRecoveryDescription, + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: identifier.length, + itemBuilder: (BuildContext context, int i) => ListTile( + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(identifier[i].iconData), + ), + title: Text(identifier[i].address), + trailing: IconButton( + tooltip: L10n.of(context)!.delete, + icon: const Icon(Icons.delete_forever_outlined), + color: Colors.red, + onPressed: () => controller.delete3Pid(identifier[i]), + ), + ), + ), + ), + ], + ); + }, ), + // ), ); } } diff --git a/lib/pages/settings_chat/settings_chat_view.dart b/lib/pages/settings_chat/settings_chat_view.dart index 76522bffa..ca3477980 100644 --- a/lib/pages/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_chat/settings_chat_view.dart @@ -1,15 +1,12 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/voip/callkeep_manager.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/settings_switch_list_tile.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import 'settings_chat.dart'; class SettingsChatView extends StatelessWidget { @@ -25,19 +22,21 @@ class SettingsChatView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ - ListTile( - title: Text(L10n.of(context)!.emoteSettings), - onTap: () => context.go('/rooms/settings/chat/emotes'), - trailing: const Icon(Icons.chevron_right_outlined), - leading: const Icon(Icons.emoji_emotions_outlined), - ), - const Divider(), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.renderRichContent, - onChanged: (b) => AppConfig.renderHtml = b, - storeKey: SettingKeys.renderHtml, - defaultValue: AppConfig.renderHtml, - ), + // #Pangea + // ListTile( + // title: Text(L10n.of(context)!.emoteSettings), + // onTap: () => context.go('/rooms/settings/chat/emotes'), + // trailing: const Icon(Icons.chevron_right_outlined), + // leading: const Icon(Icons.emoji_emotions_outlined), + // ), + // const Divider(), + // SettingsSwitchListTile.adaptive( + // title: L10n.of(context)!.renderRichContent, + // onChanged: (b) => AppConfig.renderHtml = b, + // storeKey: SettingKeys.renderHtml, + // defaultValue: AppConfig.renderHtml, + // ), + // Pangea# SettingsSwitchListTile.adaptive( title: L10n.of(context)!.hideRedactedEvents, onChanged: (b) => AppConfig.hideRedactedEvents = b, @@ -50,12 +49,14 @@ class SettingsChatView extends StatelessWidget { storeKey: SettingKeys.hideUnknownEvents, defaultValue: AppConfig.hideUnknownEvents, ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.hideUnimportantStateEvents, - onChanged: (b) => AppConfig.hideUnimportantStateEvents = b, - storeKey: SettingKeys.hideUnimportantStateEvents, - defaultValue: AppConfig.hideUnimportantStateEvents, - ), + // #Pangea + // SettingsSwitchListTile.adaptive( + // title: L10n.of(context)!.hideUnimportantStateEvents, + // onChanged: (b) => AppConfig.hideUnimportantStateEvents = b, + // storeKey: SettingKeys.hideUnimportantStateEvents, + // defaultValue: AppConfig.hideUnimportantStateEvents, + // ), + // Pangea# if (PlatformInfos.isMobile) SettingsSwitchListTile.adaptive( title: L10n.of(context)!.autoplayImages, @@ -64,28 +65,30 @@ class SettingsChatView extends StatelessWidget { defaultValue: AppConfig.autoplayImages, ), const Divider(), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.sendTypingNotifications, - onChanged: (b) => AppConfig.sendTypingNotifications = b, - storeKey: SettingKeys.sendTypingNotifications, - defaultValue: AppConfig.sendTypingNotifications, - ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.sendOnEnter, - onChanged: (b) => AppConfig.sendOnEnter = b, - storeKey: SettingKeys.sendOnEnter, - defaultValue: AppConfig.sendOnEnter, - ), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.experimentalVideoCalls, - onChanged: (b) { - AppConfig.experimentalVoip = b; - Matrix.of(context).createVoipPlugin(); - return; - }, - storeKey: SettingKeys.experimentalVoip, - defaultValue: AppConfig.experimentalVoip, - ), + // #Pangea + // SettingsSwitchListTile.adaptive( + // title: L10n.of(context)!.sendTypingNotifications, + // onChanged: (b) => AppConfig.sendTypingNotifications = b, + // storeKey: SettingKeys.sendTypingNotifications, + // defaultValue: AppConfig.sendTypingNotifications, + // ), + // SettingsSwitchListTile.adaptive( + // title: L10n.of(context)!.sendOnEnter, + // onChanged: (b) => AppConfig.sendOnEnter = b, + // storeKey: SettingKeys.sendOnEnter, + // defaultValue: AppConfig.sendOnEnter, + // ), + // SettingsSwitchListTile.adaptive( + // title: L10n.of(context)!.experimentalVideoCalls, + // onChanged: (b) { + // AppConfig.experimentalVoip = b; + // Matrix.of(context).createVoipPlugin(); + // return; + // }, + // storeKey: SettingKeys.experimentalVoip, + // defaultValue: AppConfig.experimentalVoip, + // ), + // Pangea# if (PlatformInfos.isMobile) ListTile( title: Text(L10n.of(context)!.callingPermissions), @@ -96,6 +99,15 @@ class SettingsChatView extends StatelessWidget { child: Icon(Icons.call), ), ), + const Divider(height: 1), + // #Pangea + SettingsSwitchListTile.adaptive( + title: L10n.of(context)!.showDirectChatsInSpaces, + onChanged: (b) => AppConfig.showDirectChatsInSpaces = b, + storeKey: SettingKeys.showDirectChatsInSpaces, + defaultValue: AppConfig.showDirectChatsInSpaces, + ), + // Pangea# SettingsSwitchListTile.adaptive( title: L10n.of(context)!.separateChatTypes, onChanged: (b) => AppConfig.separateChatTypes = b, diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_emotes/settings_emotes.dart index 98d5a214d..3498329a7 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_emotes/settings_emotes.dart @@ -1,29 +1,27 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:archive/archive.dart' + if (dart.library.io) 'package:archive/archive_io.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' hide Client; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/client_manager.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; import '../../widgets/matrix.dart'; import 'import_archive_dialog.dart'; import 'settings_emotes_view.dart'; -import 'package:archive/archive.dart' - if (dart.library.io) 'package:archive/archive_io.dart'; - class EmotesSettings extends StatefulWidget { - const EmotesSettings({Key? key}) : super(key: key); + const EmotesSettings({super.key}); @override EmotesSettingsController createState() => EmotesSettingsController(); diff --git a/lib/pages/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_emotes/settings_emotes_view.dart index 5803ee94a..8557d453b 100644 --- a/lib/pages/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_emotes/settings_emotes_view.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + import '../../widgets/matrix.dart'; import 'settings_emotes.dart'; @@ -15,7 +14,7 @@ enum PopupMenuEmojiActions { import, export } class EmotesSettingsView extends StatelessWidget { final EmotesSettingsController controller; - const EmotesSettingsView(this.controller, {Key? key}) : super(key: key); + const EmotesSettingsView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart b/lib/pages/settings_ignore_list/settings_ignore_list_view.dart index 438e530b3..092db16ab 100644 --- a/lib/pages/settings_ignore_list/settings_ignore_list_view.dart +++ b/lib/pages/settings_ignore_list/settings_ignore_list_view.dart @@ -1,11 +1,9 @@ +import 'package:fluffychat/widgets/avatar.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/widgets/avatar.dart'; -import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import '../../widgets/matrix.dart'; import 'settings_ignore_list.dart'; @@ -22,76 +20,79 @@ class SettingsIgnoreListView extends StatelessWidget { leading: const Center(child: BackButton()), title: Text(L10n.of(context)!.ignoredUsers), ), - body: MaxWidthBody( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: controller.controller, - autocorrect: false, - textInputAction: TextInputAction.done, - onSubmitted: (_) => controller.ignoreUser(context), - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: 'bad_guy:domain.abc', - prefixText: '@', - labelText: L10n.of(context)!.ignoreUsername, - suffixIcon: IconButton( - tooltip: L10n.of(context)!.ignore, - icon: const Icon(Icons.done_outlined), - onPressed: () => controller.ignoreUser(context), + body: + // #Pangea + // MaxWidthBody( + // child: + // Pangea# + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller.controller, + autocorrect: false, + textInputAction: TextInputAction.done, + onSubmitted: (_) => controller.ignoreUser(context), + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: 'bad_guy:domain.abc', + prefixText: '@', + labelText: L10n.of(context)!.ignoreUsername, + suffixIcon: IconButton( + tooltip: L10n.of(context)!.ignore, + icon: const Icon(Icons.done_outlined), + onPressed: () => controller.ignoreUser(context), + ), + ), + ), + const SizedBox(height: 16), + Text( + L10n.of(context)!.ignoreListDescription, + style: const TextStyle(color: Colors.orange), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: StreamBuilder( + stream: client.onAccountData.stream + .where((a) => a.type == 'm.ignored_user_list'), + builder: (context, snapshot) { + return ListView.builder( + itemCount: client.ignoredUsers.length, + itemBuilder: (c, i) => FutureBuilder( + future: client.getProfileFromUserId(client.ignoredUsers[i]), + builder: (c, s) => ListTile( + leading: Avatar( + mxContent: s.data?.avatarUrl ?? Uri.parse(''), + name: s.data?.displayName ?? client.ignoredUsers[i], + ), + title: Text( + s.data?.displayName ?? client.ignoredUsers[i], + ), + trailing: IconButton( + tooltip: L10n.of(context)!.delete, + icon: const Icon(Icons.delete_forever_outlined), + onPressed: () => showFutureLoadingDialog( + context: context, + future: () => + client.unignoreUser(client.ignoredUsers[i]), + ), ), ), ), - const SizedBox(height: 16), - Text( - L10n.of(context)!.ignoreListDescription, - style: const TextStyle(color: Colors.orange), - ), - ], - ), + ); + }, ), - const Divider(height: 1), - Expanded( - child: StreamBuilder( - stream: client.onAccountData.stream - .where((a) => a.type == 'm.ignored_user_list'), - builder: (context, snapshot) { - return ListView.builder( - itemCount: client.ignoredUsers.length, - itemBuilder: (c, i) => FutureBuilder( - future: - client.getProfileFromUserId(client.ignoredUsers[i]), - builder: (c, s) => ListTile( - leading: Avatar( - mxContent: s.data?.avatarUrl ?? Uri.parse(''), - name: s.data?.displayName ?? client.ignoredUsers[i], - ), - title: Text( - s.data?.displayName ?? client.ignoredUsers[i], - ), - trailing: IconButton( - tooltip: L10n.of(context)!.delete, - icon: const Icon(Icons.delete_forever_outlined), - onPressed: () => showFutureLoadingDialog( - context: context, - future: () => - client.unignoreUser(client.ignoredUsers[i]), - ), - ), - ), - ), - ); - }, - ), - ), - ], - ), + ), + ], ), + // ), ); } } diff --git a/lib/pages/settings_multiple_emotes/settings_multiple_emotes.dart b/lib/pages/settings_multiple_emotes/settings_multiple_emotes.dart index b4901832d..34306c41a 100644 --- a/lib/pages/settings_multiple_emotes/settings_multiple_emotes.dart +++ b/lib/pages/settings_multiple_emotes/settings_multiple_emotes.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:go_router/go_router.dart'; import 'settings_multiple_emotes_view.dart'; diff --git a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart b/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart index b699e37bf..80f81fea4 100644 --- a/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart +++ b/lib/pages/settings_multiple_emotes/settings_multiple_emotes_view.dart @@ -1,12 +1,10 @@ +import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emotes.dart'; +import 'package:fluffychat/widgets/matrix.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/pages/settings_multiple_emotes/settings_multiple_emotes.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - class MultipleEmotesSettingsView extends StatelessWidget { final MultipleEmotesSettingsController controller; diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index ff9dcbd35..453634344 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; +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'; diff --git a/lib/pages/settings_notifications/settings_notifications_view.dart b/lib/pages/settings_notifications/settings_notifications_view.dart index 0cdef98dd..91fb9a318 100644 --- a/lib/pages/settings_notifications/settings_notifications_view.dart +++ b/lib/pages/settings_notifications/settings_notifications_view.dart @@ -1,10 +1,9 @@ +import 'package:fluffychat/widgets/layouts/max_width_body.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/widgets/layouts/max_width_body.dart'; import '../../utils/localized_exception_extension.dart'; import '../../widgets/matrix.dart'; import 'settings_notifications.dart'; diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index b3a58ca68..f8b2c9c13 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -1,17 +1,16 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/app_lock.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:intl/intl.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../bootstrap/bootstrap_dialog.dart'; import 'settings_security_view.dart'; diff --git a/lib/pages/settings_security/settings_security_view.dart b/lib/pages/settings_security/settings_security_view.dart index fc241a850..da8b683f1 100644 --- a/lib/pages/settings_security/settings_security_view.dart +++ b/lib/pages/settings_security/settings_security_view.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:go_router/go_router.dart'; - import 'package:fluffychat/utils/beautify_string_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + import 'settings_security.dart'; class SettingsSecurityView extends StatelessWidget { @@ -22,12 +21,14 @@ class SettingsSecurityView extends StatelessWidget { child: MaxWidthBody( child: Column( children: [ - ListTile( - leading: const Icon(Icons.camera_outlined), - trailing: const Icon(Icons.chevron_right_outlined), - title: Text(L10n.of(context)!.whoCanSeeMyStories), - onTap: () => context.go('/rooms/settings/security/stories'), - ), + // #Pangea + // ListTile( + // leading: const Icon(Icons.camera_outlined), + // trailing: const Icon(Icons.chevron_right_outlined), + // title: Text(L10n.of(context)!.whoCanSeeMyStories), + // onTap: () => context.go('/rooms/settings/security/stories'), + // ), + // Pangea# ListTile( leading: const Icon(Icons.block_outlined), trailing: const Icon(Icons.chevron_right_outlined), diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index f7646fdc2..c4864d8fb 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import '../../config/app_config.dart'; import '../../widgets/matrix.dart'; import 'settings_style.dart'; diff --git a/lib/pages/story/story_page.dart b/lib/pages/story/story_page.dart index 1fafdf0fa..5a3d314b4 100644 --- a/lib/pages/story/story_page.dart +++ b/lib/pages/story/story_page.dart @@ -1,17 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.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:path_provider/path_provider.dart'; -import 'package:video_player/video_player.dart'; - import 'package:fluffychat/pages/story/story_view.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; @@ -22,6 +13,13 @@ import 'package:fluffychat/utils/room_status_extension.dart'; import 'package:fluffychat/utils/story_theme_data.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:video_player/video_player.dart'; class StoryPage extends StatefulWidget { const StoryPage({Key? key}) : super(key: key); diff --git a/lib/pages/story/story_view.dart b/lib/pages/story/story_view.dart index 03e25502b..ed8325604 100644 --- a/lib/pages/story/story_view.dart +++ b/lib/pages/story/story_view.dart @@ -1,12 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:flutter_blurhash/flutter_blurhash.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:matrix/matrix.dart'; -import 'package:video_player/video_player.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/story/story_page.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; @@ -15,6 +6,14 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; +import 'package:video_player/video_player.dart'; + import '../../config/themes.dart'; class StoryView extends StatelessWidget { diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index 491f86ef9..e83e4e25c 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/widgets/permission_slider_dialog.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/widgets/permission_slider_dialog.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet_view.dart'; @@ -34,7 +33,7 @@ class LoadProfileBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( - future: Matrix.of(context) + future: Matrix.of(outerContext) .client .getUserProfile(userId) .timeout(const Duration(seconds: 3)), @@ -74,14 +73,13 @@ class UserBottomSheet extends StatefulWidget { final Object? profileSearchError; const UserBottomSheet({ - Key? key, + super.key, this.user, this.profile, required this.outerContext, this.onMention, this.profileSearchError, - }) : assert(user != null || profile != null), - super(key: key); + }) : assert(user != null || profile != null); @override UserBottomSheetController createState() => UserBottomSheetController(); @@ -92,15 +90,7 @@ class UserBottomSheetController extends State { final user = widget.user; final userId = user?.id ?? widget.profile?.userId; if (userId == null) throw ('user or profile must not be null!'); - // ignore: prefer_function_declarations_over_variables - final Function askConfirmation = () async => (await showOkCancelAlertDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, - cancelLabel: L10n.of(context)!.no, - ) == - OkCancelResult.ok); + switch (action) { case UserBottomSheetAction.report: if (user == null) throw ('User must not be null for this action!'); @@ -138,7 +128,7 @@ class UserBottomSheetController extends State { if (reason == null || reason.single.isEmpty) return; final result = await showFutureLoadingDialog( context: context, - future: () => Matrix.of(context).client.reportContent( + future: () => Matrix.of(widget.outerContext).client.reportContent( user.roomId!, user.eventId, reason: reason.single, @@ -157,7 +147,15 @@ class UserBottomSheetController extends State { break; case UserBottomSheetAction.ban: if (user == null) throw ('User must not be null for this action!'); - if (await askConfirmation()) { + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.banUserDescription, + ) == + OkCancelResult.ok) { await showFutureLoadingDialog( context: context, future: () => user.ban(), @@ -167,7 +165,15 @@ class UserBottomSheetController extends State { break; case UserBottomSheetAction.unban: if (user == null) throw ('User must not be null for this action!'); - if (await askConfirmation()) { + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.unbanUserDescription, + ) == + OkCancelResult.ok) { await showFutureLoadingDialog( context: context, future: () => user.unban(), @@ -177,7 +183,15 @@ class UserBottomSheetController extends State { break; case UserBottomSheetAction.kick: if (user == null) throw ('User must not be null for this action!'); - if (await askConfirmation()) { + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.kickUserDescription, + ) == + OkCancelResult.ok) { await showFutureLoadingDialog( context: context, future: () => user.kick(), @@ -192,7 +206,16 @@ class UserBottomSheetController extends State { currentLevel: user.powerLevel, ); if (newPermission != null) { - if (newPermission == 100 && await askConfirmation() == false) break; + if (newPermission == 100 && + await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.makeAdminDescription, + ) == + OkCancelResult.ok) break; await showFutureLoadingDialog( context: context, future: () => user.setPower(newPermission), @@ -203,7 +226,7 @@ class UserBottomSheetController extends State { case UserBottomSheetAction.message: final roomIdResult = await showFutureLoadingDialog( context: context, - future: () => Matrix.of(context) + future: () => Matrix.of(widget.outerContext) .client .startDirectChat(user?.id ?? widget.profile!.userId), ); @@ -212,14 +235,7 @@ class UserBottomSheetController extends State { Navigator.of(context, rootNavigator: false).pop(); break; case UserBottomSheetAction.ignore: - if (await askConfirmation()) { - await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(context) - .client - .ignoreUser(user?.id ?? widget.profile!.userId), - ); - } + context.go('/rooms/settings/security/ignorelist'); } } diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 1926193d7..6d0c330e3 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -1,17 +1,16 @@ +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/widgets/avatar.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet.dart'; class UserBottomSheetView extends StatelessWidget { final UserBottomSheetController controller; - const UserBottomSheetView(this.controller, {Key? key}) : super(key: key); + const UserBottomSheetView(this.controller, {super.key}); @override Widget build(BuildContext context) { diff --git a/lib/pangea/choreographer/controllers/alternative_translator.dart b/lib/pangea/choreographer/controllers/alternative_translator.dart new file mode 100644 index 000000000..5771d7f90 --- /dev/null +++ b/lib/pangea/choreographer/controllers/alternative_translator.dart @@ -0,0 +1,197 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; +import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:http/http.dart' as http; + +import '../../repo/similarity_repo.dart'; + +class AlternativeTranslator { + final Choreographer choreographer; + bool showAlternativeTranslations = false; + bool loadingAlternativeTranslations = false; + bool showTranslationFeedback = false; + String? userTranslation; + FeedbackKey? translationFeedbackKey; + List translations = []; + SimilartyResponseModel? similarityResponse; + + AlternativeTranslator(this.choreographer); + + void clear() { + userTranslation = null; + showAlternativeTranslations = false; + loadingAlternativeTranslations = false; + showTranslationFeedback = false; + translationFeedbackKey = null; + translations = []; + similarityResponse = null; + } + + // void onSeeAlternativeTranslationsTap() { + // if (choreographer.itController.sourceText == null) { + // ErrorHandler.logError( + // m: "sourceText null in onSeeAlternativeTranslationsTap", + // s: StackTrace.current, + // ); + // choreographer.itController.closeIT(); + // return; + // } + // showAlternativeTranslations = true; + // loadingAlternativeTranslations = true; + // translate(choreographer.itController.sourceText!); + // choreographer.setState(); + // } + + // Future translate(String text) async { + // throw Exception('disabled translaations'); + // try { + // final FullTextTranslationResponseModel results = + // await FullTextTranslationRepo.translate( + // accessToken: await choreographer.accessToken, + // request: FullTextTranslationRequestModel( + // text: text, + // tgtLang: choreographer.l2LangCode!, + // userL2: choreographer.l2LangCode!, + // userL1: choreographer.l1LangCode!, + // ), + // ); + // // translations = results.translations; + // } catch (err, stack) { + // showAlternativeTranslations = false; + // debugger(when: kDebugMode); + // ErrorHandler.logError(e: err, s: stack); + // } finally { + // loadingAlternativeTranslations = false; + // choreographer.setState(); + // } + // } + + Future setTranslationFeedback() async { + try { + choreographer.startLoading(); + translationFeedbackKey = FeedbackKey.loadingPleaseWait; + + showTranslationFeedback = true; + + userTranslation = choreographer.currentText; + + if (choreographer.itController.allCorrect) { + translationFeedbackKey = FeedbackKey.allCorrect; + return; + } + + final String? goldRouteTranslation = + choreographer.itController.goldRouteTracker.fullTranslation; + + final FullTextTranslationResponseModel results = + await FullTextTranslationRepo.translate( + accessToken: await choreographer.accessToken, + request: FullTextTranslationRequestModel( + text: choreographer.itController.sourceText!, + tgtLang: choreographer.l2LangCode!, + userL2: choreographer.l2LangCode!, + userL1: choreographer.l1LangCode!, + deepL: goldRouteTranslation == null, + ), + ); + translations = results.translations; + if (results.deepL != null || goldRouteTranslation != null) { + translations.insert(0, (results.deepL ?? goldRouteTranslation)!); + } + // final List altAndUser = [...results.translations]; + // if (results.deepL != null) { + // altAndUser.add(results.deepL!); + // } + // altAndUser.add(userTranslation); + + if (userTranslation?.toLowerCase() == + results.bestTranslation.toLowerCase()) { + translationFeedbackKey = FeedbackKey.allCorrect; + return; + } + + similarityResponse = await SimilarityRepo.get( + accessToken: await choreographer.accessToken, + request: SimilarityRequestModel( + benchmark: results.bestTranslation, + toCompare: [userTranslation!], + ), + ); + + // if (similarityResponse! + // .userTranslationIsSameAsBotTranslation(userTranslation!)) { + // translationFeedbackKey = FeedbackKey.allCorrect; + // return; + // } + + // if (similarityResponse! + // .userTranslationIsDifferentButBetter(userTranslation!)) { + // translationFeedbackKey = FeedbackKey.newWayAllGood; + // return; + // } + showAlternativeTranslations = true; + translationFeedbackKey = FeedbackKey.othersAreBetter; + } catch (err, stack) { + if (err is! http.Response) { + ErrorHandler.logError(e: err, s: stack); + } + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: err), + ); + } finally { + choreographer.stopLoading(); + } + } + + String translationFeedback(BuildContext context) { + if (L10n.of(context) == null) { + debugger(when: kDebugMode); + } + switch (translationFeedbackKey) { + case FeedbackKey.allCorrect: + return "Score: 100%\n${L10n.of(context)!.allCorrect}"; + case FeedbackKey.newWayAllGood: + return "Score: 100%\n${L10n.of(context)!.newWayAllGood}"; + case FeedbackKey.othersAreBetter: + final num userScore = + (similarityResponse!.userScore(userTranslation!) * 100).round(); + final String displayScore = userScore.toString(); + if (userScore > 90) { + return "Score: $displayScore%\n${L10n.of(context)!.almostPerfect}"; + } + if (userScore > 80) { + return "Score: $displayScore%\n${L10n.of(context)!.prettyGood}"; + } + return "Score: $displayScore%\n${L10n.of(context)!.othersAreBetter}"; + // case FeedbackKey.commonalityFeedback: + // final int count = controller.completedITSteps + // .where((element) => element.isCorrect) + // .toList() + // .length; + // final int total = controller.completedITSteps.length; + // return L10n.of(context)!.commonalityFeedback(count,total); + case FeedbackKey.loadingPleaseWait: + return L10n.of(context)!.letMeThink; + case FeedbackKey.allDone: + return L10n.of(context)!.allDone; + default: + return L10n.of(context)!.loadingPleaseWait; + } + } +} + +enum FeedbackKey { + allCorrect, + newWayAllGood, + othersAreBetter, + loadingPleaseWait, + allDone, +} + +extension FeedbackKeyExtension on FeedbackKey {} diff --git a/lib/pangea/choreographer/controllers/analytics_sender.dart b/lib/pangea/choreographer/controllers/analytics_sender.dart new file mode 100644 index 000000000..fe4575f38 --- /dev/null +++ b/lib/pangea/choreographer/controllers/analytics_sender.dart @@ -0,0 +1,24 @@ +import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart'; + +class MlController { + final ITController controller; + MlController(this.controller); + + // sendPayloads(String message, String messageId) async { + // final MessageServiceModel serviceModel = MessageServiceModel( + // classId: controller.state!.classId, + // roomId: controller.state!.roomId, + // message: message.toString(), + // messageId: messageId.toString(), + // payloadIds: controller.state!.payLoadIds, + // userId: controller.state!.userId!, + // l1Lang: controller.state!.sourceLangCode, + // l2Lang: controller.state!.targetLangCode!, + // ); + // try { + // await MessageServiceRepo.sendPayloads(serviceModel); + // } catch (err) { + // debugPrint('$err in sendPayloads'); + // } + // } +} diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart new file mode 100644 index 000000000..419305fb6 --- /dev/null +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -0,0 +1,524 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/edit_type.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/it_step.dart'; +import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/models/widget_measurement.dart'; +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../../widgets/matrix.dart'; +import '../../enum/use_type.dart'; +import '../../models/choreo_record.dart'; +import '../../models/language_model.dart'; +import '../../models/pangea_match_model.dart'; +import '../../widgets/igc/pangea_text_controller.dart'; +import 'error_service.dart'; +import 'it_controller.dart'; + +enum ChoreoMode { igc, it } + +class Choreographer { + PangeaController pangeaController; + ChatController chatController; + late PangeaTextController _textController; + late ITController itController; + late IgcController igc; + late MessageOptions messageOptions; + late AlternativeTranslator altTranslator; + late ErrorService errorService; + + bool isFetching = false; + Timer? debounceTimer; + String? _roomId; + ChoreoRecord choreoRecord = ChoreoRecord.newRecord; + // last checked by IGC or translation + String? _lastChecked; + ChoreoMode choreoMode = ChoreoMode.igc; + final StreamController stateListener = StreamController(); + + bool toldToPay = false; + + Choreographer(this.pangeaController, this.chatController) { + _initialize(); + } + _initialize() { + _textController = PangeaTextController(choreographer: this); + itController = ITController(this); + igc = IgcController(this); + messageOptions = MessageOptions(this); + errorService = ErrorService(this); + altTranslator = AlternativeTranslator(this); + _textController.addListener(_onChangeListener); + + clear(); + } + + void send(BuildContext context) { + if (isFetching) return; + + if (!pangeaController.subscriptionController.isSubscribed && !toldToPay) { + toldToPay = true; + pangeaController.subscriptionController.showPaywall(context); + return; + } + + if (!igc.hasRelevantIGCTextData) { + getLanguageHelp().then((value) => _sendWithIGC(context)); + } else { + _sendWithIGC(context); + } + } + + void _sendWithIGC(BuildContext context) { + if (igc.canSendMessage) { + final PangeaRepresentation? originalWritten = + choreoRecord.includedIT && itController.sourceText != null + ? PangeaRepresentation( + langCode: l1LangCode ?? LanguageKeys.unknownLanguage, + text: itController.sourceText!, + originalWritten: true, + originalSent: false, + ) + : null; + + // PTODO - just put this in original message event + final PangeaRepresentation originalSent = PangeaRepresentation( + langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage, + text: currentText, + originalSent: true, + originalWritten: originalWritten == null, + ); + final ChoreoRecord? applicableChoreo = + isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; + + final UseType useType = useTypeCalculator(applicableChoreo); + debugPrint("use type in choreographer $useType"); + + chatController.send( + // PTODO - turn this back on in conjunction with saving tokens + // we need to save those tokens as well, in order for exchanges to work + // properly. in an exchange, the other user will want + // originalWritten: originalWritten, + originalSent: originalSent, + tokensSent: igc.igcTextData?.tokens != null + ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) + : null, + //TODO - save originalwritten tokens + choreo: applicableChoreo, + useType: useType, + ); + + clear(); + } else { + igc.showFirstMatch(context); + } + } + + _resetDebounceTimer() { + if (debounceTimer != null) { + debounceTimer!.cancel(); + debounceTimer = null; + } + } + + void onITStart(PangeaMatch itMatch) { + if (!itMatch.isITStart) { + throw Exception("this isn't an itStart match!"); + } + choreoMode = ChoreoMode.it; + itController.initializeIT( + ITStartData(_textController.text, igc.detectedLangCode), + ); + itMatch.status = PangeaMatchStatus.accepted; + + choreoRecord.addRecord(_textController.text, match: itMatch); + + //PTODO - if totally in L1, save tokens, that's good stuff + + igc.clear(); + + _textController.setSystemText("", EditType.itStart); + } + + _onChangeListener() { + if (_noChange) { + return; + } + + if ([ + EditType.igc, + ].contains(_textController.editType)) { + igc.justGetTokensAndAddThemToIGCTextData(); + textController.editType = EditType.keyboard; + return; + } + + MatrixState.pAnyState.closeOverlay(); + + if (errorService.isError) { + return; + } + + // if (igc.igcTextData != null) { + igc.clear(); + // setState(); + // } + + _resetDebounceTimer(); + + if (editTypeIsKeyboard) { + debounceTimer ??= Timer( + const Duration(milliseconds: 1500), + () => getLanguageHelp(), + ); + } else { + getLanguageHelp(ChoreoMode.it == choreoMode); + } + + //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to + //a change being from the keyboard unless explicitly set to one of the other + //types when that action happens (e.g. an it/igc choice is selected) + textController.editType = EditType.keyboard; + } + + Future getLanguageHelp([bool tokensOnly = false]) async { + try { + if (errorService.isError) return; + if (!pangeaController.subscriptionController.isSubscribed && + pangeaController.subscriptionController.initialized) { + debugPrint('setting not subscribed error'); + errorService.setErrorAndLock( + ChoreoError( + type: ChoreoErrorType.unsubscribed, + ), + ); + return; + } + startLoading(); + if (choreoMode == ChoreoMode.it && + itController.isTranslationDone && + !tokensOnly) { + debugger(when: kDebugMode); + } + await (choreoMode == ChoreoMode.it && !itController.isTranslationDone + ? itController.getTranslationData(_useCustomInput) + : igc.getIGCTextData(tokensOnly: tokensOnly)); + } catch (err, stack) { + ErrorHandler.logError(e: err, s: stack); + } finally { + stopLoading(); + } + } + + void onITChoiceSelect(ITStep step) { + choreoRecord.addRecord(_textController.text, step: step); + _textController.setSystemText( + _textController.text + step.continuances[step.chosen!].text, + step.continuances[step.chosen!].gold + ? EditType.itGold + : EditType.itStandard); + _textController.selection = + TextSelection.collapsed(offset: _textController.text.length); + giveInputFocus(); + } + + Future onReplacementSelect({ + required int matchIndex, + required int choiceIndex, + }) async { + try { + if (igc.igcTextData == null) { + ErrorHandler.logError( + e: "onReplacementSelect with null igcTextData", + s: StackTrace.current, + ); + MatrixState.pAnyState.closeOverlay(); + } + if (igc.igcTextData!.matches[matchIndex].match.choices == null) { + ErrorHandler.logError( + e: "onReplacementSelect with null choices", + s: StackTrace.current, + ); + MatrixState.pAnyState.closeOverlay(); + } + + //if it's the wrong choice, return + // if (!igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex] + // .selected) { + // igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex] + // .selected = true; + // setState(); + // return; + // } + + igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex] + .selected = true; + + //if it's the right choice, replace in text + choreoRecord.addRecord( + _textController.text, + match: igc.igcTextData!.matches[matchIndex].copyWith + ..status = PangeaMatchStatus.accepted, + ); + + igc.igcTextData!.acceptReplacement( + matchIndex, + choiceIndex, + ); + + _textController.setSystemText( + igc.igcTextData!.originalInput, + EditType.igc, + ); + + MatrixState.pAnyState.closeOverlay(); + setState(); + } catch (err, stack) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb.fromJson( + { + "igctextDdata": igc.igcTextData?.toJson(), + "matchIndex": matchIndex, + "choiceIndex": choiceIndex, + }, + ), + ); + ErrorHandler.logError(e: err, s: stack); + igc.igcTextData?.matches.clear(); + } finally { + giveInputFocus(); + setState(); + } + } + + void onIgnoreMatch({required int cursorOffset}) { + try { + if (igc.igcTextData == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "should not be in onIgnoreMatch with null igcTextData", + s: StackTrace.current, + ); + return; + } + + final int matchIndex = igc.igcTextData!.getTopMatchIndexForOffset( + cursorOffset, + ); + + if (matchIndex == -1) { + debugger(when: kDebugMode); + throw Exception("Cannnot find the ignored match in igcTextData"); + } + + igc.igcTextData!.matches[matchIndex].status = PangeaMatchStatus.ignored; + choreoRecord.addRecord(_textController.text, + match: igc.igcTextData!.matches[matchIndex]); + + igc.igcTextData!.matches.removeAt(matchIndex); + } catch (err, stack) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb.fromJson( + {"igcTextData": igc.igcTextData?.toJson(), "offset": cursorOffset}), + ); + ErrorHandler.logError( + e: err, + s: stack, + ); + igc.igcTextData?.matches.clear(); + } finally { + setState(); + giveInputFocus(); + } + } + + void onSelectAlternativeTranslation(String translation) { + // PTODO - add some kind of record of this + // choreoRecord.addRecord(_textController.text, match); + + _textController.setSystemText( + translation, + EditType.alternativeTranslation, + ); + altTranslator.clear(); + altTranslator.translationFeedbackKey = FeedbackKey.allDone; + altTranslator.showTranslationFeedback = true; + giveInputFocus(); + setState(); + } + + giveInputFocus() { + Future.delayed(Duration.zero, () { + chatController.inputFocus.requestFocus(); + }); + } + + String get currentText => _textController.text; + + PangeaTextController get textController => _textController; + + Future get accessToken => pangeaController.userController.accessToken; + + clear() { + choreoMode = ChoreoMode.igc; + _lastChecked = null; + isFetching = false; + choreoRecord = ChoreoRecord.newRecord; + itController.clear(); + igc.clear(); + // errorService.clear(); + _resetDebounceTimer(); + } + + void onMatchError({int? cursorOffset}) { + if (cursorOffset == null) { + igc.igcTextData?.matches.clear(); + } else { + final int? matchIndex = igc.igcTextData?.getTopMatchIndexForOffset( + cursorOffset, + ); + matchIndex == -1 || matchIndex == null + ? igc.igcTextData?.matches.clear() + : igc.igcTextData?.matches.removeAt(matchIndex); + } + + setState(); + giveInputFocus(); + } + + dispose() { + _textController.dispose(); + } + + LanguageModel? get l2Lang { + return pangeaController.languageController.activeL2Model( + roomID: roomId, + ); + } + + String? get l2LangCode => l2Lang?.langCode; + + LanguageModel? get l1Lang => + pangeaController.languageController.activeL1Model( + roomID: roomId, + ); + String? get l1LangCode => l1Lang?.langCode; + + String? get classId => roomId != null + ? pangeaController.matrixState.client + .getRoomById(roomId) + ?.firstParentWithState(PangeaEventTypes.classSettings) + ?.id + : null; + + String? get userId => pangeaController.userController.userId; + + bool get _noChange => + _lastChecked != null && _lastChecked == _textController.text; + + void startLoading() { + _lastChecked = _textController.text; + isFetching = true; + setState(); + } + + void stopLoading() { + isFetching = false; + setState(); + } + + get roomId => _roomId; + void setRoomId(String? roomId) { + _roomId = roomId ?? ''; + } + + bool get _useCustomInput => [ + EditType.keyboard, + EditType.igc, + EditType.alternativeTranslation + ].contains(_textController.editType); + + bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType; + + String? get langCodeOfCurrentText { + if (igc.detectedLangCode != null) return igc.detectedLangCode!; + + if (itController.isOpen) return l2LangCode!; + + return null; + } + + setState() { + if (!stateListener.isClosed) { + stateListener.add(0); + } + } + + WidgetMeasurements get inputBarSize => _textController.measurements!; + + bool get showIsError => !itController.isOpen && errorService.isError; + + LayerLinkAndKey get itBarLinkAndKey => + MatrixState.pAnyState.layerLinkAndKey(itBarTransformTargetKey); + + String get itBarTransformTargetKey => 'it_bar$roomId'; + + LayerLinkAndKey get inputLayerLinkAndKey => + MatrixState.pAnyState.layerLinkAndKey(inputTransformTargetKey); + + String get inputTransformTargetKey => 'input$roomId'; + + LayerLinkAndKey get itBotLayerLinkAndKey => + MatrixState.pAnyState.layerLinkAndKey(itBotTransformTargetKey); + + String get itBotTransformTargetKey => 'itBot$roomId'; + + bool get igcEnabled => pangeaController.permissionsController.isToolEnabled( + ToolSetting.interactiveGrammar, + chatController.room, + ); + + bool get itEnabled => pangeaController.permissionsController.isToolEnabled( + ToolSetting.interactiveTranslator, + chatController.room, + ); + + bool get definitionsEnabled => + pangeaController.permissionsController.isToolEnabled( + ToolSetting.definitions, + chatController.room, + ); + + bool get immersionMode => + pangeaController.permissionsController.isToolEnabled( + ToolSetting.immersionMode, + chatController.room, + ); + + bool get translationEnabled => + pangeaController.permissionsController.isToolEnabled( + ToolSetting.translations, + chatController.room, + ); + + bool get isITandIGCEnabled => + pangeaController.permissionsController.isWritingAssistanceEnabled( + chatController.room, + ); +} diff --git a/lib/pangea/choreographer/controllers/error_service.dart b/lib/pangea/choreographer/controllers/error_service.dart new file mode 100644 index 000000000..c03ec8df8 --- /dev/null +++ b/lib/pangea/choreographer/controllers/error_service.dart @@ -0,0 +1,101 @@ +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:flutter/material.dart'; + +import '../../utils/error_handler.dart'; + +enum ChoreoErrorType { + unknown, + classDisabled, + userDisabled, + unsubscribed, +} + +class ChoreoError { + final ChoreoErrorType type; + final Object? raw; + + ChoreoError({required this.type, this.raw}); + + String title(BuildContext context) { + switch (type) { + case ChoreoErrorType.classDisabled: + return "Class Disabled"; + case ChoreoErrorType.userDisabled: + return "User Disabled"; + case ChoreoErrorType.unsubscribed: + return "Unsubscribed"; + default: + return ErrorCopy(context, raw).title; + } + } + + String description(BuildContext context) { + switch (type) { + case ChoreoErrorType.classDisabled: + return "Class Disabled"; + case ChoreoErrorType.userDisabled: + return "User Disabled"; + case ChoreoErrorType.unsubscribed: + return "Unsubscribed"; + default: + return ErrorCopy(context, raw).body; + } + } + + IconData get icon { + switch (type) { + case ChoreoErrorType.classDisabled: + return Icons.history_edu_outlined; + case ChoreoErrorType.userDisabled: + return Icons.history_edu_outlined; + case ChoreoErrorType.unsubscribed: + return Icons.lock_outline; + default: + return Icons.error_outline; + } + } +} + +class ErrorService { + ChoreoError? _error; + int coolDownSeconds = 0; + final Choreographer controller; + + ErrorService(this.controller); + + bool get isError => _error != null; + + ChoreoError? get error => _error; + + Duration get defaultCooldown { + coolDownSeconds += 3; + return Duration(seconds: coolDownSeconds); + } + + setError(ChoreoError? error, {Duration? duration}) { + _error = error; + Future.delayed(duration ?? defaultCooldown, () { + clear(); + _setState(); + }); + _setState(); + } + + setErrorAndLock(ChoreoError? error) { + _error = error; + _setState(); + } + + resetError() { + clear(); + _setState(); + } + + void _setState() { + controller.setState(); + } + + void clear() { + _error = null; + } +} diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart new file mode 100644 index 000000000..38d9870a0 --- /dev/null +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; +import 'package:fluffychat/pangea/models/igc_text_data_model.dart'; +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/span_data.dart'; +import 'package:fluffychat/pangea/repo/igc_repo.dart'; +import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../../widgets/matrix.dart'; +import '../../models/language_detection_model.dart'; +import '../../models/span_card_model.dart'; +import '../../repo/span_data_repo.dart'; +import '../../repo/tokens_repo.dart'; +import '../../utils/error_handler.dart'; +import '../../utils/overlay.dart'; + +class IgcController { + Choreographer choreographer; + IGCTextData? igcTextData; + Object? igcError; + + Completer igcCompleter = Completer(); + + IgcController(this.choreographer); + + Future getIGCTextData({required bool tokensOnly}) async { + try { + if (choreographer.currentText.isEmpty) return clear(); + + debugPrint('getIGCTextData called with ${choreographer.currentText}'); + + debugPrint('getIGCTextData called with tokensOnly = $tokensOnly'); + + final IGCRequestBody reqBody = IGCRequestBody( + fullText: choreographer.currentText, + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled && !tokensOnly, + enableIT: choreographer.itEnabled && !tokensOnly, + tokensOnly: tokensOnly, + ); + + final IGCTextData igcTextDataResponse = await IgcRepo.getIGC( + await choreographer.accessToken, + igcRequest: reqBody, + ); + // temp fix + igcTextDataResponse.originalInput = reqBody.fullText; + + //this will happen when the user changes the input while igc is fetching results + if (igcTextDataResponse.originalInput != choreographer.currentText) { + // final current = choreographer.currentText; + // final igctext = igcTextDataResponse.originalInput; + // Sentry.addBreadcrumb( + // Breadcrumb(message: "igc return input does not match current text"), + // ); + // debugger(when: kDebugMode); + return; + } + + //TO-DO: in api call, specify turning off IT and/or grammar checking + if (!choreographer.igcEnabled) { + igcTextDataResponse.matches = igcTextDataResponse.matches + .where((match) => !match.isGrammarMatch) + .toList(); + } + if (!choreographer.itEnabled) { + igcTextDataResponse.matches = igcTextDataResponse.matches + .where((match) => !match.isOutOfTargetMatch) + .toList(); + } + if (!choreographer.itEnabled && !choreographer.igcEnabled) { + igcTextDataResponse.matches = []; + } + + igcTextData = igcTextDataResponse; + + debugPrint("igc text ${igcTextData.toString()}"); + } catch (err, stack) { + debugger(when: kDebugMode); + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: err), + ); + ErrorHandler.logError(e: err, s: stack); + clear(); + } + } + + Future getSpanDetails(int matchIndex) async { + if (igcTextData == null || + igcTextData!.matches.isEmpty || + matchIndex < 0 || + matchIndex >= igcTextData!.matches.length) { + debugger(when: kDebugMode); + return; + } + final SpanData span = igcTextData!.matches[matchIndex].match; + + final SpanDetailsRepoReqAndRes response = await SpanDataRepo.getSpanDetails( + await choreographer.accessToken, + request: SpanDetailsRepoReqAndRes( + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled, + enableIT: choreographer.itEnabled, + span: span, + ), + ); + + igcTextData!.matches[matchIndex].match = response.span; + + choreographer.setState(); + } + + Future justGetTokensAndAddThemToIGCTextData() async { + try { + if (igcTextData == null) { + debugger(when: kDebugMode); + choreographer.getLanguageHelp(); + return; + } + igcTextData!.loading = true; + choreographer.startLoading(); + if (igcTextData!.originalInput != choreographer.textController.text) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "igcTextData fullText does not match current text", + s: StackTrace.current, + data: igcTextData!.toJson(), + ); + } + final TokensResponseModel res = await TokensRepo.tokenize( + await choreographer.pangeaController.userController.accessToken, + TokensRequestModel( + fullText: igcTextData!.originalInput, + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + ), + ); + igcTextData?.tokens = res.tokens; + } catch (err, stack) { + debugger(when: kDebugMode); + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: err), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), + ); + ErrorHandler.logError(e: err, s: stack); + } finally { + igcTextData!.loading = false; + choreographer.stopLoading(); + } + } + + void showFirstMatch(BuildContext context) { + if (igcTextData == null || igcTextData!.matches.isEmpty) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "should not be calling showFirstMatch with this igcTextData ${igcTextData?.toJson().toString()}", + s: StackTrace.current, + ); + return; + } + + const int firstMatchIndex = 0; + final PangeaMatch match = igcTextData!.matches[firstMatchIndex]; + + OverlayUtil.showPositionedCard( + context: context, + cardToShow: SpanCard( + scm: SpanCardModel( + matchIndex: firstMatchIndex, + onReplacementSelect: choreographer.onReplacementSelect, + onSentenceRewrite: (value) async {}, + onIgnore: () => choreographer.onIgnoreMatch( + cursorOffset: match.match.offset, + ), + onITStart: () { + if (choreographer.itEnabled) { + choreographer.onITStart(igcTextData!.matches[firstMatchIndex]); + } + }, + choreographer: choreographer, + ), + roomId: choreographer.roomId, + ), + cardSize: match.isITStart ? const Size(350, 220) : const Size(350, 400), + transformTargetId: choreographer.inputTransformTargetKey, + ); + } + + bool get hasRelevantIGCTextData { + if (igcTextData == null) return false; + + if (igcTextData!.originalInput != choreographer.currentText) { + debugPrint( + "returning isIGCTextDataRelevant false because text has changed"); + return false; + } + return true; + } + + String? get detectedLangCode { + if (!hasRelevantIGCTextData) return null; + + final LanguageDetection first = igcTextData!.detections.first; + + return first.langCode; + } + + clear() { + igcTextData = null; + MatrixState.pAnyState.closeOverlay(); + } + + bool get canSendMessage { + if (choreographer.isFetching) return false; + if (igcTextData == null || + choreographer.errorService.isError || + igcTextData!.matches.isEmpty) { + return true; + } + + return !((choreographer.itEnabled && + igcTextData!.matches.any((match) => match.isOutOfTargetMatch)) || + (choreographer.igcEnabled && + igcTextData!.matches.any((match) => !match.isOutOfTargetMatch))); + } +} diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart new file mode 100644 index 000000000..f58faffae --- /dev/null +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -0,0 +1,429 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../models/custom_input_translation_model.dart'; +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 { + Choreographer choreographer; + + bool _isOpen = false; + bool _isEditingSourceText = false; + bool showChoiceFeedback = false; + + ITStartData? _itStartData; + String? sourceText; + List completedITSteps = []; + CurrentITStep? currentITStep; + GoldRouteTracker goldRouteTracker = GoldRouteTracker.defaultTracker; + List payLoadIds = []; + + ITController(this.choreographer); + + void clear() { + _isOpen = false; + showChoiceFeedback = false; + _isEditingSourceText = false; + + _itStartData = null; + sourceText = null; + completedITSteps = []; + currentITStep = null; + goldRouteTracker = GoldRouteTracker.defaultTracker; + payLoadIds = []; + + choreographer.altTranslator.clear(); + choreographer.errorService.resetError(); + choreographer.choreoMode = ChoreoMode.igc; + choreographer.setState(); + } + + Future initializeIT(ITStartData itStartData) async { + Future.delayed(const Duration(microseconds: 100), () { + _isOpen = true; + }); + _itStartData = itStartData; + } + + void closeIT() { + //if they close it before choosing anything, just put their text back + //PTODO - explore using last itStep + if (choreographer.currentText.isEmpty) { + choreographer.textController.text = sourceText ?? ""; + } + clear(); + } + + /// if IGC isn't positive that text is full L1 then translate to L1 + Future _setSourceText() async { + // try { + if (_itStartData == null || _itStartData!.text.isEmpty) { + Sentry.addBreadcrumb( + Breadcrumb(message: "choreo context", data: { + "igcTextData": choreographer.igc.igcTextData?.toJson(), + "currentText": choreographer.currentText + }), + ); + throw Exception("null _itStartData or empty text in _setSourceText"); + } + debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}"); + if (_itStartData!.langCode == choreographer.l1LangCode) { + sourceText = _itStartData!.text; + return; + } + + final FullTextTranslationResponseModel res = + await FullTextTranslationRepo.translate( + accessToken: await choreographer.accessToken, + request: FullTextTranslationRequestModel( + text: _itStartData!.text, + tgtLang: choreographer.l1LangCode!, + srcLang: choreographer.l2LangCode, + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + ), + ); + sourceText = res.bestTranslation; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // if (_itStartData?.text.isNotEmpty ?? false) { + // ErrorHandler.logError(e: err, s: stack); + // sourceText = _itStartData!.text; + // } else { + // rethrow; + // } + // } + } + + // used 1) at very beginning (with custom input = null) + // and 2) if they make direct edits to the text field + Future getTranslationData(bool useCustomInput) async { + try { + choreographer.startLoading(); + + final String currentText = choreographer.currentText; + final String? translationId = currentITStep?.translationId; + + if (sourceText == null) await _setSourceText(); + + if (useCustomInput && currentITStep != null) { + completedITSteps.add(ITStep( + currentITStep!.continuances, + customInput: currentText, + )); + } + + currentITStep = null; + + final ITResponseModel res = await (useCustomInput || + currentText.isEmpty || + translationId == null || + completedITSteps.last.chosenContinuance?.indexSavedByServer == + null + ? _customInputTranslation(currentText) + : _systemChoiceTranslation(translationId)); + + if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) { + goldRouteTracker = GoldRouteTracker( + res.goldContinuances!, + sourceText!, + ); + } + + currentITStep = CurrentITStep( + sourceText: sourceText!, + currentText: currentText, + responseModel: res, + storedGoldContinuances: goldRouteTracker.continuances, + ); + + _addPayloadId(res); + + if (isTranslationDone) { + choreographer.altTranslator.setTranslationFeedback(); + choreographer.getLanguageHelp(true); + } + } catch (e, s) { + debugger(when: kDebugMode); + if (e is! http.Response) { + ErrorHandler.logError(e: e, s: s); + } + choreographer.errorService.setErrorAndLock( + ChoreoError(type: ChoreoErrorType.unknown, raw: e), + ); + } finally { + choreographer.stopLoading(); + } + } + + Future onEditSourceTextSubmit(String newSourceText) async { + try { + sourceText = newSourceText; + _isEditingSourceText = false; + final String currentText = choreographer.currentText; + + choreographer.startLoading(); + + final List responses = await Future.wait([ + _customInputTranslation(""), + _customInputTranslation(choreographer.currentText), + ]); + if (responses[0].goldContinuances != null && + responses[0].goldContinuances!.isNotEmpty) { + goldRouteTracker = GoldRouteTracker( + responses[0].goldContinuances!, + sourceText!, + ); + } + currentITStep = CurrentITStep( + sourceText: sourceText!, + currentText: currentText, + responseModel: responses[1], + storedGoldContinuances: goldRouteTracker.continuances, + ); + + _addPayloadId(responses[1]); + } catch (err, stack) { + debugger(when: kDebugMode); + if (err is! http.Response) { + ErrorHandler.logError(e: err, s: stack); + } + choreographer.errorService.setErrorAndLock( + ChoreoError(type: ChoreoErrorType.unknown, raw: err), + ); + } finally { + choreographer.stopLoading(); + } + } + + Future _customInputTranslation(String textInput) async { + return ITRepo.customInputTranslate( + CustomInputRequestModel( + //this should be set by this time + text: sourceText!, + customInput: textInput, + sourceLangCode: sourceLangCode, + targetLangCode: targetLangCode, + userId: choreographer.userId!, + roomId: choreographer.roomId!, + classId: choreographer.classId, + ), + ); + } + + // used when user selects a choice + Future _systemChoiceTranslation(String translationId) => + ITRepo.systemChoiceTranslate( + SystemChoiceRequestModel( + userId: choreographer.userId!, + nextWordIndex: + completedITSteps.last.chosenContinuance?.indexSavedByServer, + roomId: choreographer.roomId!, + translationId: translationId, + targetLangCode: targetLangCode, + sourceLangCode: sourceLangCode, + classId: choreographer.classId, + ), + ); + + 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) { + final itStep = ITStep(currentITStep!.continuances, chosen: chosenIndex); + + completedITSteps.add(itStep); + + showChoiceFeedback = true; + Future.delayed( + const Duration( + milliseconds: ChoreoConstants.millisecondsToDisplayFeedback, + ), + () { + showChoiceFeedback = false; + choreographer.setState(); + }, + ); + + choreographer.onITChoiceSelect(itStep); + choreographer.setState(); + } + + String get uniqueKeyForLayerLink => "itChoices${choreographer.roomId}"; + + void _addPayloadId(ITResponseModel res) { + payLoadIds.add(res.payloadId); + } + + bool get usedInteractiveTranslation => sourceText != null; + + bool get isTranslationDone => currentITStep != null && currentITStep!.isFinal; + + bool get isOpen => _isOpen; + + String get targetLangCode => choreographer.l2LangCode!; + + String get sourceLangCode => choreographer.l1LangCode!; + + bool get isLoading => choreographer.isFetching; + + bool get correctChoicesSelected => + completedITSteps.every((ITStep step) => step.isCorrect); + + String latestChoiceFeedback(BuildContext context) => + completedITSteps.isNotEmpty + ? completedITSteps.last.choiceFeedback(context) + : ""; + + // String translationFeedback(BuildContext context) => + // completedITSteps.isNotEmpty + // ? completedITSteps.last.translationFeedback(context) + // : ""; + + bool get showAlternativeTranslationsOption => completedITSteps.isNotEmpty + ? completedITSteps.last.showAlternativeTranslationOption && + sourceText != null + : false; + + setIsEditingSourceText(bool value) { + _isEditingSourceText = value; + choreographer.setState(); + } + + bool get isEditingSourceText => _isEditingSourceText; + + int get correctChoices => + completedITSteps.where((element) => element.isCorrect).length; + + int get wildcardChoices => + completedITSteps.where((element) => element.isYellow).length; + + int get incorrectChoices => + completedITSteps.where((element) => element.isWrong).length; + + int get customChoices => + completedITSteps.where((element) => element.isCustom).length; + + bool get allCorrect => completedITSteps.every((element) => element.isCorrect); +} + +class ITStartData { + String text; + String? langCode; + + ITStartData(this.text, this.langCode); +} + +class GoldRouteTracker { + late String _originalText; + List continuances; + + GoldRouteTracker(this.continuances, String originalText) { + _originalText = originalText; + } + + static get defaultTracker => GoldRouteTracker([], ""); + + Continuance? currentContinuance({ + required String currentText, + required String sourceText, + }) { + if (_originalText != sourceText) { + debugPrint("$_originalText != $_originalText"); + return null; + } + + String stack = ""; + for (final cont in continuances) { + if (stack == currentText) { + return cont; + } + stack += cont.text; + } + + return null; + } + + String? get fullTranslation { + if (continuances.isEmpty) return null; + String full = ""; + for (final cont in continuances) { + full += cont.text; + } + return full; + } +} + +class CurrentITStep { + late List continuances; + late bool isFinal; + late String? translationId; + late int payloadId; + + CurrentITStep({ + required String sourceText, + required String currentText, + required ITResponseModel responseModel, + required List? storedGoldContinuances, + }) { + final List gold = + storedGoldContinuances ?? responseModel.goldContinuances ?? []; + final goldTracker = GoldRouteTracker(gold, sourceText); + + isFinal = responseModel.isFinal; + translationId = responseModel.translationId; + payloadId = responseModel.payloadId; + + if (responseModel.continuances.isEmpty) { + continuances = []; + } else { + final Continuance? goldCont = goldTracker.currentContinuance( + currentText: currentText, + sourceText: sourceText, + ); + if (goldCont != null) { + continuances = [ + ...responseModel.continuances + .where((c) => c.text.toLowerCase() != goldCont.text.toLowerCase()) + .map((e) { + //we only want one green choice and for that to be our gold + if (e.level == ChoreoConstants.levelThresholdForGreen) { + e.level = ChoreoConstants.levelThresholdForYellow; + } + return e; + }), + goldCont + ]; + continuances.shuffle(); + } else { + continuances = responseModel.continuances; + } + } + } +} diff --git a/lib/pangea/choreographer/controllers/message_options.dart b/lib/pangea/choreographer/controllers/message_options.dart new file mode 100644 index 000000000..8a01674b9 --- /dev/null +++ b/lib/pangea/choreographer/controllers/message_options.dart @@ -0,0 +1,45 @@ +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:flutter/cupertino.dart'; + +class MessageOptions { + Choreographer choreographer; + LanguageModel? _selectedDisplayLang; + + MessageOptions(this.choreographer); + + LanguageModel? get selectedDisplayLang { + if (_selectedDisplayLang != null && + _selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) { + return _selectedDisplayLang; + } + _selectedDisplayLang = choreographer.l2Lang; + return _selectedDisplayLang; + } + + bool get isTranslationOn => + _selectedDisplayLang?.langCode != choreographer.l2LangCode; + + // void setSelectedDisplayLang(LanguageModel? newLang) { + // _selectedDisplayLang = newLang; + // choreographer.setState(); + // } + + void toggleSelectedDisplayLang() { + if (_selectedDisplayLang?.langCode == choreographer.l2LangCode) { + _selectedDisplayLang = choreographer.l1Lang; + } else { + _selectedDisplayLang = choreographer.l2Lang; + } + debugPrint('toggleSelectedDisplayLang: ${_selectedDisplayLang?.langCode}'); + choreographer.setState(); + GoogleAnalytics.messageTranslate(); + } + + void resetSelectedDisplayLang() { + _selectedDisplayLang = choreographer.l2Lang; + choreographer.setState(); + } +} diff --git a/lib/pangea/choreographer/route_type.dart b/lib/pangea/choreographer/route_type.dart new file mode 100644 index 000000000..0abc175e0 --- /dev/null +++ b/lib/pangea/choreographer/route_type.dart @@ -0,0 +1,5 @@ +class ITState { + static const String loading = 'loading'; + static const String choices = 'choices'; + static const String error = 'error'; +} diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart new file mode 100644 index 000000000..acc56aa33 --- /dev/null +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -0,0 +1,135 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../utils/bot_style.dart'; +import 'it_shimmer.dart'; + +class ChoicesArray extends StatelessWidget { + final bool isLoading; + final List? choices; + final void Function(int) onPressed; + final void Function(int)? onLongPress; + final int? selectedChoiceIndex; + final String originalSpan; + final String Function(int) uniqueKeyForLayerLink; + const ChoicesArray({ + Key? key, + required this.isLoading, + required this.choices, + required this.onPressed, + required this.originalSpan, + required this.uniqueKeyForLayerLink, + required this.selectedChoiceIndex, + this.onLongPress, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return isLoading && (choices == null || choices!.length <= 1) + ? ItShimmer(originalSpan: originalSpan) + : Wrap( + alignment: WrapAlignment.center, + children: choices + ?.asMap() + .entries + .map( + (entry) => ChoiceItem( + theme: theme, + onLongPress: onLongPress, + onPressed: onPressed, + entry: entry, + isSelected: selectedChoiceIndex == entry.key, + ), + ) + .toList() ?? + [], + ); + } +} + +class Choice { + Choice({ + this.color, + required this.text, + }); + + final Color? color; + final String text; +} + +class ChoiceItem extends StatelessWidget { + const ChoiceItem( + {Key? key, + required this.theme, + required this.onLongPress, + required this.onPressed, + required this.entry, + required this.isSelected}) + : super(key: key); + + final MapEntry entry; + final ThemeData theme; + final void Function(int p1)? onLongPress; + final void Function(int p1) onPressed; + final bool isSelected; + + @override + Widget build(BuildContext context) { + try { + return Tooltip( + message: onLongPress != null ? L10n.of(context)!.holdForInfo : "", + waitDuration: onLongPress != null + ? const Duration(milliseconds: 500) + : const Duration(days: 1), + child: Container( + margin: const EdgeInsets.all(2), + padding: EdgeInsets.zero, + decoration: isSelected + ? BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10)), + border: Border.all( + color: entry.value.color ?? theme.colorScheme.primary, + style: BorderStyle.solid, + width: 2.0, + ), + ) + : null, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 7)), + //if index is selected, then give the background a slight primary color + backgroundColor: MaterialStateProperty.all( + entry.value.color != null + ? entry.value.color!.withOpacity(0.2) + : theme.colorScheme.primary.withOpacity(0.1), + ), + textStyle: MaterialStateProperty.all( + BotStyle.text(context), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + onLongPress: + onLongPress != null ? () => onLongPress!(entry.key) : null, + onPressed: () => onPressed(entry.key), + child: Text( + entry.value.text, + style: BotStyle.text(context), + ), + ), + ), + ); + } catch (e) { + debugger(when: kDebugMode); + return Container(); + } + } +} diff --git a/lib/pangea/choreographer/widgets/counters.dart b/lib/pangea/choreographer/widgets/counters.dart new file mode 100644 index 000000000..3b5c5cf6f --- /dev/null +++ b/lib/pangea/choreographer/widgets/counters.dart @@ -0,0 +1,88 @@ +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../config/app_config.dart'; + +class Counter extends StatelessWidget { + final int count; + final String label; + final Color color; + const Counter({ + Key? key, + required this.count, + required this.label, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: label, + // textStyle: BotStyle.text(context, setColor: false), + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(50.0), + ), + child: Column( + children: [ + Text( + count.toString(), + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } +} + +class CounterDisplay extends StatelessWidget { + final int correct; + final int incorrect; + final int yellow; + final int custom; + const CounterDisplay({ + Key? key, + required this.correct, + required this.incorrect, + required this.yellow, + required this.custom, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Counter( + count: custom, + label: L10n.of(context)!.customInputFeedbackChoice, + // color: Theme.of(context).brightness == Brightness.dark + // ? AppConfig.primaryColorLight + // : AppConfig.primaryColor, + color: AppConfig.primaryColor, + ), + Counter( + count: correct, + label: L10n.of(context)!.greenFeedback, + color: ChoreoConstants.green, + ), + Counter( + color: ChoreoConstants.yellow, + label: L10n.of(context)!.yellowFeedback, + count: yellow, + ), + Counter( + count: incorrect, + label: L10n.of(context)!.redFeedback, + color: ChoreoConstants.red, + ), + ], + ); + } +} diff --git a/lib/pangea/choreographer/widgets/has_error_button.dart b/lib/pangea/choreographer/widgets/has_error_button.dart new file mode 100644 index 000000000..8c04bad77 --- /dev/null +++ b/lib/pangea/choreographer/widgets/has_error_button.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import '../../controllers/pangea_controller.dart'; +import '../controllers/error_service.dart'; + +class ChoreographerHasErrorButton extends StatelessWidget { + final ChoreoError error; + final PangeaController pangeaController; + + const ChoreographerHasErrorButton( + this.pangeaController, + this.error, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + onPressed: () { + if (error.type == ChoreoErrorType.unknown) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 5), + content: Text( + "${error.title(context)} ${error.description(context)}"), + ), + ); + } else if (error.type == ChoreoErrorType.unsubscribed) { + pangeaController.subscriptionController.showPaywall(context); + } + }, + mini: true, + child: Icon(error.icon), + ), + ); + } +} diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart new file mode 100644 index 000000000..8f1e8b050 --- /dev/null +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -0,0 +1,320 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart'; +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../config/app_config.dart'; +import '../../models/it_response_model.dart'; +import '../../utils/overlay.dart'; +import 'choice_array.dart'; + +class ITBar extends StatelessWidget { + final Choreographer choreographer; + const ITBar({Key? key, required this.choreographer}) : super(key: key); + + ITController get controller => choreographer.itController; + + @override + Widget build(BuildContext context) { + if (!controller.isOpen) return const SizedBox(); + + return CompositedTransformTarget( + link: choreographer.itBarLinkAndKey.link, + child: Container( + key: choreographer.itBarLinkAndKey.key, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Colors.black, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(AppConfig.borderRadius), + topRight: Radius.circular(AppConfig.borderRadius), + )), + width: double.infinity, + padding: const EdgeInsets.fromLTRB(0, 3, 3, 3), + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // // Row( + // // mainAxisAlignment: MainAxisAlignment.start, + // // crossAxisAlignment: CrossAxisAlignment.start, + // // children: [ + // // CounterDisplay( + // // correct: controller.correctChoices, + // // custom: controller.customChoices, + // // incorrect: controller.incorrectChoices, + // // yellow: controller.wildcardChoices, + // // ), + // // CompositedTransformTarget( + // // link: choreographer.itBotLayerLinkAndKey.link, + // // child: ITBotButton( + // // key: choreographer.itBotLayerLinkAndKey.key, + // // choreographer: choreographer, + // // ), + // // ), + // // ], + // // ), + // ITCloseButton(choreographer: choreographer), + // ], + // ), + // const SizedBox(height: 40.0), + OriginalText(controller: controller), + const SizedBox(height: 7.0), + IntrinsicHeight( + child: Container( + constraints: const BoxConstraints(minHeight: 80), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Center( + child: controller.choreographer.errorService.isError + ? ITError( + error: controller + .choreographer.errorService.error!, + controller: controller, + ) + : controller.showChoiceFeedback + ? ChoiceFeedbackText(controller: controller) + : controller.isTranslationDone + ? TranslationFeedback( + controller: controller) + : ITChoices(controller: controller), + ), + ), + ), + ], + ), + ), + Positioned( + top: 0.0, + right: 0.0, + child: ITCloseButton(choreographer: choreographer), + ), + ], + ), + ), + ); + } +} + +class ChoiceFeedbackText extends StatelessWidget { + const ChoiceFeedbackText({ + Key? key, + required this.controller, + }) : super(key: key); + + final ITController controller; + + @override + Widget build(BuildContext context) { + //reimplement if we decide we want it + return const SizedBox(); + // return AnimatedTextKit( + // isRepeatingAnimation: false, + // animatedTexts: [ + // ScaleAnimatedText( + // controller.latestChoiceFeedback(context), + // duration: Duration( + // milliseconds: + // (ChoreoConstants.millisecondsToDisplayFeedback / 2).round(), + // ), + // scalingFactor: 1.4, + // textStyle: BotStyle.text(context), + // ), + // ], + // ); + } +} + +class OriginalText extends StatelessWidget { + const OriginalText({ + Key? key, + required this.controller, + }) : super(key: key); + + final ITController controller; + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(minHeight: 50), + padding: const EdgeInsets.only(left: 60.0, right: 40.0), + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(AppConfig.borderRadius), + topRight: Radius.circular(AppConfig.borderRadius), + )), + child: Row( + //PTODO - does this already update after reset or we need to setState? + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!controller.isEditingSourceText) + controller.sourceText != null + ? Flexible(child: Text(controller.sourceText!)) + : const LinearProgressIndicator(), + if (controller.isEditingSourceText) + Expanded( + child: TextField( + controller: TextEditingController(text: controller.sourceText), + autofocus: true, + enableSuggestions: false, + maxLines: null, + textInputAction: TextInputAction.send, + onSubmitted: controller.onEditSourceTextSubmit, + obscureText: false, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ), + if (!controller.isEditingSourceText && controller.sourceText != null) + IconButton( + onPressed: () => controller.setIsEditingSourceText(true), + icon: const Icon(Icons.edit_outlined), + ), + ], + ), + ); + } +} + +class ITChoices extends StatelessWidget { + const ITChoices({ + Key? key, + required this.controller, + }) : super(key: key); + + // final choices = [ + // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", + // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", + // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", + // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", + // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", + // "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.", + // ]; + + final ITController controller; + + String? get sourceText { + if ((controller.sourceText == null || controller.sourceText!.isEmpty)) { + ErrorHandler.logError(m: "null source text in ItChoices"); + } + return controller.sourceText; + } + + void showCard(BuildContext context, int index, + [Color? borderColor, String? choiceFeedback]) => + OverlayUtil.showPositionedCard( + context: context, + cardToShow: WordDataCard( + word: controller.currentITStep!.continuances[index].text, + wordLang: controller.targetLangCode, + fullText: sourceText ?? controller.choreographer.currentText, + fullTextLang: sourceText != null + ? controller.sourceLangCode + : controller.targetLangCode, + hasInfo: controller.currentITStep!.continuances[index].hasInfo, + choiceFeedback: choiceFeedback, + room: controller.choreographer.chatController.room), + cardSize: const Size(300, 300), + borderColor: borderColor, + transformTargetId: controller.choreographer.itBarTransformTargetKey, + backDropToDismiss: false, + ); + + @override + Widget build(BuildContext context) { + try { + if (controller.isEditingSourceText || controller.currentITStep == null) { + return const SizedBox(); + } + return ChoicesArray( + isLoading: controller.isLoading || + controller.choreographer.isFetching || + controller.currentITStep == null, + //TODO - pass current span being translated + originalSpan: "dummy", + choices: controller.currentITStep!.continuances.map((e) { + try { + return Choice(text: e.text.trim(), color: e.color); + } catch (e) { + debugger(when: kDebugMode); + return Choice(text: "error", color: Colors.red); + } + }).toList(), + onPressed: (int index) { + final Continuance continuance = + controller.currentITStep!.continuances[index]; + debugPrint("is gold? ${continuance.gold}"); + if (continuance.level == 1 || continuance.wasClicked) { + Future.delayed(const Duration(milliseconds: 500), + () => controller.selectTranslation(index)); + } else { + showCard( + context, + index, + continuance.level == 2 + ? ChoreoConstants.yellow + : ChoreoConstants.red, + continuance.feedbackText(context), + ); + } + controller.currentITStep!.continuances[index].wasClicked = true; + controller.choreographer.setState(); + }, + onLongPress: (int index) { + showCard(context, index); + }, + uniqueKeyForLayerLink: (int index) => "itChoices$index", + selectedChoiceIndex: null, + ); + } catch (e) { + debugger(when: kDebugMode); + return const SizedBox(); + } + } +} + +class ITError extends StatelessWidget { + final ITController controller; + final Object error; + const ITError({Key? key, required this.error, required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final ErrorCopy errorCopy = ErrorCopy(context, error); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: Text( + // Text( + "${errorCopy.title}\n${errorCopy.body}", + // Haga clic en su mensaje para ver los significados de las palabras. + style: TextStyle( + fontStyle: FontStyle.italic, + color: Theme.of(context).colorScheme.error), + ), + ), + ITRestartButton(controller: controller), + ], + ), + ); + } +} diff --git a/lib/pangea/choreographer/widgets/it_bar_buttons.dart b/lib/pangea/choreographer/widgets/it_bar_buttons.dart new file mode 100644 index 000000000..27fbb3762 --- /dev/null +++ b/lib/pangea/choreographer/widgets/it_bar_buttons.dart @@ -0,0 +1,79 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/utils/instructions.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +import '../../widgets/common/bot_face_svg.dart'; +import '../controllers/choreographer.dart'; +import '../controllers/it_controller.dart'; + +class ITCloseButton extends StatelessWidget { + const ITCloseButton({ + Key? key, + required this.choreographer, + }) : super(key: key); + + final Choreographer choreographer; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () { + if (choreographer.itController.isEditingSourceText) { + choreographer.itController.setIsEditingSourceText(false); + } else { + choreographer.itController.closeIT(); + } + }, + ); + } +} + +class ITBotButton extends StatelessWidget { + const ITBotButton({Key? key, required this.choreographer}) : super(key: key); + + final Choreographer choreographer; + + @override + Widget build(BuildContext context) { + choreographer.pangeaController.instructions.show( + context, + InstructionsEnum.itInstructions, + choreographer.itBotTransformTargetKey, + true, + ); + + return IconButton( + icon: const BotFace(width: 40.0, expression: BotExpression.right), + onPressed: () => choreographer.pangeaController.instructions.show( + context, + InstructionsEnum.itInstructions, + choreographer.itBotTransformTargetKey, + false, + ), + ); + } +} + +class ITRestartButton extends StatelessWidget { + ITRestartButton({ + Key? key, + required this.controller, + }) : super(key: key); + + final ITController controller; + final PangeaController pangeaController = MatrixState.pangeaController; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () async { + controller.choreographer.errorService.resetError(); + controller.currentITStep = null; + controller.choreographer.getLanguageHelp(); + }, + icon: const Icon(Icons.refresh_outlined), + ); + } +} diff --git a/lib/pangea/choreographer/widgets/it_shimmer.dart b/lib/pangea/choreographer/widgets/it_shimmer.dart new file mode 100644 index 000000000..adbe265b0 --- /dev/null +++ b/lib/pangea/choreographer/widgets/it_shimmer.dart @@ -0,0 +1,80 @@ +import 'dart:ui'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; + +class ItShimmer extends StatelessWidget { + const ItShimmer({Key? key, required this.originalSpan}) : super(key: key); + + final String originalSpan; + + Iterable renderShimmerIfListEmpty(BuildContext context, + {int noOfBars = 3}) { + final List dummyStrings = []; + for (int i = 0; i < noOfBars; i++) { + dummyStrings.add(originalSpan); + } + return dummyStrings.map((e) => ITShimmerElement( + text: e, + )); + } + + // PTODO - bring this back, make it shimmer + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.center, + children: [...renderShimmerIfListEmpty(context, noOfBars: 3)], + ); + } +} + +class ITShimmerElement extends StatelessWidget { + const ITShimmerElement({ + Key? key, + required this.text, + }) : super(key: key); + + final String text; + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(minWidth: 50), + margin: const EdgeInsets.all(2), + padding: EdgeInsets.zero, + // decoration: BoxDecoration( + // borderRadius: const BorderRadius.all(Radius.circular(10)), + // border: Border.all( + // color: Theme.of(context).colorScheme.primary, + // style: BorderStyle.solid, + // width: 2.0, + // ), + // ), + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 7)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + backgroundColor: MaterialStateProperty.all( + AppConfig.primaryColor.withOpacity(0.2)), + ), + onPressed: () {}, + child: Text( + text, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.transparent), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/choreographer/widgets/language_display_toggle.dart b/lib/pangea/choreographer/widgets/language_display_toggle.dart new file mode 100644 index 000000000..ce32fbd02 --- /dev/null +++ b/lib/pangea/choreographer/widgets/language_display_toggle.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../config/app_config.dart'; +import '../../../pages/chat/chat.dart'; + +class LanguageDisplayToggle extends StatelessWidget { + const LanguageDisplayToggle({ + Key? key, + required this.controller, + }) : super(key: key); + + final ChatController controller; + + get onPressed => + controller.choreographer.messageOptions.toggleSelectedDisplayLang; + + @override + Widget build(BuildContext context) { + if (!controller.choreographer.translationEnabled) { + return const SizedBox(); + } + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: controller.choreographer.messageOptions.isTranslationOn + ? AppConfig.primaryColor + : null, + ), + child: IconButton( + tooltip: L10n.of(context)!.toggleLanguages, + onPressed: onPressed, + icon: const Icon(Icons.translate_outlined), + selectedIcon: const Icon(Icons.translate), + isSelected: controller.choreographer.messageOptions.isTranslationOn, + ), + ); + // return Tooltip( + // message: L10n.of(context)!.toggleLanguages, + // waitDuration: const Duration(milliseconds: 1000), + // child: FloatingActionButton( + // onPressed: onPressed, + // backgroundColor: Colors.white, + // mini: false, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(200), // <-- Radius + // ), + // child: LanguageFlag( + // flagUrl: controller + // .choreographer.messageOptions.displayLang?.languageFlag, + // size: 50, + // ), + // ), + // ); + } +} diff --git a/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart b/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart new file mode 100644 index 000000000..8204bde7c --- /dev/null +++ b/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart @@ -0,0 +1,148 @@ +import 'dart:developer'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../widgets/matrix.dart'; + +class _ErrorCopy { + final String title; + final String? description; + + _ErrorCopy(this.title, [this.description]); +} + +class LanguagePermissionsButtons extends StatelessWidget { + final String? roomID; + final Choreographer choreographer; + + const LanguagePermissionsButtons({ + Key? key, + required this.roomID, + required this.choreographer, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (roomID == null) return const SizedBox.shrink(); + final _ErrorCopy? copy = getCopy(context); + if (copy == null) return const SizedBox.shrink(); + + final Widget text = RichText( + text: TextSpan( + children: [ + TextSpan( + text: copy.title, + style: TextStyle( + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Colors.black, + ), + ), + if (copy.description != null) + TextSpan( + text: copy.description, + style: const TextStyle(color: AppConfig.primaryColor), + recognizer: TapGestureRecognizer() + ..onTap = () => context.go('/rooms/settings/learning'), + ), + ], + ), + ); + + return Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: FloatingActionButton( + mini: true, + child: const Icon(Icons.history_edu_outlined), + onPressed: () => showMessage(context, text), + ), + ); + } + + _ErrorCopy? getCopy(BuildContext context) { + final bool itDisabled = !choreographer.itEnabled; + final bool igcDisabled = !choreographer.igcEnabled; + final Room? room = Matrix.of(context).client.getRoomById(roomID!); + + final bool itDisabledByClass = choreographer + .pangeaController.permissionsController + .isToolDisabledByClass(ToolSetting.interactiveTranslator, room); + final bool igcDisabledByClass = choreographer + .pangeaController.permissionsController + .isToolDisabledByClass(ToolSetting.interactiveGrammar, room); + + if (itDisabledByClass && igcDisabledByClass) { + return _ErrorCopy( + L10n.of(context)!.errorDisableLanguageAssistanceClassDesc, + ); + } + + if (itDisabledByClass) { + if (igcDisabled) { + return _ErrorCopy( + "{L10n.of(context)!.errorDisableITClassDesc} ${L10n.of(context)!.errorDisableIGC}", + " ${L10n.of(context)!.errorDisableIGCUserDesc}", + ); + } else { + return _ErrorCopy(L10n.of(context)!.errorDisableITClassDesc); + } + } + + if (igcDisabledByClass) { + if (itDisabled) { + return _ErrorCopy( + "${L10n.of(context)!.errorDisableIGCClassDesc} ${L10n.of(context)!.errorDisableIT}", + " ${L10n.of(context)!.errorDisableITUserDesc}", + ); + } else { + return _ErrorCopy(L10n.of(context)!.errorDisableIGCClassDesc); + } + } + + if (igcDisabled && itDisabled) { + return _ErrorCopy( + L10n.of(context)!.errorDisableLanguageAssistance, + " ${L10n.of(context)!.errorDisableLanguageAssistanceUserDesc}", + ); + } + + if (itDisabled) { + return _ErrorCopy( + L10n.of(context)!.errorDisableIT, + " ${L10n.of(context)!.errorDisableITUserDesc}", + ); + } + + if (igcDisabled) { + return _ErrorCopy( + L10n.of(context)!.errorDisableIGC, + " ${L10n.of(context)!.errorDisableIGCUserDesc}", + ); + } + + debugger(when: kDebugMode); + ErrorHandler.logError( + e: Exception("Unhandled case in language permissions"), + ); + return null; + } + + void showMessage(BuildContext context, Widget text) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 10), + content: text, + ), + ); + } +} diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart new file mode 100644 index 000000000..73c39ebf2 --- /dev/null +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/pangea/constants/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../pages/chat/chat.dart'; + +class ChoreographerSendButton extends StatelessWidget { + const ChoreographerSendButton({ + Key? key, + required this.controller, + }) : super(key: key); + + final ChatController controller; + + @override + Widget build(BuildContext context) { + // commit for cicd + return controller.choreographer.isFetching + ? Container( + height: 56, + width: 56, + padding: const EdgeInsets.all(13), + child: const CircularProgressIndicator(), + ) + : Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + icon: const Icon(Icons.send_outlined), + color: controller.choreographer.igc.canSendMessage + ? null + : PangeaColors.igcError, + onPressed: () { + controller.choreographer.send(context); + }, + tooltip: L10n.of(context)!.send, + ), + ); + } +} diff --git a/lib/pangea/choreographer/widgets/translation_finished_flow.dart b/lib/pangea/choreographer/widgets/translation_finished_flow.dart new file mode 100644 index 000000000..b63424565 --- /dev/null +++ b/lib/pangea/choreographer/widgets/translation_finished_flow.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +import '../../utils/bot_style.dart'; +import '../../utils/error_handler.dart'; +import '../controllers/it_controller.dart'; +import 'choice_array.dart'; + +class TranslationFeedback extends StatelessWidget { + final ITController controller; + const TranslationFeedback({Key? key, required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + String feedbackText; + TextStyle? style; + try { + feedbackText = + controller.choreographer.altTranslator.translationFeedback(context); + style = BotStyle.text(context); + } catch (err, stack) { + feedbackText = "Nice job!"; + style = null; + debugPrint("error getting copy and styles"); + ErrorHandler.logError(e: err, s: stack); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + if (controller.choreographer.altTranslator.showTranslationFeedback) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "$feedbackText ", + style: style, + ), + ], + ), + ), + ), + const SizedBox(height: 6), + if (controller + .choreographer.altTranslator.showAlternativeTranslations) + AlternativeTranslations(controller: controller), + // if (!controller + // .choreographer.altTranslator.showAlternativeTranslations && + // !controller.choreographer.isFetching) + // ITRestartButton(controller: controller), + ], + ), + ); + } +} + +class AlternativeTranslations extends StatelessWidget { + const AlternativeTranslations({ + Key? key, + required this.controller, + }) : super(key: key); + + final ITController controller; + + @override + Widget build(BuildContext context) { + return ChoicesArray( + originalSpan: controller.choreographer.itController.sourceText ?? "dummy", + isLoading: + controller.choreographer.altTranslator.loadingAlternativeTranslations, + // choices: controller.choreographer.altTranslator.similarityResponse.scores + choices: [ + Choice(text: controller.choreographer.altTranslator.translations.first) + ], + // choices: controller.choreographer.altTranslator.translations, + onPressed: (int index) { + controller.choreographer.onSelectAlternativeTranslation( + controller.choreographer.altTranslator.translations[index]); + }, + uniqueKeyForLayerLink: (int index) => "altTranslation$index", + selectedChoiceIndex: null, + ); + } +} diff --git a/lib/pangea/config/colors.dart b/lib/pangea/config/colors.dart new file mode 100644 index 000000000..43a3f2e23 --- /dev/null +++ b/lib/pangea/config/colors.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class ChoreoColor { + static Color containerBG(BuildContext context) => + Theme.of(context).scaffoldBackgroundColor; + static Color textColor(BuildContext context) => + isLightMode(context) ? Colors.black : Colors.white; + static Color disabled(context) => const Color.fromARGB(255, 211, 211, 211); + static Color removeButtonColor(context) => + Theme.of(context).colorScheme.primary; + static bool isLightMode(BuildContext context) => + Theme.of(context).brightness == Brightness.light ? true : false; +} diff --git a/lib/pangea/config/environment.dart b/lib/pangea/config/environment.dart new file mode 100644 index 000000000..878c591fe --- /dev/null +++ b/lib/pangea/config/environment.dart @@ -0,0 +1,91 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import '../../utils/platform_infos.dart'; + +class Environment { + static bool get itIsTime => + DateTime.utc(2023, 1, 25).isBefore(DateTime.now()); + + static String get fileName { + // return '.env.prod'; + if (kIsWeb) { + return '.env'; + } + if (PlatformInfos.isMobile) { + if (kDebugMode) { + return '.env'; + } + return '.env.prod'; + } + return ".env"; + } + + static bool get isStaging => synapsURL.contains("staging"); + + static String get baseAPI { + return dotenv.env["BASE_API"] ?? 'BASE API not found'; + } + + static String get frontendURL { + return dotenv.env["FRONTEND_URL"] ?? "Frontend URL NOT FOUND"; + } + + static String get synapsURL { + return dotenv.env['SYNAPSE_URL'] ?? 'Synapse Url not found'; + } + + static String get homeServer { + return dotenv.env["HOME_SERVER"] ?? 'Home Server not found'; + } + + static String get choreoApi { + // return "http://localhost:8000/choreo"; + return dotenv.env['CHOREO_API'] ?? 'Not found'; + } + + static String get choreoApiKey { + return dotenv.env['CHOREO_API_KEY'] ?? + 'e6fa9fa97031ba0c852efe78457922f278a2fbc109752fe18e465337699e9873'; + } + + //Question for Jordan - does the client ever pass this to the server? + static String get googleAuthKey { + return dotenv.env['GOOGLE_AUTH_KEY'] ?? + '466850640825-qegdiq3mpj3h5e0e79ud5hnnq2c22mi3.apps.googleusercontent.com'; + } + + static String get sentryDsn { + return dotenv.env["SENTRY_DSN"] ?? + 'https://c2fd19ab2cdc4ebb939a32d01c0e9fa1@o225078.ingest.sentry.io/1376295'; + } + + static String get rcProjectId { + return dotenv.env["RC_PROJECT"] ?? 'a499dc21'; + } + + static String get rcKey { + return dotenv.env["RC_KEY"] ?? 'sk_eVGBdPyInaOfJrKlPBgFVnRynqKJB'; + } + + static String get rcGoogleKey { + return dotenv.env["RC_GOOGLE_KEY"] ?? 'goog_paQMrzFKGzuWZvcMTPkkvIsifJe'; + } + + static String get rcIosKey { + return dotenv.env["RC_IOS_KEY"] ?? 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv'; + } + + static String get rcStripeKey { + return dotenv.env["RC_STRIPE_KEY"] ?? 'strp_YWZxWUeEfvagiefDNoofinaRCOl'; + } + + static String get rcOfferingName { + return dotenv.env["RC_OFFERING_NAME"] ?? 'default'; + } + + static String get stripeManagementUrl { + return dotenv.env["STRIPE_MANAGEMENT_LINK"] ?? + 'https://billing.stripe.com/p/login/dR6dSkf5p6rBc4EcMM'; + } +} diff --git a/lib/pangea/constants/age_limits.dart b/lib/pangea/constants/age_limits.dart new file mode 100644 index 000000000..9a07840ef --- /dev/null +++ b/lib/pangea/constants/age_limits.dart @@ -0,0 +1,4 @@ +class AgeLimits { + static const int toAccessFeatures = 18; + static const int toUseTheApp = 13; +} diff --git a/lib/pangea/constants/choreo_constants.dart b/lib/pangea/constants/choreo_constants.dart new file mode 100644 index 000000000..cbdfdf7d0 --- /dev/null +++ b/lib/pangea/constants/choreo_constants.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class ChoreoConstants { + static const numberOfITChoices = 4; + // static const millisecondsToDisplayFeedback = 2500; + static const millisecondsToDisplayFeedback = 0; + static const commonalityThreshold = 0.9; + static const levelThresholdForGreen = 1; + static const levelThresholdForYellow = 2; + static const levelThresholdForRed = 3; + static const green = Colors.green; + static const yellow = Color.fromARGB(255, 206, 152, 2); + static const red = Colors.red; +} diff --git a/lib/pangea/constants/class_default_values.dart b/lib/pangea/constants/class_default_values.dart new file mode 100644 index 000000000..97522ca4b --- /dev/null +++ b/lib/pangea/constants/class_default_values.dart @@ -0,0 +1,13 @@ +import '../enum/time_span.dart'; + +class ClassDefaultValues { + static const pangeaClassRoomIdDefault = "PANGEA_CLASS_ROOM_ID_DEFAULT"; + static const powerLevelOfAdmin = 100; + static const languageToolPermissions = 1; + static const minutesDelayToUpdateMyAnalytics = -1; + static const minutesDelayToMakeNewChartAnalytics = 1; + static const maxClassName = 40; + static const defaultDominantLanguage = "en"; + static const defaultTargetLanguage = "es"; + static const defaultTimeSpan = TimeSpan.month; +} diff --git a/lib/pangea/constants/colors.dart b/lib/pangea/constants/colors.dart new file mode 100644 index 000000000..16fc7f907 --- /dev/null +++ b/lib/pangea/constants/colors.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +class PangeaColors { + static const igcError = Colors.red; +} diff --git a/lib/pangea/constants/keys.dart b/lib/pangea/constants/keys.dart new file mode 100644 index 000000000..092b1b0a9 --- /dev/null +++ b/lib/pangea/constants/keys.dart @@ -0,0 +1,4 @@ +class PrefKey { + static const lastFetched = 'LAST_FETCHED'; + static const flags = 'flags'; +} diff --git a/lib/pangea/constants/language_keys.dart b/lib/pangea/constants/language_keys.dart new file mode 100644 index 000000000..cfe0c96c0 --- /dev/null +++ b/lib/pangea/constants/language_keys.dart @@ -0,0 +1,6 @@ +class LanguageKeys { + static const unknownLanguage = "unk"; + static const mixedLanguage = "mixed"; + static const defaultLanguage = "en"; + static const multiLanguage = "multi"; +} diff --git a/lib/pangea/constants/language_level_type.dart b/lib/pangea/constants/language_level_type.dart new file mode 100644 index 000000000..49136ca77 --- /dev/null +++ b/lib/pangea/constants/language_level_type.dart @@ -0,0 +1,3 @@ +class LanguageLevelType { + static List get allInts => [0, 1, 2, 3, 4, 5, 6]; +} diff --git a/lib/pangea/constants/language_list_keys.dart b/lib/pangea/constants/language_list_keys.dart new file mode 100644 index 000000000..7fff924a9 --- /dev/null +++ b/lib/pangea/constants/language_list_keys.dart @@ -0,0 +1,4 @@ +class PrefKey { + static const lastFetched = 'p_lang_lastfetched'; + static const flags = 'p_lang_flag'; +} diff --git a/lib/pangea/constants/local.key.dart b/lib/pangea/constants/local.key.dart new file mode 100644 index 000000000..238a3ecc9 --- /dev/null +++ b/lib/pangea/constants/local.key.dart @@ -0,0 +1,7 @@ +class PLocalKey { + static const String user = 'user'; + + static const String classes = 'classes'; + + static const String cachedClassCodeToJoin = "cachedclasscodetojoin"; +} diff --git a/lib/pangea/constants/match_rule_ids.dart b/lib/pangea/constants/match_rule_ids.dart new file mode 100644 index 000000000..7f3a924de --- /dev/null +++ b/lib/pangea/constants/match_rule_ids.dart @@ -0,0 +1,6 @@ +class MatchRuleIds { + static const interactiveTranslation = "interactive_translation"; + static const tokenNeedsTranslation = "token_needs_translation"; + static const tokenSpanNeedsTranslation = "token_span_needs_translation"; + static const l1SpanAndGrammar = "l1_span_and_grammar"; +} diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart new file mode 100644 index 000000000..66f9d24a8 --- /dev/null +++ b/lib/pangea/constants/model_keys.dart @@ -0,0 +1,79 @@ +class ModelKey { + ///user model keys + static const String userAccess = 'access'; + static const String userRefresh = 'refresh'; + static const String userProfile = 'profile'; + static const String userFullName = 'full_name'; + static const String userCreatedAt = 'created_at'; + static const String userPangeaUserId = 'pangea_user_id'; + static const String userDateOfBirth = 'date_of_birth'; + static const String userTargetLanguage = 'target_language'; + static const String userSourceLanguage = 'source_language'; + static const String userSpeaks = 'speaks'; + static const String userCountry = 'country'; + static const String userInterests = 'interests'; + static const String l2LanguageKey = 'target_language'; + static const String l1LanguageKey = 'source_language'; + static const String publicProfile = 'public'; + static const String userId = 'user_id'; + + static const String clientClassCity = "city"; + static const String clientClassCountry = "country"; + static const String clientClassDominantLanguage = "dominantLanguage"; + static const String clientClassTargetLanguage = "targetLanguage"; + static const String clientClassDescription = "description"; + static const String clientLanguageLevel = "languageLevel"; + static const String clientSchool = "schoolName"; + + static const String clientIsPublic = "isPublic"; + static const String clientIsOpenEnrollment = 'isOpenEnrollment'; + static const String clientIsOpenExchange = 'isOpenExchange'; + static const String clientIsOneToOneChatClass = 'oneToOneChatClass'; + static const String clientIsOneToOneChatExchange = 'oneToOneChatExchange'; + static const String clientIsCreateRooms = 'isCreateRooms'; + static const String clientIsCreateRoomsExchange = 'isCreateRoomsExchange'; + static const String clientIsShareVideo = 'isShareVideo'; + static const String clientIsSharePhoto = 'isSharePhoto'; + static const String clientIsShareFiles = 'isShareFiles'; + static const String clientIsShareLocation = 'isShareLocation'; + static const String clientIsCreateStories = 'isCreateStories'; + static const String clientIsVoiceNotes = 'isVoiceNotes'; + static const String clientIsInviteOnlyStudents = 'isInviteOnlyStudents'; + static const String clientIsInviteOnlyExchanges = 'isInviteOnlyExchanges'; + + static const String userL1 = "user_l1"; + static const String userL2 = "user_l2"; + static const String fullText = "full_text"; + static const String fullTextLang = "full_text_lang"; + static const String tokens = "tokens"; + static const String srcLang = "src_lang"; + static const String tgtLang = "tgt_lang"; + static const String word = "word"; + static const String lang = "lang"; + static const String deepL = "deepl"; + static const String langCode = 'langCode'; + static const String wordLang = "word_lang"; + static const String lemma = "lemma"; + static const String saveVocab = "save_vocab"; + static const String text = "text"; + static const String permissions = "permissions"; + static const String enableIGC = "enable_igc"; + static const String enableIT = "enable_it"; + + static const String originalSent = "original_sent"; + static const String originalWritten = "original_written"; + static const String tokensSent = "tokens_sent"; + static const String tokensWritten = "tokens_written"; + static const String choreoRecord = "choreo_record"; + static const String useType = "use_type"; + + static const String baseDefinition = "base_definition"; + static const String targetDefinition = "target_definition"; + static const String basePartOfSpeech = "base_part_of_speech"; + static const String targetPartOfSpeech = "target_part_of_speech"; + static const String partOfSpeech = "part_of_speech"; + static const String baseWord = "base_word"; + static const String targetWord = "target_word"; + static const String baseExampleSentence = "base_example_sentence"; + static const String targetExampleSentence = "target_example_sentence"; +} diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart new file mode 100644 index 000000000..68e849c88 --- /dev/null +++ b/lib/pangea/constants/pangea_event_types.dart @@ -0,0 +1,16 @@ +class PangeaEventTypes { + static const classSettings = "pangea.class"; + static const pangeaExchange = "p.exchange"; + + static const rules = "p.rules"; + + static const studentAnalyticsSummary = "pangea.usranalytics"; + + static const translation = "pangea.translation"; + static const tokens = "pangea.tokens"; + static const choreoRecord = "pangea.record"; + static const representation = "pangea.representation"; + + static const vocab = "p.vocab"; + static const roomInfo = "pangea.roomtopic"; +} diff --git a/lib/pangea/constants/pangea_message_types.dart b/lib/pangea/constants/pangea_message_types.dart new file mode 100644 index 000000000..23ee52abd --- /dev/null +++ b/lib/pangea/constants/pangea_message_types.dart @@ -0,0 +1,3 @@ +class PangeaMessageTypes { + static String report = 'm.report'; +} diff --git a/lib/pangea/constants/pangea_room_types.dart b/lib/pangea/constants/pangea_room_types.dart new file mode 100644 index 000000000..dcceadd36 --- /dev/null +++ b/lib/pangea/constants/pangea_room_types.dart @@ -0,0 +1,4 @@ +class PangeaRoomTypes { + static const analytics = 'p.analytics'; + static const exchange = 'p.exchange'; +} diff --git a/lib/pangea/constants/url_query_parameter_keys.dart b/lib/pangea/constants/url_query_parameter_keys.dart new file mode 100644 index 000000000..3f5c468db --- /dev/null +++ b/lib/pangea/constants/url_query_parameter_keys.dart @@ -0,0 +1,3 @@ +class UrlQueryParameterKeys { + static const String classCode = 'classcode'; +} diff --git a/lib/pangea/controllers/base_controller.dart b/lib/pangea/controllers/base_controller.dart new file mode 100644 index 000000000..ce41353e8 --- /dev/null +++ b/lib/pangea/controllers/base_controller.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +class BaseController { + final StreamController stateListener = StreamController(); + late Stream stateStream; + + BaseController() { + stateStream = stateListener.stream.asBroadcastStream(); + } + + dispose() { + stateListener.close(); + } + + setState({dynamic data}) { + stateListener.add(data); + } +} diff --git a/lib/pangea/controllers/class_controller.dart b/lib/pangea/controllers/class_controller.dart new file mode 100644 index 000000000..00c083e34 --- /dev/null +++ b/lib/pangea/controllers/class_controller.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/local.key.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/class_code.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import '../utils/bot_name.dart'; +import '../utils/firebase_analytics.dart'; +import 'base_controller.dart'; + +class ClassController extends BaseController { + late PangeaController _pangeaController; + + ClassController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + setActiveSpaceIdInChatListController(String classId) { + setState(data: {"activeSpaceId": classId}); + } + + Future fixClassPowerLevels() async { + try { + final List> classFixes = []; + for (final room in _pangeaController + .matrixState.client.classesAndExchangesImTeaching) { + classFixes.add(room.setClassPowerlLevels()); + } + await Future.wait(classFixes); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + void checkForClassCodeAndSubscription(BuildContext context) { + final String? classCode = _pangeaController.pStoreService.read( + PLocalKey.cachedClassCodeToJoin, + addClientIdToKey: false, + ); + + if (classCode != null) { + _pangeaController.pStoreService.delete( + PLocalKey.cachedClassCodeToJoin, + addClientIdToKey: false, + ); + joinClasswithCode( + context, + classCode, + ).onError( + (error, stackTrace) => + ClassCodeUtil.messageSnack(context, ErrorCopy(context, error).body), + ); + } else { + try { + //question for gabby: why do we need this in two places? + if (!_pangeaController.subscriptionController.isSubscribed) { + _pangeaController.subscriptionController.showPaywall(context); + } + } catch (err) { + debugger(when: kDebugMode); + } + } + } + + /// if not bot chat return + /// if bot chat, get pangeaClassContext + /// for all classes not in pangeaClassContext, add bot chat to that class + /// PTODO - add analytics bot to all chats and have that do this work + Future> addDirectChatsToClasses(Room room) async { + if (!room.isDirectChat) return []; + final List existingParentsIds = + room.pangeaSpaceParents.map((e) => e.id).toList(); + final List spaces = + _pangeaController.matrixState.client.classesAndExchangesImIn; + + //make sure we have the latest participants + await Future.wait(spaces.map((e) => e.requestParticipants())); + + //get spaces where, + //other chat participant is the bot OR is in the space AND the chat is not + final List spacesToAdd = spaces + .where( + (s) => + (room.directChatMatrixID == BotName.byEnvironment || + s + .getParticipants() + .map( + (u) => u.id, + ) + .contains(room.directChatMatrixID)) && + !existingParentsIds.contains(s.id), + ) + .toList(); + + //set the space child for each space + return Future.wait( + spacesToAdd.map((s) => s.setSpaceChild(room.id, suggested: true)), + ).then((value) => spaces); + } + + Future joinClasswithCode(BuildContext context, String classCode) async { + final QueryPublicRoomsResponse queryPublicRoomsResponse = + await Matrix.of(context).client.queryPublicRooms( + limit: 1, + filter: PublicRoomQueryFilter(genericSearchTerm: classCode), + ); + + final PublicRoomsChunk? classChunk = + queryPublicRoomsResponse.chunk.firstWhereOrNull((element) { + return element.canonicalAlias?.replaceAll("#", "").split(":")[0] == + classCode; + }); + + if (classChunk == null) { + ClassCodeUtil.messageSnack(context, L10n.of(context)!.unableToFindClass); + return; + } + + if (Matrix.of(context) + .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); + + GoogleAnalytics.joinClass(classCode); + + ClassCodeUtil.messageSnack( + context, + L10n.of(context)!.welcomeToYourNewClass, + ); + + context.go("/rooms"); + return; + // P-EPIC + // prereq - server needs ability to invite to private room. how? + // does server api have ability with admin token? + // is application service needed? + // BE - check class code and if class code is correct, invite student to room + // FE - look for invite from room and automatically accept + } +} diff --git a/lib/pangea/controllers/contextual_definition_controller.dart b/lib/pangea/controllers/contextual_definition_controller.dart new file mode 100644 index 000000000..fabca513a --- /dev/null +++ b/lib/pangea/controllers/contextual_definition_controller.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +import '../constants/model_keys.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; +import 'pangea_controller.dart'; + +class ContextualDefinitionController { + late PangeaController _pangeaController; + + final List<_ContextualDefinitionCacheItem> _definitions = []; + + ContextualDefinitionController(PangeaController pangeaController) { + _pangeaController = pangeaController; + } + + _ContextualDefinitionCacheItem? _getLocal( + ContextualDefinitionRequestModel req) => + _definitions.firstWhereOrNull( + (e) => e.word == req.word && e.fullText == req.fullText); + + Future get( + ContextualDefinitionRequestModel req) { + final _ContextualDefinitionCacheItem? localItem = _getLocal(req); + + if (localItem != null) return localItem.data; + + _definitions.add( + _ContextualDefinitionCacheItem( + word: req.word, + fullText: req.fullText, + data: _define(req), + ), + ); + + return _definitions.last.data; + } + + Future _define( + ContextualDefinitionRequestModel request, + ) async { + try { + final ContextualDefinitionResponseModel res = + await _ContextualDefinitionRepo.define( + await _pangeaController.userController.accessToken, + request, + ); + return res; + } catch (err, stack) { + debugPrint( + "error getting contextual definition for ${request.word} in '${request.fullText}'"); + ErrorHandler.logError(e: err, s: stack, data: request.toJson()); + return null; + } + } +} + +class _ContextualDefinitionCacheItem { + String word; + String fullText; + Future data; + + _ContextualDefinitionCacheItem({ + required this.word, + required this.fullText, + required this.data, + }); +} + +class _ContextualDefinitionRepo { + static Future define( + String accessToken, + ContextualDefinitionRequestModel request, + ) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.contextualDefinition, + body: request.toJson(), + ); + + final ContextualDefinitionResponseModel response = + ContextualDefinitionResponseModel.fromJson( + jsonDecode( + utf8.decode(res.bodyBytes).toString(), + ), + ); + + if (response.text.isEmpty) { + ErrorHandler.logError( + e: Exception( + "empty text in contextual definition response", + ), + ); + } + + return response; + } +} + +class ContextualDefinitionRequestModel { + final String fullText; + final String word; + final String feedbackLang; + final String fullTextLang; + final String wordLang; + + ContextualDefinitionRequestModel( + {required this.fullText, + required this.word, + required this.feedbackLang, + required this.fullTextLang, + required this.wordLang}); + + Map toJson() => { + ModelKey.fullText: fullText, + ModelKey.word: word, + ModelKey.lang: feedbackLang, + ModelKey.fullTextLang: fullTextLang, + ModelKey.wordLang: wordLang + }; +} + +class ContextualDefinitionResponseModel { + String text; + + ContextualDefinitionResponseModel({required this.text}); + + factory ContextualDefinitionResponseModel.fromJson( + Map json, + ) => + ContextualDefinitionResponseModel(text: json["response"]); +} diff --git a/lib/pangea/controllers/language_controller.dart b/lib/pangea/controllers/language_controller.dart new file mode 100644 index 000000000..16d1fc6fb --- /dev/null +++ b/lib/pangea/controllers/language_controller.dart @@ -0,0 +1,109 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../widgets/user_settings/p_language_dialog.dart'; + +class LanguageController { + late PangeaController _pangeaController; + + LanguageController(PangeaController pangeaController) { + _pangeaController = pangeaController; + } + //show diloag when user does not have languages selected + showDialogOnEmptyLanguage(BuildContext dialogContext, Function callback) { + if (_pangeaController.userController.userModel?.profile == null) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: 'calling showDialogOnEmptyLanguagae with empty user', + ), + ); + return; + } + if (_userL1Code == null || + _userL2Code == null || + _userL1Code!.isEmpty || + _userL2Code!.isEmpty || + _userL1Code == LanguageKeys.unknownLanguage || + _userL2Code == LanguageKeys.unknownLanguage) { + pLanguageDialog(dialogContext, callback); + } + } + + String? get _userL1Code { + final source = + _pangeaController.userController.userModel?.profile?.sourceLanguage; + return source == null || source.isEmpty ? null : source; + } + + String? get _userL2Code { + final target = + _pangeaController.userController.userModel?.profile?.targetLanguage; + return target == null || target.isEmpty ? null : target; + } + + LanguageModel? get userL1 { + return _userL1Code != null ? PangeaLanguage.byLangCode(_userL1Code!) : null; + } + + LanguageModel? get userL2 { + return _userL2Code != null ? PangeaLanguage.byLangCode(_userL2Code!) : null; + } + + String? activeL1Code({String? roomID}) { + final String? activeL2 = activeL2Code(roomID: roomID); + if (roomID == null || activeL2 != _userL1Code) { + return _userL1Code; + } + final ClassSettingsModel? classContext = _pangeaController + .matrixState.client + .getRoomById(roomID) + ?.firstLanguageSettings; + final String? classL1 = classContext?.dominantLanguage; + if (classL1 == LanguageKeys.mixedLanguage || + classL1 == LanguageKeys.multiLanguage || + classL1 == null) { + if (_userL2Code != _userL1Code) { + return _userL2Code; + } + return LanguageKeys.unknownLanguage; + } + return classL1; + } + + /// Class languages override user languages within a class context + String? activeL2Code({String? roomID}) { + if (roomID == null) { + return _userL2Code; + } + final ClassSettingsModel? classContext = _pangeaController + .matrixState.client + .getRoomById(roomID) + ?.firstLanguageSettings; + return classContext?.targetLanguage ?? _userL2Code; + } + + LanguageModel? activeL1Model({String? roomID}) { + final activeL1 = activeL1Code(roomID: roomID); + return activeL1 != null ? PangeaLanguage.byLangCode(activeL1) : null; + } + + LanguageModel? activeL2Model({String? roomID}) { + final activeL2 = activeL2Code(roomID: roomID); + final model = activeL2 != null ? PangeaLanguage.byLangCode(activeL2) : null; + return model; + } + + bool equalActiveL1AndActiveL2({Room? room}) => + activeL1Code() == activeL2Code(roomID: room?.id); +} diff --git a/lib/pangea/controllers/language_list_controller.dart b/lib/pangea/controllers/language_list_controller.dart new file mode 100644 index 000000000..368c7b813 --- /dev/null +++ b/lib/pangea/controllers/language_list_controller.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/repo/language_repo.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; + +import '../constants/language_list_keys.dart'; +import '../utils/shared_prefs.dart'; + +class PangeaLanguage { + PangeaLanguage() { + initialize(); + } + + static List _langList = []; + + List get langList => _langList; + + List get targetOptions => + _langList.where((element) => element.languageType == 2).toList(); + + List get baseOptions => + _langList.where((element) => element.languageType == 1).toList(); + + static Future initialize() async { + try { + _langList = await _getCahedFlags(); + if (await _shouldFetch || _langList.isEmpty) { + _langList = await LanguageRepo.fetchLanguages(); + + await _saveFlags(_langList); + await saveLastFetchDate(); + } + _langList.removeWhere((element) => + LanguageModel.codeFromNameOrCode(element.langCode) == + LanguageKeys.unknownLanguage); + _langList.sort((a, b) => a.displayName.compareTo(b.displayName)); + _langList.insert(0, LanguageModel.multiLingual()); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + static saveLastFetchDate() async { + final String now = DateTime.now().toIso8601String(); + await MyShared.saveString(PrefKey.lastFetched, now); + } + + static Future get _shouldFetch async { + final String? dateString = await MyShared.readString(PrefKey.lastFetched); + if (dateString == null) { + return true; + } + // return true; + final int lastFetched = DateTime.parse(dateString).millisecondsSinceEpoch; + final int now = DateTime.now().millisecondsSinceEpoch; + const int fetchIntervalInMilliseconds = 86534601; + return (now - lastFetched) >= fetchIntervalInMilliseconds ? true : false; + } + + static Future _saveFlags(List langFlags) async { + final Map flagMap = { + PrefKey.flags: langFlags.map((e) => e.toJson()).toList() + }; + await MyShared.saveJson(PrefKey.flags, flagMap); + } + + static Future> _getCahedFlags() async { + final Map? flagsMap = + await MyShared.readJson(PrefKey.flags); + if (flagsMap == null) { + return []; + } + + final List flags = []; + final List mapList = flagsMap[PrefKey.flags] as List; + for (final element in mapList) { + flags.add(LanguageModel.fromJson(element)); + } + + return flags; + } + + static LanguageModel byLangCode(String langCode) { + final list = _langList; + for (final element in _langList) { + if (element.langCode == langCode) return element; + } + return LanguageModel.unknown; + } +} diff --git a/lib/pangea/controllers/local_settings.dart b/lib/pangea/controllers/local_settings.dart new file mode 100644 index 000000000..5984a7bf5 --- /dev/null +++ b/lib/pangea/controllers/local_settings.dart @@ -0,0 +1,28 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; + +class LocalSettings { + late PangeaController _pangeaController; + + LocalSettings(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + bool userLanguageToolSetting(ToolSetting setting) => + _pangeaController.pStoreService.read(setting.toString()) ?? true; + + // bool get userEnableIT => + // _pangeaController.pStoreService.read(ToolSetting.interactiveTranslator.toString()) ?? true; + + // bool get userEnableIGC => + // _pangeaController.pStoreService.read(ToolSetting.interactiveGrammar.toString()) ?? true; + + // bool get userImmersionMode => + // _pangeaController.pStoreService.read(ToolSetting.immersionMode.toString()) ?? true; + + // bool get userTranslationsTool => + // _pangeaController.pStoreService.read(ToolSetting.translations.toString()) ?? true; + + // bool get userDefinitionsTool => + // _pangeaController.pStoreService.read(ToolSetting.definitions.toString()) ?? true; +} diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart new file mode 100644 index 000000000..24472f5c6 --- /dev/null +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -0,0 +1,436 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/enum/time_span.dart'; +import 'package:fluffychat/pangea/models/headwords.dart'; +import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; +import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/class_default_values.dart'; +import '../extensions/client_extension.dart'; +import '../extensions/pangea_room_extension.dart'; +import '../models/chart_analytics_model.dart'; +import '../models/construct_analytics_event.dart'; +import '../models/student_analytics_event.dart'; +import 'base_controller.dart'; +import 'pangea_controller.dart'; + +class AnalyticsController extends BaseController { + late PangeaController _pangeaController; + + final List _cachedModels = []; + + AnalyticsController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY"; + + TimeSpan get currentAnalyticsTimeSpan { + try { + final String? str = + _pangeaController.pStoreService.read(_analyticsTimeSpanKey); + return str != null + ? TimeSpan.values.firstWhere((e) { + final spanString = e.toString(); + return spanString == str; + }) + : ClassDefaultValues.defaultTimeSpan; + } catch (err) { + debugger(when: kDebugMode); + return ClassDefaultValues.defaultTimeSpan; + } + } + + Future setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async { + await _pangeaController.pStoreService + .save(_analyticsTimeSpanKey, timeSpan.toString()); + } + + Future> allClassAnalytics() { + final List> classAnalyticFutures = []; + for (final classRoom + in _pangeaController.matrixState.client.classesAndExchangesImTeaching) { + classAnalyticFutures.add( + getAnalytics(classRoom: classRoom), + ); + } + + return Future.wait(classAnalyticFutures); + } + + ChartAnalyticsModel? getAnalyticsLocal({ + TimeSpan? timeSpan, + String? classId, + String? studentId, + String? chatId, + bool forceUpdate = false, + bool updateExpired = false, + }) { + timeSpan ??= currentAnalyticsTimeSpan; + final int index = _cachedModels.indexWhere( + (e) => + (e.timeSpan == timeSpan) && + (e.classId == classId) && + (e.studentId == studentId) && + (e.chatId == chatId), + ); + + if (index != -1) { + if ((updateExpired && _cachedModels[index].isExpired) || forceUpdate) { + _cachedModels.removeAt(index); + } else { + return _cachedModels[index].chartAnalyticsModel; + } + } + + return null; + } + + Future getAnalytics({ + TimeSpan? timeSpan, + Room? classRoom, + String? studentId, + String? chatId, + bool forceUpdate = false, + }) async { + timeSpan ??= currentAnalyticsTimeSpan; + try { + final cachedModel = getAnalyticsLocal( + classId: classRoom?.id, + studentId: studentId, + chatId: chatId, + updateExpired: true, + forceUpdate: forceUpdate, + ); + if (cachedModel != null) return cachedModel; + // debugger(when: classRoom?.displayname.contains('clizass') ?? false); + late List studentAnalyticsSummaryEvents; + if (classRoom == null) { + if (studentId == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "studentId should have been defined", + s: StackTrace.current, + ); + } else { + studentAnalyticsSummaryEvents = + await _pangeaController.myAnalytics.allMyAnalyticsEvents(); + } + } else { + if (studentId != null) { + studentAnalyticsSummaryEvents = [ + await classRoom.getStudentAnalytics(studentId) + ]; + } else { + studentAnalyticsSummaryEvents = await classRoom.getClassAnalytics(); + } + if (studentId != null && chatId != null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "studentId and chatId should have both been defined", + s: StackTrace.current, + ); + studentAnalyticsSummaryEvents = []; + } + } + + final List msgs = []; + for (final event in studentAnalyticsSummaryEvents) { + if (event != null) { + msgs.addAll(event.content.messages); + } else { + debugPrint("studentAnalyticsSummaryEvent is null"); + } + } + final newModel = ChartAnalyticsModel( + timeSpan: timeSpan, + msgs: msgs, + chatId: chatId, + ); + + _cachedModels.add(CacheModel( + timeSpan: timeSpan, + classId: classRoom?.id, + studentId: studentId, + chatId: chatId, + chartAnalyticsModel: newModel, + )); + + return newModel; + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan, chatId: chatId); + } + } + + Future vocabHeadwordsWithTotals( + String langCode, List vocab, + [String? chatId]) async { + final VocabHeadwords vocabHeadwords = + await VocabHeadwords.getHeadwords(langCode); + for (final vocabList in vocabHeadwords.lists) { + for (final vocabEvent in vocab) { + vocabList.addVocabUse( + vocabEvent.content.lemma, + vocabEvent.content.uses, + ); + } + } + return vocabHeadwords; + } + + Future getAnalyticsForPrivateChats({ + TimeSpan? timeSpan, + required Room? classRoom, + bool forceUpdate = false, + }) async { + timeSpan ??= currentAnalyticsTimeSpan; + + try { + if (classRoom == null) { + return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan); + } + + final cachedModel = getAnalyticsLocal( + classId: classRoom.id, + studentId: null, + chatId: AnalyticsEntryType.privateChats.toString(), + updateExpired: true, + forceUpdate: forceUpdate, + ); + if (cachedModel != null) return cachedModel; + final List studentAnalyticsSummaryEvents = + await classRoom.getClassAnalytics(); + final List directChatIds = + classRoom.childrenAndGrandChildrenDirectChatIds; + + final List msgs = []; + for (final event in studentAnalyticsSummaryEvents) { + if (event != null) { + msgs.addAll(event.content.messages + .where((m) => directChatIds.contains(m.chatId))); + } else { + debugPrint("studentAnalyticsSummaryEvent is null"); + } + } + final newModel = ChartAnalyticsModel( + timeSpan: timeSpan, + msgs: msgs, + chatId: null, + ); + + _cachedModels.add(CacheModel( + timeSpan: timeSpan, + classId: classRoom.id, + studentId: null, + chatId: AnalyticsEntryType.privateChats.toString(), + chartAnalyticsModel: newModel, + )); + + return newModel; + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return ChartAnalyticsModel(msgs: [], timeSpan: timeSpan); + } + } + + Future myAnalyticsRoom(String langCode) => + _pangeaController.matrixState.client.getMyAnalyticsRoom(langCode); + + Future> myConstructs(String langCode) async { + final Room analyticsRoom = await myAnalyticsRoom(langCode); + + return analyticsRoom.allConstructEvents; + } + + Future> studentConstructs( + String studentId, String langCode) { + final Room? analyticsRoom = _pangeaController.matrixState.client + .analyticsRoomLocal(langCode, studentId); + return analyticsRoom!.allConstructEvents; + } + + Future> spaceMemberVocab(String spaceId) async { + await _pangeaController.matrixState.client.roomsLoading; + final Room? space = + _pangeaController.matrixState.client.getRoomById(spaceId); + if (space == null) { + throw Exception("space missing in spaceVocab"); + } + final String? langCode = space.firstLanguageSettings?.targetLanguage; + + final List>> vocabEventFutures = []; + await space.requestParticipants(); + for (final student in space.students) { + final Room? room = _pangeaController.matrixState.client + .analyticsRoomLocal(langCode, student.id); + if (room != null) vocabEventFutures.add(room.allConstructEvents); + } + + final List> allVocabLists = + await Future.wait(vocabEventFutures); + + final List allVocab = []; + for (final vocabList in allVocabLists) { + allVocab.addAll(vocabList); + } + return allVocab; + } + + /// in student analytics page, the [defaultSelected] is the student + /// in class analytics page, the [defaultSelected] is the class + /// [defaultSelected] should never be a chat + /// the specific [selected] will be those items in the lists - chat, student or class + Future vocabHeadsByAnalyticsSelected({ + required AnalyticsSelected? selected, + required AnalyticsSelected defaultSelected, + }) async { + Future> eventsFuture; + String langCode; + + if (defaultSelected.type == AnalyticsEntryType.space) { + // as long as a student isn't selected, we want the vocab events for the whole class + final Room? classRoom = + _pangeaController.matrixState.client.getRoomById(defaultSelected.id); + if (classRoom == null) { + throw Exception("classRoom missing in spaceMemberVocab"); + } + langCode = classRoom.classSettings!.targetLanguage; + + if (selected?.type != AnalyticsEntryType.student) { + eventsFuture = spaceMemberVocab(defaultSelected.id); + } else { + eventsFuture = studentConstructs(selected!.id, langCode); + } + } else if (defaultSelected.type == AnalyticsEntryType.student) { + // in this case, we're on an individual's own analytics page + + if (selected?.type == AnalyticsEntryType.space || + selected?.type == AnalyticsEntryType.student) { + langCode = _pangeaController.languageController + .activeL2Code(roomID: selected!.id)!; + eventsFuture = myConstructs(langCode); + } else { + langCode = _pangeaController.languageController.userL2!.langCode; + eventsFuture = myConstructs(langCode); + } + } else { + throw Exception("invalid defaultSelected.type - ${defaultSelected.type}"); + } + + return vocabHeadwordsWithTotals(langCode, await eventsFuture); + } + + /// in student analytics page, the [defaultSelected] is the student + /// in class analytics page, the [defaultSelected] is the class + /// [defaultSelected] should never be a chat + /// the specific [selected] will be those items in the lists - chat, student or class + Future> constuctEventsByAnalyticsSelected({ + required AnalyticsSelected? selected, + required AnalyticsSelected defaultSelected, + required ConstructType constructType, + }) async { + late Future> eventFutures; + String? langCode; + if (defaultSelected.type == AnalyticsEntryType.space) { + // as long as a student isn't selected, we want the vocab events for the whole class + final Room? space = + _pangeaController.matrixState.client.getRoomById(defaultSelected.id); + if (space == null) { + throw "No space available"; + } + langCode = space.firstLanguageSettings?.targetLanguage; + if (langCode == null) { + throw "No target language available"; + } + + if (selected?.type != AnalyticsEntryType.student) { + eventFutures = spaceMemberVocab(defaultSelected.id); + } else { + eventFutures = studentConstructs(selected!.id, langCode); + } + } else if (defaultSelected.type == AnalyticsEntryType.student) { + // in this case, we're on an individual's own analytics page + + if (selected?.type == AnalyticsEntryType.space || + selected?.type == AnalyticsEntryType.student) { + langCode = _pangeaController.languageController + .activeL2Code(roomID: selected!.id)!; + eventFutures = myConstructs(langCode); + } else { + langCode = _pangeaController.languageController.userL2!.langCode; + eventFutures = myConstructs(langCode); + } + } else { + throw "invalid defaultSelected.type - ${defaultSelected.type}"; + } + + final List events = (await eventFutures) + .where( + (element) => element.content.type == constructType, + ) + .toList(); + + final List chatIdsToFilterBy = []; + if (selected?.type == AnalyticsEntryType.room) { + chatIdsToFilterBy.add(selected!.id); + } else if (selected?.type == AnalyticsEntryType.privateChats) { + chatIdsToFilterBy.addAll(_pangeaController.matrixState.client + .getRoomById(defaultSelected.id) + ?.childrenAndGrandChildrenDirectChatIds ?? + []); + } else if (defaultSelected.type == AnalyticsEntryType.space) { + chatIdsToFilterBy.addAll(_pangeaController.matrixState.client + .getRoomById(defaultSelected.id) + ?.childrenAndGrandChildren + .where((e) => e.roomId != null) + .map((e) => e.roomId!) ?? + []); + } + if (chatIdsToFilterBy.isNotEmpty) { + for (final event in events) { + event.content.uses + .removeWhere((u) => !chatIdsToFilterBy.contains(u.chatId)); + } + events.removeWhere((e) => e.content.uses.isEmpty); + } + + return events; + } +} + +class CacheModel { + TimeSpan timeSpan; + ChartAnalyticsModel chartAnalyticsModel; + String? classId; + String? chatId; + String? studentId; + late DateTime _createdAt; + + CacheModel({ + required this.timeSpan, + required this.classId, + required this.chartAnalyticsModel, + required this.chatId, + required this.studentId, + }) { + _createdAt = DateTime.now(); + } + + bool get isExpired => + DateTime.now().difference(_createdAt).inMinutes > + ClassDefaultValues.minutesDelayToMakeNewChartAnalytics; +} + +// class ListTotals { +// String listName; +// ConstructUses vocabUse; + +// ListTotals({required this.listName, required this.vocabUse}); +// } diff --git a/lib/pangea/controllers/message_data_controller.dart b/lib/pangea/controllers/message_data_controller.dart new file mode 100644 index 000000000..7e273e551 --- /dev/null +++ b/lib/pangea/controllers/message_data_controller.dart @@ -0,0 +1,228 @@ +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/controllers/base_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/repo/tokens_repo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../constants/pangea_event_types.dart'; +import '../enum/use_type.dart'; +import '../models/choreo_record.dart'; +import '../repo/full_text_translation_repo.dart'; +import '../utils/error_handler.dart'; + +class MessageDataController extends BaseController { + late PangeaController _pangeaController; + + final List _cache = []; + + final Map _messageDataToSave = {}; + + MessageDataController(PangeaController pangeaController) { + _pangeaController = pangeaController; + } + + CacheItem? getItem(String parentId, String type, String langCode) => + _cache.firstWhereOrNull((e) => + e.parentId == parentId && e.type == type && e.langCode == langCode); + + Future _getTokens( + TokensRequestModel req, + ) async { + final accessToken = await _pangeaController.userController.accessToken; + + final TokensResponseModel igcTextData = + await TokensRepo.tokenize(accessToken, req); + + return PangeaMessageTokens(tokens: igcTextData.tokens); + } + + Future _getTokenEvent({ + required BuildContext context, + required String repEventId, + required TokensRequestModel req, + required Room room, + }) async { + try { + final PangeaMessageTokens? pangeaMessageTokens = await _getTokens( + req, + ); + if (pangeaMessageTokens == null) return null; + + final Event? tokensEvent = await room.sendPangeaEvent( + content: pangeaMessageTokens.toJson(), + parentEventId: repEventId, + type: PangeaEventTypes.tokens, + ); + + return tokensEvent; + } catch (err, stack) { + Sentry.addBreadcrumb( + Breadcrumb( + message: "err in _getTokenEvent with repEventId $repEventId"), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"req": req.toJson()}), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"room": room.toJson()}), + ); + ErrorHandler.logError(e: err, s: stack); + return null; + } + } + + Future getTokenEvent({ + required BuildContext context, + required String repEventId, + required TokensRequestModel req, + required Room room, + }) async { + final CacheItem? item = + getItem(repEventId, PangeaEventTypes.tokens, req.userL2); + if (item != null) return item.data; + + _cache.add(CacheItem( + repEventId, + PangeaEventTypes.tokens, + req.userL2, + _getTokenEvent( + context: context, + repEventId: repEventId, + req: req, + room: room, + ), + )); + + return _cache.last.data; + } + + /////// translation //////// + + /// make representation (originalSent and originalWritten always false) + Future _getRepresentationMatrixEvent({ + required BuildContext context, + required String messageEventId, + required FullTextTranslationRequestModel req, + required Room room, + }) async { + try { + final FullTextTranslationResponseModel res = + await FullTextTranslationRepo.translate( + accessToken: await _pangeaController.userController.accessToken, + request: req, + ); + + final PangeaRepresentation representation = PangeaRepresentation( + langCode: req.tgtLang, + text: res.bestTranslation, + originalSent: false, + originalWritten: false, + ); + + final Event? repEvent = await room.sendPangeaEvent( + content: representation.toJson(), + parentEventId: messageEventId, + type: PangeaEventTypes.representation, + ); + + debugger(when: kDebugMode && repEvent == null); + + return repEvent; + } catch (err, stack) { + Sentry.addBreadcrumb( + Breadcrumb( + message: + "err in _getRepresentationMatrixEvent with messageEventId $messageEventId", + ), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"req": req.toJson()}), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"room": room.toJson()}), + ); + ErrorHandler.logError(e: err, s: stack); + return null; + } + } + + /// make representation (originalSent and originalWritten always false) + Future getRepresentationMatrixEvent({ + required BuildContext context, + required String messageEventId, + required String text, + required String? source, + required String target, + required Room room, + }) { + final CacheItem? item = + getItem(messageEventId, PangeaEventTypes.representation, target); + if (item != null) return item.data; + + _cache.add( + CacheItem( + messageEventId, + PangeaEventTypes.representation, + target, + _getRepresentationMatrixEvent( + context: context, + messageEventId: messageEventId, + req: FullTextTranslationRequestModel( + text: text, + tgtLang: target, + srcLang: source, + userL2: _pangeaController.languageController + .activeL2Code(roomID: room.id)!, + userL1: _pangeaController.languageController + .activeL1Code(roomID: room.id)!, + ), + room: room, + ), + ), + ); + + return _cache.last.data; + } +} + +class MessageDataQueueItem { + String transactionId; + + List repTokensAndRecords; + + UseType useType; + + MessageDataQueueItem( + this.transactionId, this.repTokensAndRecords, this.useType + // required this.recentMessageRecord, + ); +} + +class RepTokensAndRecord { + PangeaRepresentation representation; + ChoreoRecord? choreoRecord; + PangeaMessageTokens? tokens; + RepTokensAndRecord(this.representation, this.choreoRecord, this.tokens); + + Map toJson() => { + "rep": representation.toJson(), + "choreoRecord": choreoRecord?.toJson(), + "tokens": tokens?.toJson(), + }; +} + +class CacheItem { + String parentId; + String langCode; + String type; + Future data; + + CacheItem(this.parentId, this.type, this.langCode, this.data); +} diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart new file mode 100644 index 000000000..8a5c5783f --- /dev/null +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../extensions/client_extension.dart'; +import '../extensions/pangea_room_extension.dart'; +import '../models/constructs_analytics_model.dart'; +import '../models/student_analytics_event.dart'; + +class MyAnalyticsController { + late PangeaController _pangeaController; + + MyAnalyticsController(PangeaController pangeaController) { + _pangeaController = pangeaController; + } + + String? get _userId => _pangeaController.matrixState.client.userID; + + //PTODO - locally cache and update periodically + Future handleMessage( + Room room, RecentMessageRecord messageRecord) async { + try { + debugPrint("in handle message with type ${messageRecord.useType}"); + if (_userId == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "null userId in updateAnalytics", s: StackTrace.current); + return; + } + + await _pangeaController.classController.addDirectChatsToClasses(room); + //expanding this to all parents of the room + // final List spaces = room.immediateClassParents; + final List spaces = room.pangeaSpaceParents; + // janky but probably stays until we have a class analytics bot added by + // default to all chats + + final List events = await analyticsEvents(spaces); + + for (final event in events) { + debugPrint("adding to total ${event?.content.messages.length}"); + if (event != null) { + event.handleNewMessage(messageRecord); + } + } + } catch (err) { + debugger(when: kDebugMode); + } + } + + Future> analyticsEvents( + List spaces) async { + final List> events = []; + for (final space in spaces) { + events.add(space.getStudentAnalytics(_userId!)); + } + return Future.wait(events); + } + + Future> allMyAnalyticsEvents() => + analyticsEvents( + _pangeaController.matrixState.client.classesAndExchangesImStudyingIn, + ); + + Future saveConstructsMixed( + List allUses, String langCode) async { + try { + final Map> aggregatedVocabUse = {}; + for (final use in allUses) { + aggregatedVocabUse[use.lemma!] ??= []; + aggregatedVocabUse[use.lemma]!.add(use); + } + final Room analyticsRoom = await _pangeaController.matrixState.client + .getMyAnalyticsRoom(langCode); + analyticsRoom.makeSureTeachersAreInvitedToAnalyticsRoom(); + final List> saveFutures = []; + for (final uses in aggregatedVocabUse.entries) { + debugPrint("saving of type ${uses.value.first.constructType}"); + saveFutures.add( + analyticsRoom.saveConstructUsesSameLemma( + uses.key, uses.value.first.constructType!, uses.value), + ); + } + + await Future.wait(saveFutures); + } catch (err, s) { + debugger(when: kDebugMode); + if (!kDebugMode) rethrow; + ErrorHandler.logError(e: err, s: s); + } + } +} diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart new file mode 100644 index 000000000..5e92b79f4 --- /dev/null +++ b/lib/pangea/controllers/pangea_controller.dart @@ -0,0 +1,235 @@ +import 'dart:developer'; +import 'dart:math'; + +import 'package:fluffychat/pangea/constants/class_default_values.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_list_controller.dart'; +import 'package:fluffychat/pangea/controllers/local_settings.dart'; +import 'package:fluffychat/pangea/controllers/message_data_controller.dart'; +import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; +import 'package:fluffychat/pangea/controllers/permissions_controller.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/controllers/user_controller.dart'; +import 'package:fluffychat/pangea/controllers/word_net_controller.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'; +import 'package:fluffychat/pangea/utils/instructions.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../config/app_config.dart'; +import '../utils/firebase_analytics.dart'; +import '../utils/p_store.dart'; +import 'message_analytics_controller.dart'; + +class PangeaController { + ///pangeaControllers + late UserController userController; + late LanguageController languageController; + late ClassController classController; + late PermissionsController permissionsController; + late AnalyticsController analytics; + late MyAnalyticsController myAnalytics; + late WordController wordNet; + late LocalSettings localSettings; + late MessageDataController messageData; + late ContextualDefinitionController definitions; + late InstructionsController instructions; + late SubscriptionController subscriptionController; + + ///store Services + late PLocalStore pStoreService; + final pLanguageStore = PangeaLanguage(); + + ///Matrix Variables + MatrixState matrixState; + Matrix matrix; + + int? randomint; + PangeaController({required this.matrix, required this.matrixState}) { + _setup(); + _subscribeToMatrixStreams(); + randomint = Random().nextInt(2000); + } + + /// Pangea Initialization + void _setup() { + _addRefInObjects(); + } + + void afterSyncAndFirstLoginInitialization(BuildContext context) { + classController.checkForClassCodeAndSubscription(context); + + // startChatWithBotIfNotPresent(); + + classController.fixClassPowerLevels(); + } + + /// Initialize controllers + _addRefInObjects() { + pStoreService = PLocalStore(pangeaController: this); + userController = UserController(this); + languageController = LanguageController(this); + localSettings = LocalSettings(this); + classController = ClassController(this); + permissionsController = PermissionsController(this); + analytics = AnalyticsController(this); + myAnalytics = MyAnalyticsController(this); + messageData = MessageDataController(this); + wordNet = WordController(this); + definitions = ContextualDefinitionController(this); + instructions = InstructionsController(this); + subscriptionController = SubscriptionController(this); + PAuthGaurd.pController = this; + } + + _logOutfromPangea() { + debugPrint("Pangea logout"); + GoogleAnalytics.logout(); + pStoreService.clearStorage(); + } + + Future checkHomeServerAction() async { + if (matrixState.getLoginClient().homeserver != null) { + await Future.delayed(Duration.zero); + return; + } + + final String homeServer = + AppConfig.defaultHomeserver.trim().toLowerCase().replaceAll(' ', '-'); + var homeserver = Uri.parse(homeServer); + if (homeserver.scheme.isEmpty) { + homeserver = Uri.https(homeServer, ''); + } + + matrixState.loginHomeserverSummary = + await matrixState.getLoginClient().checkHomeserver(homeserver); + final ssoSupported = matrixState.loginHomeserverSummary!.loginFlows + .any((flow) => flow.type == 'm.login.sso'); + + try { + await matrixState.getLoginClient().register(); + matrixState.loginRegistrationSupported = true; + } on MatrixException catch (e) { + matrixState.loginRegistrationSupported = + e.requireAdditionalAuthentication; + } + + // setState(() => error = (e).toLocalizedString(context)); + } + + /// check user information if not found then redirect to Date of birth page + _handleLoginStateChange(LoginState state) { + if (state != LoginState.loggedIn) { + _logOutfromPangea(); + } + Sentry.configureScope( + (scope) => scope.setUser(SentryUser(id: matrixState.client.userID)), + ); + GoogleAnalytics.analyticsUserUpdate(matrixState.client.userID); + } + + // void startChatWithBotIfNotPresent() { + // Future.delayed(const Duration(milliseconds: 5000), () async { + // try { + // if (pStoreService.read("started_bot_chat", addClientIdToKey: false) ?? + // false) { + // return; + // } + // await pStoreService.save("started_bot_chat", true, + // addClientIdToKey: false); + // final rooms = matrixState.client.rooms; + + // await matrixState.client.startDirectChat( + // BotName.byEnvironment, + // enableEncryption: false, + // ); + // } catch (err, stack) { + // debugger(when: kDebugMode); + // ErrorHandler.logError(e: err, s: stack); + // } + // }); + // } + + void startChatWithBotIfNotPresent() { + Future.delayed(const Duration(milliseconds: 10000), () async { + try { + await matrixState.client.startDirectChat( + BotName.byEnvironment, + enableEncryption: false, + ); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + }); + } + + _handleJoinEvent(SyncUpdate syncUpdate) { + // for (final joinedRoomUpdate in syncUpdate.rooms!.join!.entries) { + // debugPrint( + // "room update for ${joinedRoomUpdate.key} - ${joinedRoomUpdate.value}"); + // } + } + + _handleOnSyncUpdate(SyncUpdate syncUpdate) { + // debugPrint(syncUpdate.toString()); + } + + _handleSyncStatusFinished(SyncStatusUpdate event) { + //might be useful to do something periodically, probably be overkill + } + + void _subscribeToMatrixStreams() { + matrixState.client.onLoginStateChanged.stream + .listen(_handleLoginStateChange); + + // matrixState.client.onSyncStatus.stream + // .where((SyncStatusUpdate event) => event.status == SyncStatus.finished) + // .listen(_handleSyncStatusFinished); + + //PTODO - listen to incoming invites and autojoin if in class + // matrixState.client.onSync.stream + // .where((event) => event.rooms?.invite?.isNotEmpty ?? false) + // .listen((SyncUpdate event) { + // }); + + // matrixState.client.onSync.stream.listen(_handleOnSyncUpdate); + } + + Future inviteBotToExistingSpaces() async { + final List spaces = + matrixState.client.rooms.where((room) => room.isSpace).toList(); + for (final Room space in spaces) { + List participants; + try { + participants = await space.requestParticipants(); + } catch (err) { + ErrorHandler.logError( + e: "Failed to fetch participants for space ${space.id}", + ); + continue; + } + final List userIds = participants.map((user) => user.id).toList(); + if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { + try { + await space.invite(BotName.byEnvironment); + await space.setPower( + BotName.byEnvironment, + ClassDefaultValues.powerLevelOfAdmin, + ); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${space.id}", + ); + } + } + } + } +} diff --git a/lib/pangea/controllers/permissions_controller.dart b/lib/pangea/controllers/permissions_controller.dart new file mode 100644 index 000000000..479f53ff6 --- /dev/null +++ b/lib/pangea/controllers/permissions_controller.dart @@ -0,0 +1,141 @@ +import 'package:fluffychat/pangea/constants/age_limits.dart'; +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/controllers/base_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/utils/p_extension.dart'; +import 'package:matrix/matrix.dart'; + +class PermissionsController extends BaseController { + late PangeaController _pangeaController; + + PermissionsController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + Room? _getRoomById(String? roomId) => roomId == null + ? null + : _pangeaController.matrixState.client.getRoomById(roomId); + + PangeaRoomRules? _getRoomRules(String? roomId) => + roomId == null ? null : _getRoomById(roomId)?.firstRules; + + Room? firstRoomWithState({required String? roomID, required String type}) { + final Room? room = _getRoomById(roomID); + + return room?.pangeaRoomRules != null + ? room + : room?.firstParentWithState(type); + } + + /// Returns false if user is null + bool isUser18() { + final dob = + _pangeaController.userController.userModel?.profile?.dateOfBirth; + return dob != null + ? DateTime.parse(dob).isAtLeastYearsOld(AgeLimits.toAccessFeatures) + : false; + } + + /// A user can private chat if + /// 1) they are 18 and outside a class context or + /// 2) they are in a class context and the class rules permit it + /// If no class is passed, uses classController.activeClass + bool canUserPrivateChat({String? roomID}) { + final Room? classContext = + firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); + return classContext?.pangeaRoomRules == null + ? isUser18() + : classContext!.pangeaRoomRules!.oneToOneChatClass || + classContext.isRoomAdmin; + } + + bool canUserGroupChat({String? roomID}) { + final Room? classContext = + firstRoomWithState(roomID: roomID, type: PangeaEventTypes.rules); + + return classContext?.pangeaRoomRules == null + ? isUser18() + : classContext!.pangeaRoomRules!.isCreateRooms || + classContext.isRoomAdmin; + } + + bool showChatInputAddButton(String roomId) { + final PangeaRoomRules? perms = _getRoomRules(roomId); + if (perms == null) return isUser18(); + return perms.isShareFiles || + perms.isShareLocation || + perms.isSharePhoto || + perms.isShareVideo; + } + + /// works for both roomID of chat and class + bool canShareVideo(String? roomID) => + _getRoomRules(roomID)?.isShareVideo ?? isUser18(); + + /// works for both roomID of chat and class + bool canSharePhoto(String? roomID) => + _getRoomRules(roomID)?.isSharePhoto ?? isUser18(); + + /// works for both roomID of chat and class + bool canShareFile(String? roomID) => + _getRoomRules(roomID)?.isShareFiles ?? isUser18(); + + /// works for both roomID of chat and class + bool canShareLocation(String? roomID) => + _getRoomRules(roomID)?.isShareLocation ?? isUser18(); + + int? classLanguageToolPermission(Room room, ToolSetting setting) => + room.firstRules?.getToolSettings(setting); + + //what happens if a room isn't in a class? + bool isToolDisabledByClass(ToolSetting setting, Room? room) { + if (room?.isSpaceAdmin ?? false) return false; + final int? classPermission = + room != null ? classLanguageToolPermission(room, setting) : 1; + return classPermission == 0; + } + + bool userToolSetting(ToolSetting setting) => + _pangeaController.localSettings.userLanguageToolSetting(setting); + + bool isToolEnabled(ToolSetting setting, Room? room) { + if (room?.isSpaceAdmin ?? false) { + return userToolSetting(setting); + } + final int? classPermission = + room != null ? classLanguageToolPermission(room, setting) : 1; + if (classPermission == 0) return false; + if (classPermission == 2) return true; + return userToolSetting(setting); + } + + bool isWritingAssistanceEnabled(Room? room) { + return isToolEnabled(ToolSetting.interactiveTranslator, room) && + isToolEnabled(ToolSetting.interactiveGrammar, room); + } + + // bool get showChatListStartChatFloatingActionButton { + // //for now, I'm turning off chat button when not in the context of a clas + // //it will still be possible to private chat outside of a class + // //need to investigate if private chats can be put in a space. i suppose they can + // //if so, do we want that? + // try { + // if (_pangeaController.classController.activeClass == null) return false; + + // // final isExchange = + // // (_pangeaController.classController.activeClass?.isExchange ?? false); + // const isExchange = false; + // final regular = (canUserPrivateChat() || canUserGroupChat()); + // final inExchange = + // (canUserPrivateChatExchanges() || canUserGroupChatExchanges()); + // final theAnswer = isExchange ? inExchange : regular; + // // debugger(when: kDebugMode && !theAnswer); + // return theAnswer; + // } catch (e, s) { + // ErrorHandler.logError(e: e, s: s); + // return false; + // } + // } +} diff --git a/lib/pangea/controllers/space_rules_edit_controller.dart b/lib/pangea/controllers/space_rules_edit_controller.dart new file mode 100644 index 000000000..d47de0156 --- /dev/null +++ b/lib/pangea/controllers/space_rules_edit_controller.dart @@ -0,0 +1,20 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:matrix/matrix.dart'; + +import '../extensions/pangea_room_extension.dart'; +import '../models/class_model.dart'; + +class RoomRulesEditController { + final Room? room; + + late PangeaRoomRules rules; + + RoomRulesEditController([this.room]) { + rules = room?.pangeaRoomRules ?? PangeaRoomRules(); + } + + StateEvent get toStateEvent => StateEvent( + content: rules.toJson(), + type: PangeaEventTypes.rules, + ); +} diff --git a/lib/pangea/controllers/subscription_controller.dart b/lib/pangea/controllers/subscription_controller.dart new file mode 100644 index 000000000..98a73c68e --- /dev/null +++ b/lib/pangea/controllers/subscription_controller.dart @@ -0,0 +1,257 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/controllers/base_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/base_subscription_info.dart'; +import 'package:fluffychat/pangea/models/mobile_subscriptions.dart'; +import 'package:fluffychat/pangea/models/web_subscriptions.dart'; +import 'package:fluffychat/pangea/network/requests.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/widgets/subscription/subscription_paywall.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:http/http.dart'; +import 'package:purchases_flutter/purchases_flutter.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class SubscriptionController extends BaseController { + late PangeaController _pangeaController; + SubscriptionInfo? subscription; + + //convert this logic to use completer + bool initialized = false; + + SubscriptionController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + bool get isSubscribed => + subscription != null && + (subscription!.currentSubscriptionId != null || + subscription!.currentSubscription != null); + + bool get currentSubscriptionAvailable => + isSubscribed && subscription?.currentSubscription != null; + + bool get currentSubscriptionIsTrial => + subscription?.currentSubscription?.isTrial ?? false; + + Future initialize() async { + try { + if (_pangeaController.matrixState.client.userID == null) { + debugPrint( + "Attempted to initalize subscription information with null userId", + ); + return; + } + + subscription = kIsWeb + ? WebSubscriptionInfo(pangeaController: _pangeaController) + : MobileSubscriptionInfo(pangeaController: _pangeaController); + + await subscription!.configure(); + + initialized = true; + + if (!kIsWeb) { + Purchases.addCustomerInfoUpdateListener( + (CustomerInfo info) => updateCustomerInfo(), + ); + } + setState(); + } catch (e, s) { + debugPrint("Failed to initialize subscription controller"); + ErrorHandler.logError(e: e, s: s); + } + } + + Future updateCustomerInfo() async { + if (subscription == null) { + ErrorHandler.logError( + m: "Null subscription info in subscription settings", + s: StackTrace.current, + ); + return; + } + await subscription!.setCustomerInfo(); + + setState(); + } + + Future showPaywall(BuildContext context, + [bool forceShow = false]) async { + try { + if (!initialized) { + await initialize(); + } + if (subscription?.availableSubscriptions.isEmpty ?? true) { + return; + } + if (!forceShow && isSubscribed) return; + await showModalBottomSheet( + isScrollControlled: true, + useRootNavigator: !PlatformInfos.isMobile, + clipBehavior: Clip.hardEdge, + context: context, + constraints: BoxConstraints( + maxHeight: PlatformInfos.isMobile ? 600 : 480, + ), + builder: (_) => SubscriptionPaywall( + pangeaController: _pangeaController, + ), + ); + } catch (e, s) { + ErrorHandler.logError(e: e, s: s); + } + } + + Future fetchSubscriptionStatus() async { + final Requests req = Requests(baseUrl: PApiUrls.baseAPI); + final String reqUrl = Uri.encodeFull( + "${PApiUrls.subscriptionExpiration}?pangea_user_id=${_pangeaController.matrixState.client.userID}", + ); + + DateTime? expiration; + try { + final Response res = await req.get(url: reqUrl); + final json = jsonDecode(res.body); + if (json["premium_expires_date"] != null) { + expiration = DateTime.parse(json["premium_expires_date"]); + } + } catch (err) { + ErrorHandler.logError( + e: "Failed to fetch subscripton status for user ${_pangeaController.matrixState.client.userID}", + s: StackTrace.current, + ); + } + final bool subscribed = + expiration == null ? false : DateTime.now().isBefore(expiration); + GoogleAnalytics.updateUserSubscriptionStatus(subscribed); + return subscribed; + } + + Future getPaymentLink(String duration, {bool isPromo = false}) async { + final Requests req = Requests(baseUrl: PApiUrls.baseAPI); + final String reqUrl = Uri.encodeFull( + "${PApiUrls.paymentLink}?pangea_user_id=${_pangeaController.matrixState.client.userID}&duration=$duration&redeem=$isPromo", + ); + final Response res = await req.get(url: reqUrl); + final json = jsonDecode(res.body); + String paymentLink = json["link"]["url"]; + + final String? email = await _pangeaController.userController.userEmail; + if (email != null) { + paymentLink += "?prefilled_email=${Uri.encodeComponent(email)}"; + } + return paymentLink; + } + + void submitSubscriptionChange( + SubscriptionDetails? selectedSubscription, BuildContext context, + {bool isPromo = false}) async { + if (selectedSubscription != null) { + if (kIsWeb) { + if (selectedSubscription.duration == null) { + ErrorHandler.logError( + m: "Tried to subscribe to web SubscriptionDetails with Null duration", + s: StackTrace.current, + ); + return; + } + final String paymentLink = await getPaymentLink( + selectedSubscription.duration!, + isPromo: isPromo, + ); + setState(); + launchUrlString( + paymentLink, + webOnlyWindowName: "_self", + ); + return; + } + if (selectedSubscription.package == null) { + ErrorHandler.logError( + m: "Tried to subscribe to SubscriptionDetails with Null revenuecat Package", + s: StackTrace.current, + ); + return; + } + try { + GoogleAnalytics.beginPurchaseSubscription( + selectedSubscription, context); + await Purchases.purchasePackage(selectedSubscription.package!); + GoogleAnalytics.updateUserSubscriptionStatus(true); + } catch (err) { + ErrorHandler.logError( + m: "Failed to purchase revenuecat package for user ${_pangeaController.matrixState.client.userID}", + s: StackTrace.current, + ); + return; + } + } + } + + Future redeemPromoCode(BuildContext context) async { + final List? promoCode = await showTextInputDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.enterPromoCode, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + textFields: [const DialogTextField()], + ); + if (promoCode == null || promoCode.single.isEmpty) return; + launchUrlString( + "${AppConfig.iosPromoCode}${promoCode.single}", + ); + } +} + +class SubscriptionDetails { + double price; + String? duration; + Package? package; + String? appId; + final String id; + String? periodType = "normal"; + + SubscriptionDetails({ + required this.price, + required this.id, + this.duration, + this.package, + this.appId, + this.periodType, + }); + + void makeTrial() => periodType = 'trial'; + bool get isTrial => periodType == 'trial'; + + String displayPrice(BuildContext context) { + if (isTrial || price <= 0) { + return L10n.of(context)!.freeTrial; + } + return "\$${price.toStringAsFixed(2)}"; + } + + String displayName(BuildContext context) { + if (isTrial) { + return L10n.of(context)!.oneWeekTrial; + } + switch (duration) { + case ('month'): + return L10n.of(context)!.monthlySubscription; + case ('year'): + return L10n.of(context)!.yearlySubscription; + default: + return L10n.of(context)!.defaultSubscription; + } + } +} diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart new file mode 100644 index 000000000..3dbe462b1 --- /dev/null +++ b/lib/pangea/controllers/user_controller.dart @@ -0,0 +1,200 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/controllers/base_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/fluffy_chat_app.dart'; +import 'package:jwt_decode/jwt_decode.dart'; +import 'package:matrix/matrix.dart' as matrix; + +import '../constants/local.key.dart'; +import '../models/user_model.dart'; +import '../repo/user_repo.dart'; + +class UserController extends BaseController { + late PangeaController _pangeaController; + final Completer _completer = Completer(); + UserController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + Future fetchUserModel() async { + try { + if (_matrixAccessToken == null) { + throw Exception( + "calling fetchUserModel with matrixAccesstoken == null", + ); + } + final PUserModel? newUserModel = await PUserRepo.fetchPangeaUserInfo( + userID: userId!, + matrixAccessToken: _matrixAccessToken!, + ); + + if (newUserModel != null) { + _savePUserModel(newUserModel); + } + _completeCompleter(); + + return newUserModel; + } catch (err) { + log("User model not found. Probably first signup and needs Pangea account"); + rethrow; + } + } + + void _completeCompleter() { + if (!_completer.isCompleted) { + _completer.complete(null); + } + } + + Future get completer async { + if (await isPUserDataAvailable) { + _completeCompleter(); + } + return _completer; + } + + bool get needNewJWT => + userModel?.access != null ? Jwt.isExpired(userModel!.access) : true; + + Future get accessToken async { + await (await completer).future; + // if userModel null or access token expired then fetchUserModel + final PUserModel? useThisOne = + needNewJWT ? await fetchUserModel() : userModel; + + if (useThisOne == null) { + //debugger(when: kDebugMode); + throw Exception("trying to get accessToken with userModel = null"); + } + return useThisOne.access; + } + + String? get userId { + return _pangeaController.matrixState.client.userID; + } + + String get fullname { + final String? userID = userId; + if (userID == null) { + throw Exception('User ID not found'); + } + return userID.substring(0, userID.indexOf(":")).replaceAll("@", ""); + } + + PUserModel? get userModel { + final data = _pangeaController.pStoreService.read(PLocalKey.user); + return data != null ? PUserModel.fromJson(data) : null; + } + + Future get isPUserDataAvailable async { + try { + final PUserModel? toCheck = userModel ?? (await fetchUserModel()); + return toCheck != null ? true : false; + } catch (err) { + return false; + } + } + + Future get isUserDataAvailableAndDateOfBirthSet async { + try { + final PUserModel? toCheck = userModel ?? (await fetchUserModel()); + return toCheck?.profile?.dateOfBirth != null ? true : false; + } catch (err) { + return false; + } + } + + redirectToUserInfo() { + // _pangeaController.matrix.router!.currentState!.to( + // "/home/connect/user_age", + // queryParameters: + // _pangeaController.matrix.router!.currentState!.queryParameters, + // ); + FluffyChatApp.router.go("/rooms/user_age"); + } + + _savePUserModel(PUserModel? pUserModel) { + final jsonUser = pUserModel!.toJson(); + _pangeaController.pStoreService.save(PLocalKey.user, jsonUser); + + setState(data: pUserModel); + } + + Future updateUserProfile({ + String? dateOfBirth, + String? targetLanguage, + String? sourceLanguage, + String? country, + List? interests, + List? speaks, + bool? publicProfile, + }) async { + if (userModel == null) throw Exception("Local userModel not defined"); + final profileJson = userModel!.profile!.toJson(); + + if (dateOfBirth != null) { + profileJson[ModelKey.userDateOfBirth] = dateOfBirth; + } + if (targetLanguage != null) { + profileJson[ModelKey.userTargetLanguage] = targetLanguage; + } + if (sourceLanguage != null) { + profileJson[ModelKey.userSourceLanguage] = sourceLanguage; + } + if (interests != null) { + profileJson[ModelKey.userInterests] = interests.toString(); + } + if (speaks != null) { + profileJson[ModelKey.userSpeaks] = speaks.toString(); + } + if (country != null) { + profileJson[ModelKey.userCountry] = country; + } + if (publicProfile != null) { + profileJson[ModelKey.publicProfile] = publicProfile; + } + final Profile updatedUserProfile = await PUserRepo.updateUserProfile( + Profile.fromJson(profileJson), + await accessToken, + ); + + await _savePUserModel( + PUserModel( + access: await accessToken, + refresh: userModel!.refresh, + profile: updatedUserProfile, + ), + ); + } + + Future createPangeaUser({required String dob}) async { + final PUserModel newUserModel = await PUserRepo.repoCreatePangeaUser( + userID: userId!, + dateOfBirth: dob, + fullName: fullname, + matrixAccessToken: _matrixAccessToken!, + ); + await _savePUserModel(newUserModel); + } + + String? get _matrixAccessToken => + _pangeaController.matrixState.client.accessToken; + + bool get isPublic => + _pangeaController.userController.userModel?.profile?.publicProfile ?? + false; + + Future get userEmail async { + final List? identifiers = + await _pangeaController.matrixState.client.getAccount3PIDs(); + final matrix.ThirdPartyIdentifier? email = identifiers?.firstWhereOrNull( + (identifier) => + identifier.medium == matrix.ThirdPartyIdentifierMedium.email, + ); + return email?.address; + } +} diff --git a/lib/pangea/controllers/word_net_controller.dart b/lib/pangea/controllers/word_net_controller.dart new file mode 100644 index 000000000..394fd020f --- /dev/null +++ b/lib/pangea/controllers/word_net_controller.dart @@ -0,0 +1,81 @@ +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/repo/word_repo.dart'; +import 'package:http/http.dart' as http; + +import '../models/word_data_model.dart'; +import 'base_controller.dart'; +import 'pangea_controller.dart'; + +class WordController extends BaseController { + late PangeaController _pangeaController; + + final List _wordData = []; + + WordController(PangeaController pangeaController) : super() { + _pangeaController = pangeaController; + } + + WordData? getWordDataLocal({ + required String word, + required String fullText, + required String? userL1, + required String? userL2, + }) => + _wordData.firstWhereOrNull((e) => e.isMatch( + w: word, + f: fullText, + l1: userL1, + l2: userL2, + )); + + Future getWordDataGlobal({ + required String word, + required String fullText, + required String? userL1, + required String? userL2, + }) async { + if (userL1 == null || + userL2 == null || + userL1 == LanguageKeys.unknownLanguage || + userL2 == LanguageKeys.unknownLanguage) { + throw http.Response("", 405); + } + + final WordData? local = getWordDataLocal( + word: word, + fullText: fullText, + userL1: userL1, + userL2: userL2, + ); + + if (local != null) return local; + + final WordData remote = await WordRepo.getWordNetData( + accessToken: await _pangeaController.userController.accessToken, + fullText: fullText, + word: word, + userL1: userL1, + userL2: userL2, + ); + + _addWordData(remote); + + return remote; + } + + _addWordData(WordData w) { + final WordData? local = getWordDataLocal( + word: w.word, + fullText: w.fullText, + userL1: w.userL1, + userL2: w.userL2, + ); + + if (local == null) { + if (_wordData.length > 100) _wordData.clear(); + _wordData.add(w); + setState(); + } + } +} diff --git a/lib/pangea/enum/ReactDialogAlignment.dart b/lib/pangea/enum/ReactDialogAlignment.dart new file mode 100644 index 000000000..959ec429a --- /dev/null +++ b/lib/pangea/enum/ReactDialogAlignment.dart @@ -0,0 +1 @@ +enum DialogAlignment { center, left, right } diff --git a/lib/pangea/enum/bar_chart_view_enum.dart b/lib/pangea/enum/bar_chart_view_enum.dart new file mode 100644 index 000000000..aba0652af --- /dev/null +++ b/lib/pangea/enum/bar_chart_view_enum.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum BarChartViewSelection { + messages, + // vocab, + grammar, +} + +extension BarChartViewSelectionExtension on BarChartViewSelection { + String string(BuildContext context) { + switch (this) { + case BarChartViewSelection.messages: + return L10n.of(context)!.messages; + // case BarChartViewSelection.vocab: + // return L10n.of(context)!.vocab; + case BarChartViewSelection.grammar: + return L10n.of(context)!.grammarAnalytics; + } + } + + IconData get icon { + switch (this) { + case BarChartViewSelection.messages: + return Icons.chat_bubble; + // case BarChartViewSelection.vocab: + // return Icons.abc; + case BarChartViewSelection.grammar: + return Icons.spellcheck_outlined; + } + } +} diff --git a/lib/pangea/enum/construct_type_enum.dart b/lib/pangea/enum/construct_type_enum.dart new file mode 100644 index 000000000..2a7d5583d --- /dev/null +++ b/lib/pangea/enum/construct_type_enum.dart @@ -0,0 +1,30 @@ +enum ConstructType { + grammar, + vocab, +} + +extension ConstructExtension on ConstructType { + String get string { + switch (this) { + case ConstructType.grammar: + return 'grammar'; + case ConstructType.vocab: + return 'vocab'; + } + } +} + +class ConstructTypeUtil { + static ConstructType fromString(String? string) { + switch (string) { + case 'g': + case 'grammar': + return ConstructType.grammar; + case 'v': + case 'vocab': + return ConstructType.vocab; + default: + return ConstructType.vocab; + } + } +} diff --git a/lib/pangea/enum/direction.dart b/lib/pangea/enum/direction.dart new file mode 100644 index 000000000..9fa647749 --- /dev/null +++ b/lib/pangea/enum/direction.dart @@ -0,0 +1,3 @@ +enum EditDirection { + append, remove +} \ No newline at end of file diff --git a/lib/pangea/enum/edit_type.dart b/lib/pangea/enum/edit_type.dart new file mode 100644 index 000000000..5d0a43932 --- /dev/null +++ b/lib/pangea/enum/edit_type.dart @@ -0,0 +1,8 @@ +enum EditType { + itStandard, + igc, + keyboard, + alternativeTranslation, + itGold, + itStart, +} diff --git a/lib/pangea/enum/pop-for.dart b/lib/pangea/enum/pop-for.dart new file mode 100644 index 000000000..77b75b5ac --- /dev/null +++ b/lib/pangea/enum/pop-for.dart @@ -0,0 +1 @@ +enum PopupFor { ownMessage, notOwnMessage, inputBar } diff --git a/lib/pangea/enum/span_choice_type.dart b/lib/pangea/enum/span_choice_type.dart new file mode 100644 index 000000000..f7fac6b9e --- /dev/null +++ b/lib/pangea/enum/span_choice_type.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum SpanChoiceType { + bestCorrection, + distractor, + bestAnswer, +} + +extension SpanChoiceExt on SpanChoiceType { + String get name { + switch (this) { + case SpanChoiceType.bestCorrection: + return "bestCorrection"; + case SpanChoiceType.distractor: + return "distractor"; + case SpanChoiceType.bestAnswer: + return "bestAnswer"; + } + } + + String defaultFeedback(BuildContext context) { + switch (this) { + case SpanChoiceType.bestCorrection: + return L10n.of(context)!.bestCorrectionFeedback; + case SpanChoiceType.distractor: + return L10n.of(context)!.distractorFeedback; + case SpanChoiceType.bestAnswer: + return L10n.of(context)!.bestAnswerFeedback; + } + } + + IconData get icon { + switch (this) { + case SpanChoiceType.bestCorrection: + return Icons.check_circle; + case SpanChoiceType.distractor: + return Icons.cancel; + case SpanChoiceType.bestAnswer: + return Icons.check_circle; + } + } + + Color get color { + switch (this) { + case SpanChoiceType.bestCorrection: + return Colors.green; + case SpanChoiceType.distractor: + return Colors.red; + case SpanChoiceType.bestAnswer: + return Colors.green; + } + } +} diff --git a/lib/pangea/enum/span_data_type.dart b/lib/pangea/enum/span_data_type.dart new file mode 100644 index 000000000..380facead --- /dev/null +++ b/lib/pangea/enum/span_data_type.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum SpanDataTypeEnum { + definition, + practice, + correction, + itStart, +} + +extension SpanDataTypeEnumExt on SpanDataTypeEnum { + String get name { + switch (this) { + case SpanDataTypeEnum.definition: + return "definition"; + case SpanDataTypeEnum.practice: + return "practice"; + case SpanDataTypeEnum.correction: + return "correction"; + case SpanDataTypeEnum.itStart: + return "itStart"; + } + } + + String defaultPrompt(BuildContext context) { + switch (this) { + case SpanDataTypeEnum.definition: + return L10n.of(context)!.definitionDefaultPrompt; + case SpanDataTypeEnum.practice: + return L10n.of(context)!.practiceDefaultPrompt; + case SpanDataTypeEnum.correction: + return L10n.of(context)!.correctionDefaultPrompt; + case SpanDataTypeEnum.itStart: + return L10n.of(context)!.needsItMessage; + } + } +} diff --git a/lib/pangea/enum/time_span.dart b/lib/pangea/enum/time_span.dart new file mode 100644 index 000000000..8e8254730 --- /dev/null +++ b/lib/pangea/enum/time_span.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../models/chart_analytics_model.dart'; + +enum TimeSpan { day, week, month, sixmonths, year } + +extension TimeSpanFunctions on TimeSpan { + String string(BuildContext context) { + switch (this) { + case TimeSpan.day: + return L10n.of(context)!.oneday; + case TimeSpan.week: + return L10n.of(context)!.oneweek; + case TimeSpan.month: + return L10n.of(context)!.onemonth; + case TimeSpan.sixmonths: + return L10n.of(context)!.sixmonth; + case TimeSpan.year: + return L10n.of(context)!.oneyear; + default: + return "Invalid time span"; + } + } + + int get numberOfIntervals { + switch (this) { + case TimeSpan.day: + return 24; + case TimeSpan.week: + return 7; + case TimeSpan.month: + return DateTime.now().month == 2 ? 26 : 28; + case TimeSpan.sixmonths: + return 6; + case TimeSpan.year: + return 12; + } + } + + Duration timeAgo(int index) { + switch (this) { + case TimeSpan.day: + return Duration(hours: index); + case TimeSpan.week: + case TimeSpan.month: + return Duration(days: index); + case TimeSpan.year: + case TimeSpan.sixmonths: + return Duration(days: index * 32); + } + } + + DateTime get cutOffDate { + switch (this) { + case TimeSpan.day: + return DateTime.now().subtract(Duration(hours: numberOfIntervals)); + case TimeSpan.week: + return DateTime.now().subtract(Duration(days: numberOfIntervals)); + case TimeSpan.month: + //PTODO - get onee month agoo + return DateTime.now().subtract(Duration(days: numberOfIntervals)); + case TimeSpan.sixmonths: + //PTODO - get six months ago + return DateTime.now().subtract(Duration(days: numberOfIntervals * 30)); + case TimeSpan.year: + return DateTime.now().subtract(const Duration(days: 365)); + } + } + + String getMapKey(DateTime date) { + switch (this) { + case TimeSpan.day: + return date.hour.toString(); + case TimeSpan.week: + return date.weekday.toString(); + case TimeSpan.month: + return date.day.toString(); + case TimeSpan.sixmonths: + case TimeSpan.year: + return date.month.toString(); + } + } + + /// Note: end is same as start!! + Map get emptyIntervals { + final DateTime now = DateTime.now(); + final List numbers = + List.generate(numberOfIntervals, (index) => index); + final Map map = {}; + + // debugger(when: kDebugMode); + for (final index in numbers) { + final timeAgos = timeAgo(index); + final DateTime end = now.subtract(timeAgos); + // debugger(when: end.isBefore(now.subtract(const Duration(days: 30)))); + final String mapKey = getMapKey(end); + // debugger(when: mapKey.toString() == "5"); + map[mapKey] = TimeSeriesInterval( + start: end, + end: end, + totals: TimeSeriesTotals.empty, + ); + } + // debugger(when: kDebugMode); + return map; + } +} diff --git a/lib/pangea/enum/use_type.dart b/lib/pangea/enum/use_type.dart new file mode 100644 index 000000000..d5fecf5dc --- /dev/null +++ b/lib/pangea/enum/use_type.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../models/choreo_record.dart'; +import '../utils/bot_style.dart'; + +enum UseType { wa, ta, ga, un } + +extension UseTypeMethods on UseType { + String get string => toString().split(".").last; + + String tooltipString(BuildContext context) { + final l10n = L10n.of(context); + if (l10n == null) return string; + switch (this) { + case UseType.ga: + return l10n.gaTooltip; + case UseType.ta: + return l10n.taTooltip; + case UseType.wa: + return l10n.waTooltip; + default: + return l10n.unTooltip; + } + } + + IconData get iconData { + switch (this) { + case UseType.ga: + return Icons.spellcheck_outlined; + case UseType.ta: + return Icons.translate; + case UseType.wa: + return Icons.thumb_up; + default: + return Icons.question_mark_outlined; + } + } + + Widget iconView(BuildContext context, Color color, [int size = 14]) => + Tooltip( + message: tooltipString(context), + child: Icon( + iconData, + color: color, + size: size.toDouble(), + ), + ); + + Widget iconButtonView(BuildContext context, Color color, [int size = 14]) => + Tooltip( + message: tooltipString(context), + child: Icon( + iconData, + color: color, + size: size.toDouble(), + ), + ); + + Widget textView(BuildContext context, [TextStyle? existingStyle]) => Tooltip( + message: tooltipString(context), + child: Text( + string, + style: BotStyle.text( + context, + existingStyle: existingStyle, + italics: true, + ), + textAlign: TextAlign.end, + ), + ); + + static bool isDarkMode(BuildContext context) => + Theme.of(context).brightness == Brightness.dark; + + Color color(BuildContext context) { + switch (this) { + case UseType.ga: + return isDarkMode(context) + ? const Color.fromARGB(255, 157, 234, 172) + : const Color.fromARGB(255, 31, 146, 54); + case UseType.ta: + return isDarkMode(context) + ? const Color.fromARGB(255, 169, 183, 237) + : const Color.fromARGB(255, 38, 59, 141); + case UseType.wa: + return isDarkMode(context) + ? const Color.fromARGB(255, 212, 144, 216) + : const Color.fromARGB(255, 163, 39, 169); + default: + return Theme.of(context).textTheme.bodyLarge!.color ?? Colors.blueGrey; + } + } +} + +UseType useTypeCalculator( + ChoreoRecord? choreoRecord, +) { + if (choreoRecord == null) { + return UseType.un; + } else if (choreoRecord.includedIT) { + return UseType.ta; + } else if (choreoRecord.hasAcceptedMatches) { + return UseType.ga; + } else { + return UseType.wa; + } +} diff --git a/lib/pangea/enum/vocab_proficiency_enum.dart b/lib/pangea/enum/vocab_proficiency_enum.dart new file mode 100644 index 000000000..85a3345ed --- /dev/null +++ b/lib/pangea/enum/vocab_proficiency_enum.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum VocabProficiencyEnum { low, medium, high, unk } + +extension Copy on VocabProficiencyEnum { + String toolTipString(BuildContext context) { + switch (this) { + case VocabProficiencyEnum.low: + return L10n.of(context)!.low; + case VocabProficiencyEnum.medium: + return L10n.of(context)!.medium; + case VocabProficiencyEnum.high: + return L10n.of(context)!.high; + case VocabProficiencyEnum.unk: + return L10n.of(context)!.unknownProficiency; + } + } + + IconData get iconData { + switch (this) { + case VocabProficiencyEnum.low: + return Icons.sentiment_dissatisfied_outlined; + case VocabProficiencyEnum.medium: + return Icons.sentiment_neutral_outlined; + case VocabProficiencyEnum.high: + return Icons.sentiment_satisfied_outlined; + case VocabProficiencyEnum.unk: + return Icons.question_mark_outlined; + } + } +} + +class VocabProficiencyUtil { + static VocabProficiencyEnum proficiency(num numeric) { + if (numeric > 1) { + return VocabProficiencyEnum.high; + } + if (numeric < -1) { + return VocabProficiencyEnum.low; + } + return VocabProficiencyEnum.medium; + } +} diff --git a/lib/pangea/extensions/client_extension.dart b/lib/pangea/extensions/client_extension.dart new file mode 100644 index 000000000..00673150a --- /dev/null +++ b/lib/pangea/extensions/client_extension.dart @@ -0,0 +1,144 @@ +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'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../utils/p_store.dart'; + +extension PangeaClient on Client { + List get classes => rooms.where((e) => e.isPangeaClass).toList(); + + List get classesImTeaching => rooms + .where((e) => + e.isPangeaClass && + e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin) + .toList(); + + List get classesAndExchangesImTeaching => rooms + .where((e) => + (e.isPangeaClass || e.isExchange) && + e.ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin) + .toList(); + + List get classesImIn => rooms + .where((e) => + e.isPangeaClass && + e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) + .toList(); + + List get classesAndExchangesImStudyingIn => rooms + .where((e) => + (e.isPangeaClass || e.isExchange) && + e.ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) + .toList(); + + List get classesAndExchangesImIn => + rooms.where((e) => e.isPangeaClass || e.isExchange).toList(); + + Future> get myTeachers async { + final List teachers = []; + for (final classRoom in classesImIn) { + for (final teacher in await classRoom.teachers) { + if (!teachers.any((e) => e.id == teacher.id)) { + teachers.add(teacher); + } + } + } + return teachers; + } + + Future updateMyLearningAnalyticsForAllClassesImIn([ + PLocalStore? storageService, + ]) async { + try { + final List> updateFutures = []; + for (final classRoom in classesImIn) { + updateFutures + .add(classRoom.updateMyLearningAnalyticsForClass(storageService)); + } + await Future.wait(updateFutures); + } catch (err, s) { + if (kDebugMode) rethrow; + // debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + } + } + + // get analytics room matching targetlanguage + // if not present, create it and invite teachers of that language + // set description to let people know what the hell it is + Future getMyAnalyticsRoom(String langCode) async { + await roomsLoading; + + final Room? analyticsRoom = analyticsRoomLocal(langCode); + + if (analyticsRoom != null) return analyticsRoom; + + return _makeAnalyticsRoom(langCode); + } + + //note: if langCode is null and user has >1 analyticsRooms then this could + //return the wrong one. this is to account for when an exchange might not + //be in a class. + Room? analyticsRoomLocal(String? langCode, [String? userIdParam]) { + final Room? analyticsRoom = rooms.firstWhereOrNull((e) { + return e.isAnalyticsRoom && + e.isAnalyticsRoomOfUser(userIdParam ?? userID!) && + (langCode != null ? e.isMadeForLang(langCode) : true); + }); + if (analyticsRoom != null && + analyticsRoom.membership == Membership.invite) { + final membership = analyticsRoom.membership; + debugger(when: kDebugMode); + analyticsRoom + .join() + .onError((error, stackTrace) => + ErrorHandler.logError(e: error, s: stackTrace)) + .then((value) => analyticsRoom.postLoad()); + return analyticsRoom; + } + return analyticsRoom; + } + + Future _makeAnalyticsRoom(String langCode) async { + final String roomID = await createRoom( + creationContent: { + 'type': PangeaRoomTypes.analytics, + ModelKey.langCode: langCode + }, + name: "$userID $langCode Analytics", + topic: "This room stores learning analytics for $userID.", + invite: [ + ...(await myTeachers).map((e) => e.id).toList(), + // 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); + } + + return getRoomById(roomID)!; + } + + Future getReportsDM(User teacher, Room space) async { + final String roomId = await teacher.startDirectChat( + enableEncryption: false, + ); + space.setSpaceChild( + roomId, + suggested: false, + ); + return getRoomById(roomId)!; + } +} diff --git a/lib/pangea/extensions/my_list_extionsion.dart b/lib/pangea/extensions/my_list_extionsion.dart new file mode 100644 index 000000000..2527c1eb8 --- /dev/null +++ b/lib/pangea/extensions/my_list_extionsion.dart @@ -0,0 +1,8 @@ +extension ReturnShuffle on List { + List shuffleReturn() { + // final List copyList = toList(); + shuffle(); + return this; + // return copyList; + } +} diff --git a/lib/pangea/extensions/pangea_event_extension.dart b/lib/pangea/extensions/pangea_event_extension.dart new file mode 100644 index 000000000..d2ce87227 --- /dev/null +++ b/lib/pangea/extensions/pangea_event_extension.dart @@ -0,0 +1,32 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +extension PangeaEvent on Event { + V getPangeaContent() { + final Map? json = content[type] as Map?; + + if (json == null) { + debugger(when: kDebugMode); + throw Exception("$type event with null content $eventId"); + } + + //PTODO - how does this work? abstract class? + // return V.fromJson(json); + + switch (type) { + case PangeaEventTypes.tokens: + return PangeaMessageTokens.fromJson(json) as V; + case PangeaEventTypes.representation: + return PangeaRepresentation.fromJson(json) as V; + case PangeaEventTypes.choreoRecord: + return ChoreoRecord.fromJson(json) as V; + default: + throw Exception("$type events do not have pangea content"); + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart new file mode 100644 index 000000000..3c333c4d0 --- /dev/null +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -0,0 +1,972 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:matrix/src/utils/space_child.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../config/app_config.dart'; +import '../constants/pangea_event_types.dart'; +import '../enum/construct_type_enum.dart'; +import '../enum/use_type.dart'; +import '../models/choreo_record.dart'; +import '../models/construct_analytics_event.dart'; +import '../models/constructs_analytics_model.dart'; +import '../models/message_data_models.dart'; +import '../models/student_analytics_event.dart'; +import '../models/student_analytics_summary_model.dart'; +import '../utils/p_store.dart'; +import 'client_extension.dart'; + +extension PangeaRoom on Room { + /// the pangeaClass event is listed an importantStateEvent so, if event exists, + /// it's already local. If it's an old class and doesn't, then the class_controller + /// should automatically migrate during this same session, when the space is first loaded + ClassSettingsModel? get classSettings { + try { + if (!isSpace) { + return null; + } + final Map? content = languageSettingsStateEvent?.content; + if (content != null) { + final ClassSettingsModel classSettings = + ClassSettingsModel.fromJson(content); + return classSettings; + } + return null; + } catch (err, s) { + Sentry.addBreadcrumb( + Breadcrumb( + message: "Error in classSettings", + data: {"room": toJson()}, + ), + ); + ErrorHandler.logError(e: err, s: s); + return null; + } + } + + PangeaRoomRules? get pangeaRoomRules { + try { + final Map? content = pangeaRoomRulesStateEvent?.content; + if (content != null) { + final PangeaRoomRules roomRules = PangeaRoomRules.fromJson(content); + return roomRules; + } + return null; + } catch (err, s) { + Sentry.addBreadcrumb( + Breadcrumb( + message: "Error in pangeaRoomRules", + data: {"room": toJson()}, + ), + ); + ErrorHandler.logError(e: err, s: s); + return null; + } + } + + String? get creatorId => getState(EventTypes.RoomCreate)?.senderId; + + ClassSettingsModel? get firstLanguageSettings => + classSettings ?? + firstParentWithState(PangeaEventTypes.classSettings)?.classSettings; + + PangeaRoomRules? get firstRules => + pangeaRoomRules ?? + firstParentWithState(PangeaEventTypes.rules)?.pangeaRoomRules; + + //resolve somehow if multiple rooms have the state? + //check logic + Room? firstParentWithState(String stateType) { + if (![PangeaEventTypes.classSettings, PangeaEventTypes.rules] + .contains(stateType)) { + return null; + } + + for (final parent in pangeaSpaceParents) { + if (parent.getState(stateType) != null) { + return parent; + } + } + for (final parent in pangeaSpaceParents) { + final parentFirstRoom = parent.firstParentWithState(stateType); + if (parentFirstRoom != null) return parentFirstRoom; + } + return null; + } + + IconData? get roomTypeIcon { + if (membership == Membership.invite) return Icons.add; + if (isPangeaClass) return Icons.school; + if (isExchange) return Icons.connecting_airports; + if (isAnalyticsRoom) return Icons.analytics; + if (isDirectChat) return Icons.forum; + return Icons.group; + } + + Text nameAndRoomTypeIcon([TextStyle? textStyle]) => Text.rich( + style: textStyle, + TextSpan( + children: [ + WidgetSpan( + child: Icon(roomTypeIcon), + ), + TextSpan( + text: ' $name', + ), + ], + ), + ); + + /// find any parents and return the rooms + List get immediateClassParents => pangeaSpaceParents + .where( + (element) => element.isPangeaClass, + ) + .toList(); + + List get pangeaSpaceParents => client.rooms + .where( + (r) => r.isSpace, + ) + .where( + (space) => space.spaceChildren.any( + (room) => room.roomId == id, + ), + ) + .toList(); + + bool isChild(String roomId) => + isSpace && spaceChildren.any((room) => room.roomId == roomId); + + bool isFirstOrSecondChild(String roomId) => + isSpace && spaceChildren.any((room) => room.roomId == roomId) || + spaceChildren + .where( + (sc) => sc.roomId != null, + ) + .map( + (sc) => client.getRoomById(sc.roomId!), + ) + .any( + (room) => + room != null && + room.spaceChildren.any((room) => room.roomId == roomId), + ); + + //note this only will return rooms that the user has joined or been invited to + List get childrenAndGrandChildren { + if (!isSpace) return []; + final List kids = []; + for (final child in spaceChildren) { + kids.add(child); + if (child.roomId != null) { + final Room? childRoom = client.getRoomById(child.roomId!); + if (childRoom != null && childRoom.isSpace) { + kids.addAll(childRoom.spaceChildren); + } + } + } + return kids.where((element) => element.roomId != null).toList(); + } + + //this assumes that a user has been invited to all group chats in a space + //it is a janky workaround for determining whether a spacechild is a direct chat + //since the spaceChild object doesn't contain this info. this info is only accessible + //when the user has joined or been invited to the room. direct chats included in + //a space show up in spaceChildren but the user has not been invited to them. + List get childrenAndGrandChildrenDirectChatIds { + final List nonDirectChatRoomIds = childrenAndGrandChildren + .map((e) => client.getRoomById(e.roomId!)) + .where((r) => r != null && !r.isDirectChat) + .map((e) => e!.id) + .toList(); + + return childrenAndGrandChildren + .where( + (child) => + child.roomId != null && + !nonDirectChatRoomIds.contains(child.roomId), + ) + .map((e) => e.roomId) + .cast() + .toList(); + + // return childrenAndGrandChildren + // .where((element) => element.roomId != null) + // .where( + // (child) { + // final room = client.getRoomById(child.roomId!); + // return room == null || room.isDirectChat; + // }, + // ) + // .map((e) => e.roomId) + // .cast() + // .toList(); + } + + //if the user is an admin of the room or any immediate parent of the room + //Question: check parents of parents? + //check logic + bool get isSpaceAdmin { + if (isSpace) return isRoomAdmin; + + for (final parent in pangeaSpaceParents) { + if (parent.isRoomAdmin) { + return true; + } + } + for (final parent in pangeaSpaceParents) { + for (final parent2 in parent.pangeaSpaceParents) { + if (parent2.isRoomAdmin) { + return true; + } + } + } + return false; + } + + bool isUserRoomAdmin(String userId) => getParticipants().any( + (e) => + e.id == userId && + e.powerLevel == ClassDefaultValues.powerLevelOfAdmin, + ); + + bool isUserSpaceAdmin(String userId) { + if (isSpace) return isUserRoomAdmin(userId); + + for (final parent in pangeaSpaceParents) { + if (parent.isUserRoomAdmin(userId)) { + return true; + } + } + return false; + } + + Event? get languageSettingsStateEvent => + getState(PangeaEventTypes.classSettings); + + Event? get pangeaRoomRulesStateEvent => getState(PangeaEventTypes.rules); + + bool get isPangeaClass => isSpace && languageSettingsStateEvent != null; + + bool get isAnalyticsRoom => + getState(EventTypes.RoomCreate)?.content.tryGet('type') == + PangeaRoomTypes.analytics; + + bool get isExchange => + isSpace && + languageSettingsStateEvent == null && + pangeaRoomRulesStateEvent != null; + + bool get isDirectChatWithoutMe => + isDirectChat && !getParticipants().any((e) => e.id == client.userID); + + bool isMadeByUser(String userId) => + getState(EventTypes.RoomCreate)?.senderId == userId; + + bool isMadeForLang(String langCode) => + getState(EventTypes.RoomCreate) + ?.content + .tryGet(ModelKey.langCode) == + langCode; + + bool isAnalyticsRoomOfUser(String userId) => + isAnalyticsRoom && isMadeByUser(userId); + + String get domainString => + AppConfig.defaultHomeserver.replaceAll("matrix.", ""); + + String get classCode { + if (!isSpace) { + for (final Room potentialClassRoom in pangeaSpaceParents) { + if (potentialClassRoom.isPangeaClass) { + return potentialClassRoom.classCode; + } + } + return "Not in a class!"; + } + + return canonicalAlias.replaceAll(":$domainString", "").replaceAll("#", ""); + } + + StudentAnalyticsEvent? _getStudentAnalyticsLocal(String studentId) { + if (!isSpace) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "calling getStudentAnalyticsLocal on non-space room", + s: StackTrace.current, + ); + return null; + } + + final Event? matrixEvent = getState( + PangeaEventTypes.studentAnalyticsSummary, + studentId, + ); + + return matrixEvent != null + ? StudentAnalyticsEvent(event: matrixEvent) + : null; + } + + Future getStudentAnalytics( + String studentId, { + bool forcedUpdate = false, + }) async { + try { + debugPrint("getStudentAnalytics $studentId"); + if (!isSpace) { + debugger(when: kDebugMode); + throw Exception("calling getStudentAnalyticsLocal on non-space room"); + } + StudentAnalyticsEvent? localEvent = _getStudentAnalyticsLocal(studentId); + + if (localEvent == null) { + await postLoad(); + localEvent = _getStudentAnalyticsLocal(studentId); + } + + if (studentId == client.userID && localEvent == null) { + final Event? matrixEvent = await _createStudentAnalyticsEvent(); + if (matrixEvent != null) { + localEvent = StudentAnalyticsEvent(event: matrixEvent); + } + } + + return localEvent; + } catch (err) { + debugger(when: kDebugMode); + rethrow; + } + } + + void checkClass() { + if (!isSpace) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb(message: "calling room.students with non-class room"), + ); + } + } + + List get students { + checkClass(); + return isSpace + ? getParticipants() + .where( + (e) => + e.powerLevel < ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList() + : getParticipants(); + } + + Future> get teachers async { + checkClass(); + final List participants = await requestParticipants(); + return isSpace + ? participants + .where( + (e) => + e.powerLevel == ClassDefaultValues.powerLevelOfAdmin && + e.id != BotName.byEnvironment, + ) + .toList() + : participants; + } + + /// if [studentIds] is null, returns all students + Future> getClassAnalytics([ + List? studentIds, + ]) async { + await postLoad(); + await requestParticipants(); + final List> sassFutures = []; + final List filteredIds = students + .where( + (element) => studentIds == null || studentIds.contains(element.id), + ) + .map((e) => e.id) + .toList(); + for (final id in filteredIds) { + sassFutures.add( + getStudentAnalytics( + id, + ), + ); + } + return Future.wait(sassFutures); + } + + /// if [isSpace] + /// for all child chats, call _getChatAnalyticsGlobal and merge results + /// else + /// get analytics from pangea chat server + /// do any needed conversion work + /// save RoomAnalytics object to PangeaEventTypes.analyticsSummary event + Future _createStudentAnalyticsEvent() async { + try { + if (!pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { + ErrorHandler.logError( + m: "null powerLevels in createStudentAnalytics", + s: StackTrace.current, + ); + return null; + } + if (client.userID == null) { + debugger(when: kDebugMode); + throw Exception("null userId in createStudentAnalytics"); + } + await postLoad(); + final String eventId = await client.setRoomStateWithKey( + id, + PangeaEventTypes.studentAnalyticsSummary, + client.userID!, + StudentAnalyticsSummary( + // studentId: client.userID!, + lastUpdated: DateTime.now(), + messages: [], + ).toJson(), + ); + final Event? event = await getEventById(eventId); + + if (event == null) { + debugger(when: kDebugMode); + throw Exception( + "null event after creation with eventId $eventId in createStudentAnalytics", + ); + } + return event; + } catch (err, stack) { + ErrorHandler.logError(e: err, s: stack, data: powerLevels); + return null; + } + } + + /// for each chat in class + /// get timeline back to january 15 + /// get messages + /// discard timeline + /// save messages to StudentAnalyticsSummary + Future updateMyLearningAnalyticsForClass([ + PLocalStore? storageService, + ]) async { + try { + final String migratedAnalyticsKey = + "MIGRATED_ANALYTICS_KEY${id.localpart}"; + + if (storageService?.read(migratedAnalyticsKey) ?? false) return; + + if (!isPangeaClass) { + throw Exception( + "In updateMyLearningAnalyticsForClass with room that is not not a class", + ); + } + + if (client.userID == null) { + debugger(when: kDebugMode); + return; + } + + final StudentAnalyticsEvent? myAnalEvent = + await getStudentAnalytics(client.userID!); + + if (myAnalEvent == null) { + debugPrint("null analytcs event for $id"); + if (pangeaCanSendEvent(PangeaEventTypes.studentAnalyticsSummary)) { + // debugger(when: kDebugMode); + } + return; + } + + myAnalEvent.bulkUpdate(await _messageListForAllChildChats); + + storageService?.save(migratedAnalyticsKey, true); + } catch (err, s) { + if (kDebugMode) rethrow; + // debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + } + } + + Future> get _messageListForAllChildChats async { + try { + if (!isSpace) return []; + final List spaceChats = spaceChildren + .where((e) => e.roomId != null) + .map((e) => client.getRoomById(e.roomId!)) + .where((element) => element != null) + .cast() + .where((element) => !element.isSpace) + .toList(); + + if (spaceChildren.length != spaceChats.length) { + // debugger(when: kDebugMode); + ErrorHandler.logError( + m: "spaceChildren.length > chats.length in updateMyLearningAnalyticsForClass", + ); + } + + final List>> msgListFutures = []; + for (final chat in spaceChats) { + msgListFutures.add(chat._messageListForChat); + } + final List> msgLists = + await Future.wait(msgListFutures); + + final List joined = []; + for (final msgList in msgLists) { + joined.addAll(msgList); + } + return joined; + } catch (err) { + // debugger(when: kDebugMode); + rethrow; + } + } + + Future> get _messageListForChat async { + try { + int numberOfSearches = 0; + + if (isSpace) { + throw Exception( + "In messageListForChat with room that is not a chat", + ); + } + final Timeline timeline = await getTimeline(); + + while (timeline.canRequestHistory && numberOfSearches < 50) { + await timeline.requestHistory(historyCount: 100); + numberOfSearches += 1; + } + if (timeline.canRequestHistory) { + debugger(when: kDebugMode); + } + + final List msgs = []; + for (final event in timeline.events) { + if (event.senderId == client.userID && + event.type == EventTypes.Message) { + if (event.content['msgtype'] == MessageTypes.Text) { + final PangeaMessageEvent pMsgEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: true, + selected: false, + ); + msgs.add( + RecentMessageRecord( + eventId: event.eventId, + chatId: id, + useType: pMsgEvent.useType, + time: event.originServerTs, + ), + ); + } else { + debugger(when: kDebugMode); + } + } + } + return msgs; + } catch (err, s) { + if (kDebugMode) rethrow; + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return []; + } + } + + Future sendPangeaEvent({ + required Map content, + required String parentEventId, + required String type, + }) async { + try { + debugPrint("creating $type child for $parentEventId"); + Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); + if (parentEventId.contains("web")) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "sendPangeaEvent with likely invalid parentEventId $parentEventId", + ), + ); + } + final Map repContent = { + // what is the functionality of m.reference? + "m.relates_to": {"rel_type": type, "event_id": parentEventId}, + type: content, + }; + + final String? newEventId = await sendEvent(repContent, type: type); + + if (newEventId == null) { + debugger(when: kDebugMode); + } + + //PTODO - handle the frequent case of a null newEventId + final Event? newEvent = await getEventById(newEventId!); + + if (newEvent == null) { + debugger(when: kDebugMode); + } + + return newEvent; + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: err, + s: stack, + data: { + "type": type, + "parentEventId": parentEventId, + "content": content, + }, + ); + return null; + } + } + + ConstructEvent? _vocabEventLocal(String lemma) { + if (!isAnalyticsRoom) throw Exception("not an analytics room"); + + final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma); + + return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null; + } + + bool get isRoomOwner => + getState(EventTypes.RoomCreate)?.senderId == client.userID; + + Future vocabEvent( + String lemma, + ConstructType type, [ + bool makeIfNull = false, + ]) async { + try { + if (!isAnalyticsRoom) throw Exception("not an analytics room"); + + ConstructEvent? localEvent = _vocabEventLocal(lemma); + + if (localEvent != null) return localEvent; + + await postLoad(); + localEvent = _vocabEventLocal(lemma); + + if (localEvent == null && isRoomOwner && makeIfNull) { + final Event matrixEvent = await _createVocabEvent(lemma, type); + localEvent = ConstructEvent(event: matrixEvent); + } + + return localEvent!; + } catch (err) { + debugger(when: kDebugMode); + rethrow; + } + } + + Future saveConstructUsesSameLemma( + String lemma, + ConstructType type, + List lemmaUses, + ) async { + final ConstructEvent? localEvent = _vocabEventLocal(lemma); + + if (localEvent == null) { + final json = + ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(); + await client.setRoomStateWithKey( + id, + PangeaEventTypes.vocab, + lemma, + ConstructUses(lemma: lemma, type: type, uses: lemmaUses).toJson(), + ); + } else { + localEvent.addAll(lemmaUses); + await updateStateEvent(localEvent.event); + } + } + + Future> get allConstructEvents async { + await postLoad(); + + return states[PangeaEventTypes.vocab] + ?.values + .map((Event event) => ConstructEvent(event: event)) + .toList() + .cast() ?? + []; + } + + Future _createVocabEvent(String lemma, ConstructType type) async { + try { + if (!isRoomOwner) { + throw Exception( + "Tried to create vocab event in room where user is not owner", + ); + } + final String eventId = await client.setRoomStateWithKey( + id, + PangeaEventTypes.vocab, + lemma, + ConstructUses(lemma: lemma, type: type).toJson(), + ); + final Event? event = await getEventById(eventId); + + if (event == null) { + debugger(when: kDebugMode); + throw Exception( + "null event after creation with eventId $eventId in _createVocabEvent", + ); + } + return event; + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack, data: powerLevels); + rethrow; + } + } + + Future makeSureTeachersAreInvitedToAnalyticsRoom() async { + try { + if (!isAnalyticsRoom) { + throw Exception("not an analytics room"); + } + if (!participantListComplete) { + await requestParticipants(); + } + final toAdd = [ + ...getParticipants([Membership.invite, Membership.join]) + .map((e) => e.id) + .toList(), + BotName.byEnvironment, + ]; + for (final teacher in await client.myTeachers) { + if (!toAdd.contains(teacher.id)) { + debugPrint("inviting ${teacher.id} to analytics room"); + await invite(teacher.id); + } + } + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + /// update state event and return eventId + Future updateStateEvent(Event stateEvent) { + return client.setRoomStateWithKey( + id, + stateEvent.type, + stateEvent.stateKey!, + stateEvent.content, + ); + } + + bool canIAddSpaceChild(Room? room) { + if (!isSpace) { + ErrorHandler.logError( + m: "should not call canIAddSpaceChildren on non-space room", + data: toJson(), + s: StackTrace.current, + ); + return false; + } + if (!pangeaCanSendEvent(EventTypes.spaceChild) && !isRoomAdmin) { + return false; + } + if (room == null) { + return isRoomAdmin || (pangeaRoomRules?.isCreateRooms ?? false); + } + if (room.isExchange) { + return isRoomAdmin; + } + if (!room.isSpace) { + return pangeaRoomRules?.isCreateRooms ?? false; + } + if (room.isPangeaClass) { + ErrorHandler.logError( + m: "should not call canIAddSpaceChild with class", + data: room.toJson(), + s: StackTrace.current, + ); + return false; + } + return false; + } + + bool get canIAddSpaceParents => + isRoomAdmin || pangeaCanSendEvent(EventTypes.spaceParent); + + bool get showClassEditOptions => isSpace && isRoomAdmin; + + bool get canDelete => isSpaceAdmin; + + bool get isRoomAdmin => ownPowerLevel == ClassDefaultValues.powerLevelOfAdmin; + + //overriding the default canSendEvent to check power levels + bool pangeaCanSendEvent(String eventType) { + final powerLevelsMap = getState(EventTypes.RoomPowerLevels)?.content; + if (powerLevelsMap == null) return 0 <= ownPowerLevel; + final pl = powerLevelsMap + .tryGetMap('events') + ?.tryGet(eventType) ?? + 100; + return ownPowerLevel >= pl; + } + + Future setClassPowerlLevels() async { + try { + if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { + return; + } + final currentPower = getState(EventTypes.RoomPowerLevels); + final Map? currentPowerContent = + currentPower!.content["events"] as Map?; + final spaceChildPower = currentPowerContent?[EventTypes.spaceChild]; + final studentAnalyticsPower = + currentPowerContent?[PangeaEventTypes.studentAnalyticsSummary]; + + if (spaceChildPower == null || studentAnalyticsPower == null) { + currentPowerContent!["events"][EventTypes.spaceChild] = 0; + currentPowerContent["events"] + [PangeaEventTypes.studentAnalyticsSummary] = 0; + + await client.setRoomStateWithKey( + id, + EventTypes.RoomPowerLevels, + currentPower.stateKey ?? "", + currentPowerContent, + ); + } + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s, data: toJson()); + } + } + + Future pangeaSendTextEvent( + String message, { + String? txid, + Event? inReplyTo, + String? editEventId, + bool parseMarkdown = false, + bool parseCommands = false, + String msgtype = MessageTypes.Text, + String? threadRootEventId, + String? threadLastEventId, + PangeaRepresentation? originalSent, + PangeaRepresentation? originalWritten, + PangeaMessageTokens? tokensSent, + PangeaMessageTokens? tokensWritten, + ChoreoRecord? choreo, + UseType? useType, + }) { + // if (parseCommands) { + // return client.parseAndRunCommand(this, message, + // inReplyTo: inReplyTo, + // editEventId: editEventId, + // txid: txid, + // threadRootEventId: threadRootEventId, + // threadLastEventId: threadLastEventId); + // } + final event = { + 'msgtype': msgtype, + 'body': message, + ModelKey.choreoRecord: choreo?.toJson(), + ModelKey.originalSent: originalSent?.toJson(), + ModelKey.originalWritten: originalWritten?.toJson(), + ModelKey.tokensSent: tokensSent?.toJson(), + ModelKey.tokensWritten: tokensWritten?.toJson(), + ModelKey.useType: useType?.string, + }; + // if (parseMarkdown) { + // final html = markdown(event['body'], + // getEmotePacks: () => getImagePacksFlat(ImagePackUsage.emoticon), + // getMention: getMention); + // // if the decoded html is the same as the body, there is no need in sending a formatted message + // if (HtmlUnescape().convert(html.replaceAll(RegExp(r'
\n?'), '\n')) != + // event['body']) { + // event['format'] = 'org.matrix.custom.html'; + // event['formatted_body'] = html; + // } + // } + return sendEvent( + event, + txid: txid, + inReplyTo: inReplyTo, + editEventId: editEventId, + threadRootEventId: threadRootEventId, + threadLastEventId: threadLastEventId, + ); + } + + bool get locked { + final Event? powerLevels = getState(EventTypes.RoomPowerLevels); + if (powerLevels == null) { + return false; + } + final Map powerLevelsContent = Map.from( + powerLevels.content, + ); + + if (!isSpace) { + return powerLevelsContent['events_default'] != null && + powerLevelsContent['events_default'] >= 100; + } + + final List children = spaceChildren + .map( + (child) => + child.roomId != null ? client.getRoomById(child.roomId!) : null, + ) + .toList(); + + for (final Room? child in children) { + if (child != null && !child.locked) { + return false; + } + } + return true; + } + + Future suggestedInSpace(Room space) async { + try { + final Map resp = + await client.getRoomStateWithKey(space.id, EventTypes.spaceChild, id); + return resp.containsKey('suggested') ? resp['suggested'] as bool : true; + } catch (err) { + ErrorHandler.logError( + e: "Failed to fetch suggestion status of room $id in space ${space.id}", + s: StackTrace.current, + ); + return true; + } + } + + Future setSuggestedInSpace(bool suggest, Room space) async { + try { + await space.setSpaceChild(id, suggested: suggest); + } catch (err) { + ErrorHandler.logError( + e: "Failed to set suggestion status of room $id in space ${space.id}", + s: StackTrace.current, + ); + return; + } + } +} diff --git a/lib/pangea/guard/p_vguard.dart b/lib/pangea/guard/p_vguard.dart new file mode 100644 index 000000000..098ddc75b --- /dev/null +++ b/lib/pangea/guard/p_vguard.dart @@ -0,0 +1,150 @@ +import 'dart:async'; + +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../controllers/pangea_controller.dart'; + +class PAuthGaurd { + static bool isPublicLeaving = false; + static PangeaController? pController; + + static FutureOr loggedInRedirect( + BuildContext context, + GoRouterState state, + ) async { + if (pController != null) { + final bool setDob = await pController! + .userController.isUserDataAvailableAndDateOfBirthSet; + if (Matrix.of(context).client.isLogged()) { + return !setDob ? '/rooms/user_age' : '/rooms'; + } + return null; + } else { + debugPrint("controller is null in pguard check"); + Matrix.of(context).client.isLogged() ? '/rooms' : null; + } + return null; + } + + static FutureOr loggedOutRedirect( + BuildContext context, + GoRouterState state, + ) async { + if (pController != null) { + final bool setDob = await pController! + .userController.isUserDataAvailableAndDateOfBirthSet; + return !Matrix.of(context).client.isLogged() + ? '/home' + : !setDob + ? '/rooms/user_age' + : null; + } else { + debugPrint("controller is null in pguard check"); + return Matrix.of(context).client.isLogged() ? null : '/home'; + } + } + + // static const defaultRoute = '/home'; + + // static Future onPublicEnter() async { + // final bool setDob = + // await pController!.userController.isUserDataAvailableAndDateOfBirthSet; + // if (_isLogged != null && _isLogged! && setDob) { + // vRedirector.to('/rooms'); + // } + // } + + // static Future onPublicUpdate(VRedirector vRedirector) async { + // final bool setDob = + // await pController!.userController.isUserDataAvailableAndDateOfBirthSet; + // if (_isLogged != null && _isLogged! && setDob) { + // vRedirector.to('/rooms'); + // } + // bool oldHaveParms = false; + + // final bool haveData = vRedirector.previousVRouterData != null; + // if (haveData) { + // final bool isPublicRoute = + // vRedirector.newVRouterData!.url!.startsWith(defaultRoute); + // if (!isPublicRoute) { + // return; + // } + // oldHaveParms = + // vRedirector.previousVRouterData!.queryParameters.isNotEmpty; + // if (oldHaveParms) { + // if (vRedirector.newVRouterData!.queryParameters.isEmpty) { + // vRedirector.to( + // vRedirector.toUrl!, + // queryParameters: vRedirector.previousVRouterData!.queryParameters, + // ); + // } + // } + // } + + // return; + // } + + // static Future onPublicLeave( + // VRedirector vRedirector, + // Function(Map onLeave) callback, + // ) async { + // final bool haveData = vRedirector.previousVRouterData != null; + + // if (haveData) { + // try { + // if (vRedirector.previousVRouterData!.queryParameters['redirect'] == + // 'true') { + // if (!isPublicLeaving) { + // isPublicLeaving = true; + // vRedirector.to( + // vRedirector.previousVRouterData!.queryParameters['redirectPath']!, + // ); + // } + // } + // } catch (e, s) { + // ErrorHandler.logError(e: e, s: s); + // } + // } + // return; + // } + + // static Future onPrivateUpdate(VRedirector vRedirector) async { + // if (_isLogged == null) { + // return; + // } + // final Map redirectParm = {}; + // final bool haveData = vRedirector.newVRouterData != null; + // if (haveData) { + // if (vRedirector.newVRouterData!.queryParameters.isNotEmpty) { + // redirectParm['redirect'] = 'true'; + // redirectParm['redirectPath'] = vRedirector.newVRouterData!.url!; + // } + // } + // if (!_isLogged!) { + // debugPrint("onPrivateUpdate with user not logged in"); + // ErrorHandler.logError( + // e: Exception("onPrivateUpdate with user not logged in"), + // s: StackTrace.current, + // ); + // // vRedirector.to(defaultRoute, queryParameters: redirectParm); + // } else { + // if (pController != null) { + // if (!await pController! + // .userController.isUserDataAvailableAndDateOfBirthSet) { + // debugPrint("reroute to user_age"); + // vRedirector.to( + // '/home/connect/user_age', + // queryParameters: redirectParm, + // ); + // } + // } else { + // debugPrint("controller is null in pguard check"); + // } + // } + + // isPublicLeaving = false; + // return; + // } +} diff --git a/lib/pangea/models/analytics_model_old.dart b/lib/pangea/models/analytics_model_old.dart new file mode 100644 index 000000000..2c51a1f3a --- /dev/null +++ b/lib/pangea/models/analytics_model_old.dart @@ -0,0 +1,100 @@ +// import 'dart:convert'; + +// class UserTimeSeriesInterval { +// String? userId; +// int? taTotal; +// int? gaTotal; +// int? waTotal; + +// UserTimeSeriesInterval({ +// required this.userId, +// required this.taTotal, +// required this.gaTotal, +// required this.waTotal, +// }); + +// Map toJson() => +// {"usr": userId, "ta": taTotal, "ga": gaTotal, "wa": waTotal}; + +// factory UserTimeSeriesInterval.fromJson(json) => UserTimeSeriesInterval( +// userId: json["usr"], +// taTotal: json["ta"], +// gaTotal: json["ga"], +// waTotal: json["wa"], +// ); +// } + +// class TimeSeriesInterval { +// DateTime start; +// DateTime end; +// List users; + +// TimeSeriesInterval({ +// required this.start, +// required this.end, +// required this.users, +// }); + +// Map toJson() => { +// "strt": start, +// "end": end, +// "usrs": jsonEncode(users.map((e) => e.toJson()).toList()) +// }; + +// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( +// start: json["strt"], +// end: json["end"], +// users: ((jsonDecode(json["usrs"]) as Iterable) +// .map((e) => UserTimeSeriesInterval.fromJson(e)) +// .toList() +// .cast()), +// ); +// } + +// class RoomAnalyticsSummary { +// List monthlyTotalsForAllTime; +// List dailyTotalsForLast30Days; +// List hourlyTotalsForLast24Hours; + +// DateTime? updatedAt; + +// RoomAnalyticsSummary({ +// required this.monthlyTotalsForAllTime, +// required this.dailyTotalsForLast30Days, +// required this.hourlyTotalsForLast24Hours, +// }); + +// Map toJson() => { +// "mnths": +// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()), +// "dys": jsonEncode( +// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()), +// "hrs": jsonEncode( +// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()), +// }; + +// factory RoomAnalyticsSummary.fromJson(json) => RoomAnalyticsSummary( +// monthlyTotalsForAllTime: (jsonDecode(json["mnths"]) as Iterable) +// .map((e) => TimeSeriesInterval.fromJson(e)) +// .toList() +// .cast(), +// dailyTotalsForLast30Days: (jsonDecode(json["dys"]) as Iterable) +// .map((e) => TimeSeriesInterval.fromJson(e)) +// .toList() +// .cast(), +// hourlyTotalsForLast24Hours: (jsonDecode(json["hrs"]) as Iterable) +// .map((e) => TimeSeriesInterval.fromJson(e)) +// .toList() +// .cast(), +// ); +// } + +// class UserDirectChatAnalyticsSummary { +// // directChatRoomIds and analytics for those rooms +// // updated by user; +// Map? directChatSummaries; + +// Map toJson() => {}; +// } + +// // maybe search how to do date ranges in dart \ No newline at end of file diff --git a/lib/pangea/models/analytics_model_older.dart b/lib/pangea/models/analytics_model_older.dart new file mode 100644 index 000000000..2ee817f0b --- /dev/null +++ b/lib/pangea/models/analytics_model_older.dart @@ -0,0 +1,124 @@ +// import 'dart:convert'; + +// class ChatTimeSeriesInterval { +// String? chatId; +// int? taTotal; +// int? gaTotal; +// int? waTotal; + +// ChatTimeSeriesInterval({ +// required this.chatId, +// required this.taTotal, +// required this.gaTotal, +// required this.waTotal, +// }); + +// Map toJson() => +// {"id": chatId, "ta": taTotal, "ga": gaTotal, "wa": waTotal}; + +// factory ChatTimeSeriesInterval.fromJson(json) => ChatTimeSeriesInterval( +// chatId: json["id"], +// taTotal: json["ta"], +// gaTotal: json["ga"], +// waTotal: json["wa"], +// ); +// } + +// class TimeSeriesInterval { +// DateTime start; +// DateTime end; +// List chats; + +// TimeSeriesInterval({ +// required this.start, +// required this.end, +// required this.chats, +// }); + +// Map toJson() => { +// "strt": start, +// "end": end, +// "usrs": jsonEncode(chats.map((e) => e.toJson()).toList()) +// }; + +// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( +// start: DateTime(json["strt"]), +// end: DateTime(json["end"]), +// chats: ((jsonDecode(json["usrs"]) as Iterable) +// .map((e) => ChatTimeSeriesInterval.fromJson(e)) +// .toList() +// .cast()), +// ); +// } + +// // class RecentMessageRecord { +// // String eventId; +// // String typeOfUse; +// // String time; +// // } + +// class StudentAnalyticsSummary { +// /// event statekey = studentId +// // String studentId; + +// List monthlyTotalsForAllTime; +// List dailyTotalsForLast30Days; +// List hourlyTotalsForLast24Hours; + +// // List messages; + +// DateTime lastLogin; +// DateTime lastMessage; + +// DateTime lastUpdated; + +// StudentAnalyticsSummary({ +// // required this.studentId, +// required this.monthlyTotalsForAllTime, +// required this.dailyTotalsForLast30Days, +// required this.hourlyTotalsForLast24Hours, +// required this.lastLogin, +// required this.lastMessage, +// required this.lastUpdated, +// }); + +// // static const _studentIdKey = 'usr'; +// static const _monthKey = "mnths"; +// static const _dayKey = "dys"; +// static const _hoursKey = "hrs"; +// static const _lastLoginKey = "lgn"; +// static const _lastMessageKey = "msg"; +// static const _lastUpdated = "lupt"; + +// Map toJson() => { +// _monthKey: +// jsonEncode(monthlyTotalsForAllTime.map((e) => e.toJson()).toList()), +// _dayKey: jsonEncode( +// dailyTotalsForLast30Days.map((e) => e.toJson()).toList()), +// _hoursKey: jsonEncode( +// hourlyTotalsForLast24Hours.map((e) => e.toJson()).toList()), +// // _studentIdKey: studentId, +// _lastLoginKey: lastLogin.toIso8601String(), +// _lastMessageKey: lastMessage.toIso8601String(), +// _lastUpdated: lastUpdated.toIso8601String() +// }; + +// factory StudentAnalyticsSummary.fromJson(json) => StudentAnalyticsSummary( +// // studentId: json[_studentIdKey], +// monthlyTotalsForAllTime: (jsonDecode(json[_monthKey]) as Iterable) +// .map((e) => TimeSeriesInterval.fromJson(e)) +// .toList() +// .cast(), +// dailyTotalsForLast30Days: (jsonDecode(json[_dayKey]) as Iterable) +// .map((e) => TimeSeriesInterval.fromJson(e)) +// .toList() +// .cast(), +// hourlyTotalsForLast24Hours: (jsonDecode(json[_hoursKey]) as Iterable) +// .map((e) => TimeSeriesInterval.fromJson(e)) +// .toList() +// .cast(), +// lastLogin: DateTime(json[_lastLoginKey]), +// lastUpdated: DateTime(json[_lastLoginKey]), +// lastMessage: DateTime(json[_lastMessageKey]), +// ); +// } diff --git a/lib/pangea/models/analytics_model_oldest.dart b/lib/pangea/models/analytics_model_oldest.dart new file mode 100644 index 000000000..f075c1fe4 --- /dev/null +++ b/lib/pangea/models/analytics_model_oldest.dart @@ -0,0 +1,77 @@ +// import 'dart:convert'; + +// class BaseDataModel { +// late int spanTotal; +// late int spanIT; +// late int spanIGC; +// late int spanDirect; + +// BaseDataModel(Map json) { +// fromJson(json); +// } + +// fromJson(Map json) { +// spanTotal = json["total"]; +// spanIT = json["it"]; +// spanIGC = json["igc"]; +// spanDirect = json["direct"]; +// } +// } + +// class TimeSeriesInterval extends BaseDataModel { +// //note: always in UTC +// late DateTime start; +// late DateTime end; + +// TimeSeriesInterval(Map json) : super(json) { +// fromJsonTimeSeriesInterval(json); +// } + +// fromJsonTimeSeriesInterval(Map json) { +// start = DateTime.parse(json["start"]); +// end = DateTime.parse(json["end"]); +// } +// } + +// class chartAnalytics extends BaseDataModel { +// late String id; +// late int allTotal; +// late int allIT; +// late int allIGC; +// late int allDirect; +// late String timeSpan; +// late DateTime fetchedAt; +// late List? chatIds; +// late List? userIds; +// late List? classIds; +// late List timeSeries; + +// chartAnalytics(Map json) : super(json) { +// fromJsonchartAnalytics(json); +// fetchedAt = DateTime.now(); +// } + +// fromJsonchartAnalytics(Map json) { +// id = json["id"]; +// timeSpan = json["timespan"]; +// allTotal = json["alltime"]["total"]; +// allIT = json["alltime"]["it"]; +// allIGC = json["alltime"]["igc"]; +// allDirect = json["alltime"]["direct"]; +// timeSeries = (json["timeseries"] as Iterable) +// .map( +// (timeSeriesJsonEntry) => TimeSeriesInterval(timeSeriesJsonEntry), +// ) +// .toList() +// .cast(); +// chatIds = json["chats"] != null && json["chats"] != [] +// ? (json["chats"] as List).cast() +// : null; +// userIds = json["users"] != null && json["userIds"] != [] +// ? (json["users"] as List).cast() +// : null; +// classIds = json["classes"] != null && json["classes"] != [] +// ? (json["classes"] as List).cast() +// : null; +// } +// } diff --git a/lib/pangea/models/base_subscription_info.dart b/lib/pangea/models/base_subscription_info.dart new file mode 100644 index 000000000..604abf6e9 --- /dev/null +++ b/lib/pangea/models/base_subscription_info.dart @@ -0,0 +1,74 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/repo/subscription_repo.dart'; +import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; + +class SubscriptionInfo { + PangeaController pangeaController; + List availableSubscriptions = []; + String? currentSubscriptionId; + SubscriptionDetails? currentSubscription; + // Gabby - is it necessary to store appIds for each platform? + SubscriptionAppIds? appIds; + List? allProducts; + final SubscriptionPlatform platform = SubscriptionPlatform(); + List allEntitlements = []; + DateTime? expirationDate; + + bool get hasSubscribed => allEntitlements.isNotEmpty; + + SubscriptionInfo({ + required this.pangeaController, + }) : super(); + + Future configure() async {} + + //TO-DO - hey Gabby this file feels like it could be reorganized. i'd like to + // 1) move these api calls to a class in a file in repo and + // 2) move the url to the urls file. + // 3) any stateful info to the subscription controller + // let's discuss before you make the changes though + // maybe you had some reason for this organization + + /* + Fetch App Ids for each RC app (iOS, Android, and Stripe). Used to determine which app a user + with an active subscription purchased that subscription. + */ + Future setAppIds() async { + if (appIds != null) return; + appIds = await SubscriptionRepo.getAppIds(); + } + + Future setAllProducts() async { + if (allProducts != null) return; + allProducts = await SubscriptionRepo.getAllProducts(); + } + + bool get currentSubscriptionIsPromotional => + currentSubscriptionId?.startsWith("rc_promo") ?? false; + + bool get isLifetimeSubscription => + currentSubscriptionIsPromotional && + expirationDate != null && + expirationDate!.isAfter(DateTime(2100)); + + String? get purchasePlatformDisplayName { + if (currentSubscription?.appId == null) return null; + return appIds?.appDisplayName(currentSubscription!.appId!); + } + + bool get purchasedOnWeb => + (currentSubscription != null && appIds != null) && + (currentSubscription?.appId == appIds?.stripeId); + + bool get currentPlatformMatchesPurchasePlatform => + (currentSubscription != null && appIds != null) && + (currentSubscription?.appId == appIds?.currentAppId); + + void resetSubscription() { + currentSubscription = null; + currentSubscriptionId = null; + } + + Future setCustomerInfo() async {} +} diff --git a/lib/pangea/models/chart_analytics_model.dart b/lib/pangea/models/chart_analytics_model.dart new file mode 100644 index 000000000..6c71329d5 --- /dev/null +++ b/lib/pangea/models/chart_analytics_model.dart @@ -0,0 +1,140 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/enum/time_span.dart'; +import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; +import 'package:flutter/foundation.dart'; + +import '../enum/use_type.dart'; + +class TimeSeriesTotals { + int ta; + int ga; + int wa; + int un; + + int get all => ta + ga + wa + un; + + TimeSeriesTotals({ + required this.ta, + required this.ga, + required this.wa, + required this.un, + }); + + Map toJson() => { + UseType.ta.string: ta, + UseType.ga.string: ga, + UseType.wa.string: wa, + UseType.un.string: un + }; + + factory TimeSeriesTotals.fromJson(json) => TimeSeriesTotals( + ta: json[UseType.ta.string], + ga: json[UseType.ga.string], + wa: json[UseType.wa.string], + un: json[UseType.un.string], + ); + + static get empty => TimeSeriesTotals(ta: 0, ga: 0, wa: 0, un: 0); + + int get taPercent => all != 0 ? (ta / all * 100).round() : 0; + int get gaPercent => all != 0 ? (ga / all * 100).round() : 0; + int get waPercent => all != 0 ? (wa / all * 100).round() : 0; + int get unPercent => all != 0 ? (un / all * 100).round() : 0; + + void increment(RecentMessageRecord msg) { + switch (msg.useType) { + case UseType.ta: + ta += 1; + break; + case UseType.wa: + wa += 1; + break; + case UseType.ga: + ga += 1; + break; + case UseType.un: + un += 1; + break; + default: + debugger(when: kDebugMode); + debugPrint("message with bad type ${msg.toJson()}"); + } + } +} + +class TimeSeriesInterval { + DateTime start; + DateTime end; + TimeSeriesTotals totals; + + TimeSeriesInterval({ + required this.start, + required this.end, + required this.totals, + }); + + Map toJson() => { + "strt": start.toIso8601String(), + "end": end.toIso8601String(), + "totals": totals.toJson() + }; + + factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval( + start: DateTime.parse(json["strt"]), + end: DateTime.parse(json["end"]), + totals: TimeSeriesTotals.fromJson(json["totals"]), + ); +} + +class ChartAnalyticsModel { + final TimeSpan timeSpan; + final TimeSeriesTotals totals = TimeSeriesTotals.empty; + final List msgs; + final String? chatId; + + late DateTime fetchedAt; + late List timeSeries; + DateTime? lastMessage; + + ChartAnalyticsModel({ + required this.timeSpan, + required this.msgs, + this.chatId, + }) { + fetchedAt = DateTime.now(); + calculate(); + } + + bool get isEmpty => (totals.ga + totals.ta + totals.wa == 0); + + void calculate() { + final Map intervals = timeSpan.emptyIntervals; + final DateTime cutOff = timeSpan.cutOffDate; + + final filtered = msgs.where( + (msg) => + (chatId == null || msg.chatId == chatId) && msg.time.isAfter(cutOff), + ); + + //remove msgs with duplicate ids + final Map unique = {}; + for (final msg in filtered) { + if (unique[msg.eventId] == null) { + unique[msg.eventId] = msg; + } + } + + for (final msg in unique.values) { + final String key = timeSpan.getMapKey(msg.time); + if (intervals[key] == null) { + debugger(when: kDebugMode); + } else { + intervals[key]!.totals.increment(msg); + totals.increment(msg); + lastMessage = msg.time; + } + } + timeSeries = intervals.values.toList().reversed.toList(); + } +} diff --git a/lib/pangea/models/chat_topic_model.dart b/lib/pangea/models/chat_topic_model.dart new file mode 100644 index 000000000..c83e3f7c6 --- /dev/null +++ b/lib/pangea/models/chat_topic_model.dart @@ -0,0 +1,124 @@ +import 'lemma.dart'; + +class ChatTopic { + String name; + String description; + String langCode; + String languageLevel; + List discussionPrompts; + List vocab; + + ChatTopic({ + this.name = "", + this.description = "", + required this.langCode, + this.languageLevel = "Pre-A1", + this.discussionPrompts = const [], + this.vocab = const [], + }); + + factory ChatTopic.fromJson(Map json) { + return ChatTopic( + name: json['name'], + description: json['description'], + langCode: json['lang_code'], + languageLevel: json['language_level'], + discussionPrompts: (json['discussion_prompts'] as Iterable) + .map((e) => DiscussionPrompt.fromJson(e)) + .toList(), + vocab: (json['vocab'] as Iterable) + .map((e) => Lemma.fromJson(e)) + .toList(), + ); + } + + Map toJson() { + return { + 'name': name, + 'description': description, + 'lang_code': langCode, + 'language_level': languageLevel, + 'discussion_prompts': discussionPrompts, + 'vocab': vocab, + }; + } + + static ChatTopic get empty => ChatTopic( + name: '', + description: '', + langCode: '', + languageLevel: '', + discussionPrompts: [], + vocab: [], + ); + + /// set equals operator + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChatTopic && + runtimeType == other.runtimeType && + name == other.name && + description == other.description && + langCode == other.langCode && + languageLevel == other.languageLevel && + discussionPrompts == other.discussionPrompts && + vocab == other.vocab; + + /// set hashcode + @override + int get hashCode => + name.hashCode ^ + description.hashCode ^ + langCode.hashCode ^ + languageLevel.hashCode ^ + discussionPrompts.hashCode ^ + vocab.hashCode; + + /// mock data + static ChatTopic get mockTopic => ChatTopic( + name: 'Mock Topic', + description: 'Mock Description', + langCode: 'en', + languageLevel: 'A1', + discussionPrompts: [ + DiscussionPrompt(text: 'Mock Prompt 1'), + DiscussionPrompt(text: 'Mock Prompt 2'), + ], + vocab: [ + Lemma(text: 'Mock Lemma 1', saveVocab: true, form: 'Mock Form 1'), + Lemma(text: 'Mock Lemma 2', saveVocab: true, form: 'Mock Form 2'), + ], + ); +} + +/// just one parameter, text +class DiscussionPrompt { + final String text; + + DiscussionPrompt({required this.text}); + + factory DiscussionPrompt.fromJson(Map json) { + return DiscussionPrompt( + text: json['text'], + ); + } + + Map toJson() { + return { + 'text': text, + }; + } + + /// set equals operator + @override + bool operator ==(Object other) => + identical(this, other) || + other is DiscussionPrompt && + runtimeType == other.runtimeType && + text == other.text; + + /// set hashcode + @override + int get hashCode => text.hashCode; +} diff --git a/lib/pangea/models/choreo_init_response.model.dart b/lib/pangea/models/choreo_init_response.model.dart new file mode 100644 index 000000000..d91233ce8 --- /dev/null +++ b/lib/pangea/models/choreo_init_response.model.dart @@ -0,0 +1,81 @@ +class ChoreoResponseModel { + GrammarData? grammarData; + String? detectedLang; + String? route; + String? feedbackMessage; + int? payloadId; + ChoreoResponseModel( + {this.grammarData, this.detectedLang, this.route, this.feedbackMessage}); + + ChoreoResponseModel.fromJson(Map json) { + grammarData = json['grammar_data'] != null + ? GrammarData.fromJson(json['grammar_data']) + : null; + detectedLang = json['detected_lang']; + route = json['route']; + feedbackMessage = json['feedback_message']; + payloadId = json['payload_id']; + } + + Map toJson() { + final Map data = {}; + if (grammarData != null) { + data['grammar_data'] = grammarData!.toJson(); + } + data['detected_lang'] = detectedLang; + data['route'] = route; + data['feedback_message'] = feedbackMessage; + return data; + } +} + +class GrammarData { + String? text; + List? tokens; + double? slor; + + GrammarData({this.text, this.tokens, this.slor}); + + GrammarData.fromJson(Map json) { + text = json['text']; + if (json['tokens'] != null) { + tokens = []; + json['tokens'].forEach((v) { + tokens!.add(Tokens.fromJson(v)); + }); + } + slor = json['slor']; + } + + Map toJson() { + final Map data = {}; + data['text'] = text; + if (tokens != null) { + data['tokens'] = tokens!.map((v) => v.toJson()).toList(); + } + data['slor'] = slor; + return data; + } +} + +class Tokens { + String? token; + int? category; + String? feedbackMessage; + + Tokens({this.token, this.category, this.feedbackMessage}); + + Tokens.fromJson(Map json) { + token = json['token']; + category = json['category']; + feedbackMessage = json['feedback_message']; + } + + Map toJson() { + final Map data = {}; + data['token'] = token; + data['category'] = category; + data['feedback_message'] = feedbackMessage; + return data; + } +} diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart new file mode 100644 index 000000000..048c42f96 --- /dev/null +++ b/lib/pangea/models/choreo_record.dart @@ -0,0 +1,311 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; + +import '../constants/choreo_constants.dart'; +import '../enum/construct_type_enum.dart'; +import 'constructs_analytics_model.dart'; +import 'it_step.dart'; +import 'lemma.dart'; + +/// this class lives within a [PangeaIGCEvent] +/// it always has a [RepresentationEvent] parent +/// These live as separate event so that anyone can add and edit grammar checks +/// to a representation +/// It represents the real-time changes to a text +/// TODO - start saving senderL2Code in choreoRecord to be able better decide the useType + +class ChoreoRecord { + /// ordered versions of the representation, with first being original and last + /// being the final sent text + /// there is not a 1-to-1 map from steps to matches + List choreoSteps; + + // String current; + + List openMatches; + + ChoreoRecord({ + required this.choreoSteps, + required this.openMatches, + // required this.current, + }); + + factory ChoreoRecord.fromJson(Map json) { + final stepsRaw = json[_stepsKey]; + return ChoreoRecord( + choreoSteps: (jsonDecode(stepsRaw ?? "[]") as Iterable) + .map((e) { + return ChoreoRecordStep.fromJson(e); + }) + .toList() + .cast(), + openMatches: (jsonDecode(json[_openMatchesKey] ?? "[]") as Iterable) + .map((e) { + return PangeaMatch.fromJson(e); + }) + .toList() + .cast(), + // current: json[_currentKey], + ); + } + + static const _stepsKey = "stps"; + static const _openMatchesKey = "mtchs"; + // static const _currentKey = "crnt"; + + Map toJson() { + final data = {}; + data[_stepsKey] = jsonEncode(choreoSteps.map((e) => e.toJson()).toList()); + data[_openMatchesKey] = + jsonEncode(openMatches.map((e) => e.toJson()).toList()); + // data[_currentKey] = current; + return data; + } + + addRecord(String text, {PangeaMatch? match, ITStep? step}) { + if (match != null && step != null) { + throw Exception("match and step should not both be defined"); + } + choreoSteps.add(ChoreoRecordStep( + text: text, acceptedOrIgnoredMatch: match, itStep: step)); + } + + bool get hasAcceptedMatches => choreoSteps.any( + (element) => + element.acceptedOrIgnoredMatch?.status == + PangeaMatchStatus.accepted, + ); + + bool get hasIgnoredMatches => choreoSteps.any( + (element) => + element.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.ignored, + ); + + // bool get includedIT => choreoSteps.any((step) { + // return step.acceptedOrIgnoredMatch?.status == + // PangeaMatchStatus.accepted && + // (step.acceptedOrIgnoredMatch?.isITStart ?? false); + // }); + + bool get includedIT => choreoSteps.any((step) { + return step.acceptedOrIgnoredMatch?.status == + PangeaMatchStatus.accepted && + (step.acceptedOrIgnoredMatch?.isOutOfTargetMatch ?? false); + }); + + bool get includedIGC => choreoSteps.any((step) { + return step.acceptedOrIgnoredMatch?.status == + PangeaMatchStatus.accepted && + (step.acceptedOrIgnoredMatch?.isGrammarMatch ?? false); + }); + + static ChoreoRecord get newRecord => ChoreoRecord( + choreoSteps: [], + openMatches: [], + ); + + /// [tokens] is the final list of tokens that were sent + /// if no ga or ta, + /// make wa use for each and return + /// else + /// for each saveable vocab in the final message + /// if vocab is contained in an accepted replacement, make ga use + /// if vocab is contained in ta choice, + /// if selected as choice, corIt + /// if written as customInput, corIt? (account for score in this) + /// for each it step + /// for each continuance + /// if not within the final message, save ignIT/incIT + List toVocabUse( + List tokens, String chatId, String msgId) { + final List uses = []; + final DateTime now = DateTime.now(); + List lemmasToVocabUses( + List lemmas, ConstructUseType type) { + final List uses = []; + for (final lemma in lemmas) { + if (lemma.saveVocab) { + uses.add(OneConstructUse( + useType: type, + chatId: chatId, + timeStamp: now, + lemma: lemma.text, + form: lemma.form, + msgId: msgId, + constructType: ConstructType.vocab, + )); + } + } + return uses; + } + + List getVocabUseForToken(PangeaToken token) { + for (final step in choreoSteps) { + /// if 1) accepted match 2) token is in the replacement and 3) replacement + /// is in the overall step text, then token was a ga + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && + (step.acceptedOrIgnoredMatch!.match.choices?.any((r) => + r.value.contains(token.text.content) && + step.text.contains(r.value)) ?? + false)) { + return lemmasToVocabUses(token.lemmas, ConstructUseType.ga); + } + if (step.itStep != null) { + final bool pickedThroughIT = step.itStep!.chosenContinuance?.text + .contains(token.text.content) ?? + false; + if (pickedThroughIT) { + return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt); + //PTODO - check if added via custom input in IT flow + } + } + } + return lemmasToVocabUses(token.lemmas, ConstructUseType.wa); + } + + /// for each token, record whether selected in ga, ta, or wa + for (final token in tokens) { + uses.addAll(getVocabUseForToken(token)); + } + + for (final itStep in itSteps) { + for (final continuance in itStep.continuances) { + // this seems to always be false for continuances right now + + if (finalMessage.contains(continuance.text)) { + continue; + } + if (continuance.wasClicked) { + //PTODO - account for end of flow score + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + uses.addAll( + lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt), + ); + } + } else { + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + uses.addAll( + lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt), + ); + } + } + } + } + + return uses; + } + + List toGrammarConstructUse(String msgId, String chatId) { + final List uses = []; + final DateTime now = DateTime.now(); + for (final step in choreoSteps) { + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { + final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? + step.acceptedOrIgnoredMatch!.match.shortMessage ?? + step.acceptedOrIgnoredMatch!.match.type.typeName.name; + uses.add(OneConstructUse( + useType: ConstructUseType.ga, + chatId: chatId, + timeStamp: now, + lemma: name, + form: name, + msgId: msgId, + constructType: ConstructType.grammar, + )); + } + } + return uses; + } + + List get itSteps => + choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList(); + + String get finalMessage => + choreoSteps.isNotEmpty ? choreoSteps.last.text : ""; +} + +/// new step are saved +/// 1) before every system-provided text is accepted, if final text is different +/// from last step +/// 2) on the acceptance of system-provided text +/// 3) on message send, if final text is different from last step +/// +/// user edit +/// "hey ther" +/// user accepts "there" correction +/// "hey there" +/// step made for user edits and step made for system suggestion +/// user goes through IT, chooses "hola" +/// "hola" +/// step saved +/// adds "amigo" +/// step saved +class ChoreoRecordStep { + /// text after changes have been made + String text; + + /// all matches throughout edit process, + /// including those open, accepted and ignored + /// last step in list may contain open + PangeaMatch? acceptedOrIgnoredMatch; + + ITStep? itStep; + + ChoreoRecordStep( + {required this.text, this.acceptedOrIgnoredMatch, this.itStep}) { + if (itStep != null && acceptedOrIgnoredMatch != null) { + throw Exception( + "itStep and acceptedOrIgnoredMatch should not both be defined"); + } + } + + factory ChoreoRecordStep.fromJson(Map json) { + return ChoreoRecordStep( + text: json[_textKey], + acceptedOrIgnoredMatch: json[_acceptedOrIgnoredMatchKey] != null + ? PangeaMatch.fromJson(json[_acceptedOrIgnoredMatchKey]) + : null, + itStep: json[_stepKey] != null ? ITStep.fromJson(json[_stepKey]) : null, + ); + } + + static const _textKey = "txt"; + static const _acceptedOrIgnoredMatchKey = "mtch"; + static const _stepKey = "stp"; + + Map toJson() { + final data = {}; + data[_textKey] = text; + data[_acceptedOrIgnoredMatchKey] = acceptedOrIgnoredMatch?.toJson(); + data[_stepKey] = itStep?.toJson(); + return data; + } +} + +// Example flow +// hello my name is Jordan! Im typing a message + +// igc called, step saved with matchIndex = null + +// click Im and fixed + +// hello my name is Jordan! I'm typing a message + +// igc called, step saved with matchIndex equal to i'm index + +// hello my name is Jordan! I'm typing a message + +// igc called, step saved with matchIndex = null + +// class ITStepRecord { +// List allContinuances; + +// /// continuances that were clicked but not selected +// List clicked; + +// ITStepRecord({required this.continuances}); +// } + +// class ContinuanceRecord {} diff --git a/lib/pangea/models/class_analytics_model.dart b/lib/pangea/models/class_analytics_model.dart new file mode 100644 index 000000000..8c55be5aa --- /dev/null +++ b/lib/pangea/models/class_analytics_model.dart @@ -0,0 +1,103 @@ +import 'package:intl/intl.dart'; + +class ClassAnalyticsModel { + ClassAnalyticsModel(); + late final Null classId; + late final List userIds; + late final List analytics; + get tableView {} + ClassAnalyticsModel.fromJson(Map json) { + classId = null; + userIds = List.castFrom(json['user_ids']); + analytics = + List.from(json['analytics']).map((e) => Analytics.fromJson(e)).toList(); + } + + Map toJson() { + final _data = {}; + _data['class_id'] = classId; + _data['user_ids'] = userIds; + _data['analytics'] = analytics.map((e) => e.toJson()).toList(); + return _data; + } +} + +class Analytics { + Analytics({ + required this.title, + required this.section, + }); + late final String title; + late final List
section; + + Analytics.fromJson(Map json) { + title = json['title']; + section = + List.from(json['section']).map((e) => Section.fromJson(e)).toList(); + } + + Map toJson() { + final _data = {}; + _data['title'] = title; + _data['section'] = section.map((e) => e.toJson()).toList(); + return _data; + } +} + +class Section { + Section({ + required this.title, + required this.classTotal, + required this.data, + }); + late final String title; + late final String classTotal; + late final List data; + + Section.fromJson(Map json) { + title = json['title']; + classTotal = json['class_total']; + data = List.from(json['data']).map((e) => Data.fromJson(e)).toList(); + } + + Map toJson() { + final _data = {}; + _data['title'] = title; + _data['class_total'] = classTotal; + _data['data'] = data.map((e) => e.toJson()).toList(); + return _data; + } +} + +class Data { + Data(); + set value(String val) => _value = val; + String get value { + if (_value == null) { + return _value.toString(); + } + if (value_type == 'date') { + return DateFormat('yyyy/M/dd hh:mm a') + .format(DateTime.parse(_value).toLocal()) + .toString(); + } + return _value; + } + + late final String userId; + late final String _value; + late final String value_type; + Data.fromJson(Map json) { + userId = json['user_id']; + _value = json['value']; + value_type = json['value_type']; + } + + Map toJson() { + final _data = {}; + _data['user_id'] = userId; + _data['value'] = _value; + _data['value_type'] = value_type; + return _data; + } +} diff --git a/lib/pangea/models/class_email_invite_model.dart b/lib/pangea/models/class_email_invite_model.dart new file mode 100644 index 000000000..31d6b2472 --- /dev/null +++ b/lib/pangea/models/class_email_invite_model.dart @@ -0,0 +1,47 @@ +class PClassEmailInviteModel { + List? data; + String? pangeaClassRoomId; + String? teacherName; + + PClassEmailInviteModel({this.data, this.pangeaClassRoomId, this.teacherName}); + + PClassEmailInviteModel.fromJson(Map json) { + if (json['data'] != null) { + data = []; + json['data'].forEach((v) { + data!.add(ClassEmailInviteData.fromJson(v)); + }); + } + pangeaClassRoomId = json['pangea_class_room_id']; + teacherName = json['teacher_name']; + } + + Map toJson() { + final Map data = {}; + if (this.data != null) { + data['data'] = this.data!.map((v) => v.toJson()).toList(); + } + data['pangea_class_room_id'] = pangeaClassRoomId; + data['teacher_name'] = teacherName; + return data; + } +} + +class ClassEmailInviteData { + String? name; + String? email; + + ClassEmailInviteData({this.name, this.email}); + + ClassEmailInviteData.fromJson(Map json) { + name = json['name']; + email = json['email']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['email'] = email; + return data; + } +} diff --git a/lib/pangea/models/class_model.dart b/lib/pangea/models/class_model.dart new file mode 100644 index 000000000..37018a2d3 --- /dev/null +++ b/lib/pangea/models/class_model.dart @@ -0,0 +1,332 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/class_default_values.dart'; +import '../constants/language_keys.dart'; +import '../constants/pangea_event_types.dart'; +import 'language_model.dart'; + +class ClassSettingsModel { + String? city; + String? country; + String? schoolName; + int? languageLevel; + String dominantLanguage; + String targetLanguage; + + ClassSettingsModel({ + this.dominantLanguage = ClassDefaultValues.defaultDominantLanguage, + this.targetLanguage = ClassDefaultValues.defaultTargetLanguage, + this.languageLevel, + this.city, + this.country, + this.schoolName, + }); + + static ClassSettingsModel get newClass => ClassSettingsModel( + city: null, + country: null, + dominantLanguage: ClassDefaultValues.defaultDominantLanguage, + languageLevel: null, + schoolName: null, + targetLanguage: ClassDefaultValues.defaultTargetLanguage, + ); + + factory ClassSettingsModel.fromJson(Map json) { + return ClassSettingsModel( + city: json['city'], + country: json['country'], + dominantLanguage: LanguageModel.codeFromNameOrCode( + json['dominant_language'] ?? LanguageKeys.unknownLanguage), + targetLanguage: LanguageModel.codeFromNameOrCode( + json['target_language'] ?? LanguageKeys.unknownLanguage), + languageLevel: json['language_level'], + schoolName: json['school_name'], + ); + } + + Map toJson() { + final data = {}; + try { + data['city'] = city; + data['country'] = country; + //check for and do "english" => en and "spanish" => es + data['dominant_language'] = dominantLanguage; + //check for and do "english" => en and "spanish" => es + data['target_language'] = targetLanguage; + data['language_level'] = languageLevel; + data['school_name'] = schoolName; + return data; + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + return data; + } + } + + //TODO: define enum with all possible values + updateEditableClassField(String key, dynamic value) { + switch (key) { + case ModelKey.clientClassCity: + city = value; + break; + case ModelKey.clientClassCountry: + country = value; + break; + case ModelKey.clientClassDominantLanguage: + dominantLanguage = value; + break; + case ModelKey.clientClassTargetLanguage: + targetLanguage = value; + break; + case ModelKey.clientLanguageLevel: + languageLevel = value; + break; + case ModelKey.clientSchool: + schoolName = value; + break; + default: + throw Exception('Invalid key for setting permissions - $key'); + } + } + + StateEvent get toStateEvent => StateEvent( + content: toJson(), + type: PangeaEventTypes.classSettings, + ); +} + +class PangeaRoomRules { + // int? pangeaClassID; // this is id our database + bool isPublic; + bool isOpenEnrollment; + bool oneToOneChatClass; + bool isCreateRooms; + bool isShareVideo; + bool isSharePhoto; + bool isShareFiles; + bool isShareLocation; + bool isCreateStories; + bool isVoiceNotes; + bool isInviteOnlyStudents; + // 0 = forbidden, 1 = allow individual to choose, 2 = require + int interactiveTranslator; + int interactiveGrammar; + int immersionMode; + int definitions; + int translations; + + PangeaRoomRules({ + this.isPublic = false, + this.isOpenEnrollment = false, + this.oneToOneChatClass = true, + this.isCreateRooms = true, + this.isShareVideo = true, + this.isSharePhoto = true, + this.isShareFiles = true, + this.isShareLocation = false, + this.isCreateStories = false, + this.isVoiceNotes = true, + this.isInviteOnlyStudents = true, + this.interactiveTranslator = ClassDefaultValues.languageToolPermissions, + this.interactiveGrammar = ClassDefaultValues.languageToolPermissions, + this.immersionMode = ClassDefaultValues.languageToolPermissions, + this.definitions = ClassDefaultValues.languageToolPermissions, + this.translations = ClassDefaultValues.languageToolPermissions, + }); + + updatePermission(String key, bool value) { + switch (key) { + case 'isPublic': + isPublic = value; + break; + case 'isOpenEnrollment': + isOpenEnrollment = value; + break; + case 'oneToOneChatClass': + oneToOneChatClass = value; + break; + case 'isCreateRooms': + isCreateRooms = value; + break; + case 'isShareVideo': + isShareVideo = value; + break; + case 'isSharePhoto': + isSharePhoto = value; + break; + case 'isShareFiles': + isShareFiles = value; + break; + case 'isShareLocation': + isShareLocation = value; + break; + case 'isCreateStories': + isCreateStories = value; + break; + case 'isVoiceNotes': + isVoiceNotes = value; + break; + case 'isInviteOnlyStudents': + isInviteOnlyStudents = value; + break; + default: + throw Exception('Invalid key for setting permissions - $key'); + } + } + + setLanguageToolSetting(ToolSetting setting, int value) { + switch (setting) { + case ToolSetting.interactiveTranslator: + interactiveTranslator = value; + break; + case ToolSetting.interactiveGrammar: + interactiveGrammar = value; + break; + case ToolSetting.immersionMode: + immersionMode = value; + break; + case ToolSetting.definitions: + definitions = value; + break; + case ToolSetting.translations: + translations = value; + break; + default: + throw Exception('Invalid key for setting permissions - $setting'); + } + } + + StateEvent get toStateEvent => StateEvent( + content: toJson(), + type: PangeaEventTypes.rules, + ); + + factory PangeaRoomRules.fromJson(Map json) => + PangeaRoomRules( + // pangeaClassID: json['pangea_class']; + isPublic: json['is_public'], + isOpenEnrollment: json['is_open_enrollment'], + oneToOneChatClass: json['one_to_one_chat_class'], + isCreateRooms: json['is_create_rooms'], + isShareVideo: json['is_share_video'], + isSharePhoto: json['is_share_photo'], + isShareFiles: json['is_share_files'], + isShareLocation: json['is_share_location'], + isCreateStories: json['is_create_stories'], + isVoiceNotes: json['is_voice_notes'], + isInviteOnlyStudents: json['is_invite_only_students'] ?? true, + interactiveTranslator: json['interactive_translator'] ?? + ClassDefaultValues.languageToolPermissions, + interactiveGrammar: json['interactive_grammar'] ?? + ClassDefaultValues.languageToolPermissions, + immersionMode: json['immersion_mode'] ?? + ClassDefaultValues.languageToolPermissions, + definitions: + json['definitions'] ?? ClassDefaultValues.languageToolPermissions, + translations: + json['translations'] ?? ClassDefaultValues.languageToolPermissions, + ); + + Map toJson() { + final data = {}; + // data['pangea_class'] = pangeaClassID; + data['is_public'] = isPublic; + data['is_open_enrollment'] = isOpenEnrollment; + data['one_to_one_chat_class'] = oneToOneChatClass; + data['is_create_rooms'] = isCreateRooms; + data['is_share_video'] = isShareVideo; + data['is_share_photo'] = isSharePhoto; + data['is_share_files'] = isShareFiles; + data['is_share_location'] = isShareLocation; + data['is_create_stories'] = isCreateStories; + data['is_voice_notes'] = isVoiceNotes; + data['is_invite_only_students'] = isInviteOnlyStudents; + data['interactive_translator'] = interactiveTranslator; + data['interactive_grammar'] = interactiveGrammar; + data['immersion_mode'] = immersionMode; + data['definitions'] = definitions; + data['translations'] = translations; + return data; + } + + int getToolSettings(ToolSetting setting) { + switch (setting) { + case ToolSetting.interactiveTranslator: + return interactiveTranslator; + case ToolSetting.interactiveGrammar: + return interactiveGrammar; + case ToolSetting.immersionMode: + return immersionMode; + case ToolSetting.definitions: + return definitions; + case ToolSetting.translations: + return translations; + default: + throw Exception('Invalid key for setting permissions - $setting'); + } + } + + String languageToolPermissionsText( + BuildContext context, + ToolSetting setting, + ) { + switch (getToolSettings(setting)) { + case 0: + return L10n.of(context)!.interactiveTranslatorNotAllowed; + case 1: + return L10n.of(context)!.interactiveTranslatorAllowed; + case 2: + return L10n.of(context)!.interactiveTranslatorRequired; + default: + return L10n.of(context)!.notYetSet; + } + } +} + +enum ToolSetting { + interactiveTranslator, + interactiveGrammar, + immersionMode, + definitions, + translations, +} + +extension SettingCopy on ToolSetting { + String toolName(BuildContext context) { + switch (this) { + case ToolSetting.interactiveTranslator: + return L10n.of(context)!.interactiveTranslatorSliderHeader; + case ToolSetting.interactiveGrammar: + return L10n.of(context)!.interactiveGrammarSliderHeader; + case ToolSetting.immersionMode: + return L10n.of(context)!.toggleImmersionMode; + case ToolSetting.definitions: + return L10n.of(context)!.definitionsToolName; + case ToolSetting.translations: + return L10n.of(context)!.messageTranslationsToolName; + } + } + + //use l10n to get tool name + String toolDescription(BuildContext context) { + switch (this) { + case ToolSetting.interactiveTranslator: + return L10n.of(context)!.itToggleDescription; + case ToolSetting.interactiveGrammar: + return L10n.of(context)!.igcToggleDescription; + case ToolSetting.immersionMode: + return L10n.of(context)!.toggleImmersionModeDesc; + case ToolSetting.definitions: + return L10n.of(context)!.definitionsToolDescription; + case ToolSetting.translations: + return L10n.of(context)!.translationsToolDescrption; + } + } +} diff --git a/lib/pangea/models/construct_analytics_event.dart b/lib/pangea/models/construct_analytics_event.dart new file mode 100644 index 000000000..5b24cccfe --- /dev/null +++ b/lib/pangea/models/construct_analytics_event.dart @@ -0,0 +1,33 @@ +import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; + +class ConstructEvent { + late Event _event; + ConstructUses? _contentCache; + + ConstructEvent({required Event event}) { + if (event.type != PangeaEventTypes.vocab) { + throw Exception( + "${event.type} should not be used to make a StudentAnalyticsEvent", + ); + } + _event = event; + } + + Event get event => _event; + + ConstructUses get content { + _contentCache ??= ConstructUses.fromJson(event.content); + if (_contentCache!.lemma.isEmpty) { + _contentCache!.lemma = event.stateKey!; + } + return _contentCache!; + } + + void addAll(List uses) { + content.uses.addAll(uses); + event.content = content.toJson(); + } +} diff --git a/lib/pangea/models/constructs_analytics_model.dart b/lib/pangea/models/constructs_analytics_model.dart new file mode 100644 index 000000000..510326361 --- /dev/null +++ b/lib/pangea/models/constructs_analytics_model.dart @@ -0,0 +1,179 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../enum/construct_type_enum.dart'; + +class ConstructUses { + String lemma; + ConstructType type; + + List uses; + + //PTODO - how to incorporate semantic similarity score into this? + + //PTODO - add variables for saving requests for + // 1) definitions + // 2) translations + // 3) examples??? (gpt suggested) + + ConstructUses({ + required this.lemma, + required this.type, + this.uses = const [], + }); + + factory ConstructUses.fromJson(Map json) { + // try { + debugger( + when: kDebugMode && + (json['uses'] == null || json[ModelKey.lemma] == null)); + return ConstructUses( + lemma: json[ModelKey.lemma], + uses: (json['uses'] as Iterable) + .map( + (use) => use != null ? OneConstructUse.fromJson(use) : null) + .where((element) => element != null) + .cast() + .toList(), + type: ConstructTypeUtil.fromString(json['type']), + ); + // } catch (err) { + // debugger(when: kDebugMode); + // rethrow; + // } + } + + toJson() { + return { + ModelKey.lemma: lemma, + 'uses': uses.map((use) => use.toJson()).toList(), + 'type': type.string, + }; + } + + void addUsesByUseType(List uses) { + for (final use in uses) { + if (use.lemma != lemma) { + throw Exception('lemma mismatch'); + } + uses.add(use); + } + } +} + +enum ConstructUseType { + /// encountered match and accepted it + ga, + + /// used without assistance + wa, + + /// selected correctly in IT flow + corIt, + + /// encountered as it distractor and selected it + incIt, + + /// encountered as IT distractor and correctly ignored it + ignIt, + + /// encountered in igc match and ignored match + ignIGC, + + /// encountered in igc match and ignored match + corIGC, +} + +extension on ConstructUseType { + String get string { + switch (this) { + case ConstructUseType.ga: + return 'ga'; + case ConstructUseType.wa: + return 'wa'; + case ConstructUseType.corIt: + return 'corIt'; + case ConstructUseType.incIt: + return 'incIt'; + case ConstructUseType.ignIt: + return 'ignIt'; + case ConstructUseType.ignIGC: + return 'ignIGC'; + case ConstructUseType.corIGC: + return 'corIGC'; + } + } + + IconData get icon { + switch (this) { + case ConstructUseType.ga: + return Icons.check; + case ConstructUseType.wa: + return Icons.thumb_up_sharp; + case ConstructUseType.corIt: + return Icons.check; + case ConstructUseType.incIt: + return Icons.close; + case ConstructUseType.ignIt: + return Icons.close; + case ConstructUseType.ignIGC: + return Icons.close; + case ConstructUseType.corIGC: + return Icons.check; + } + } +} + +class OneConstructUse { + String? lemma; + ConstructType? constructType; + String? form; + ConstructUseType useType; + String chatId; + String? msgId; + DateTime timeStamp; + + OneConstructUse({ + required this.useType, + required this.chatId, + required this.timeStamp, + required this.lemma, + required this.form, + required this.msgId, + required this.constructType, + }); + + factory OneConstructUse.fromJson(Map json) { + return OneConstructUse( + useType: ConstructUseType.values + .firstWhere((e) => e.string == json['useType']), + chatId: json['chatId'], + timeStamp: DateTime.parse(json['timeStamp']), + lemma: json['lemma'], + form: json['form'], + msgId: json['msgId'], + constructType: json['constructType'] != null + ? ConstructTypeUtil.fromString(json['constructType']) + : null, + ); + } + + Map toJson([bool condensed = true]) { + final Map data = { + 'useType': useType.string, + 'chatId': chatId, + 'timeStamp': timeStamp.toIso8601String(), + 'form': form, + 'msgId': msgId, + }; + if (!condensed && lemma != null) data['lemma'] = lemma!; + if (!condensed && constructType != null) { + data['constructType'] = constructType!.string; + } + + return data; + } +} diff --git a/lib/pangea/models/custom_input_translation_model.dart b/lib/pangea/models/custom_input_translation_model.dart new file mode 100644 index 000000000..6a45911c9 --- /dev/null +++ b/lib/pangea/models/custom_input_translation_model.dart @@ -0,0 +1,41 @@ +import 'package:fluffychat/pangea/constants/model_keys.dart'; + +class CustomInputRequestModel { + String text; + String customInput; + String sourceLangCode; + String targetLangCode; + String userId; + String roomId; + String? classId; + + CustomInputRequestModel({ + required this.text, + required this.customInput, + required this.sourceLangCode, + required this.targetLangCode, + required this.userId, + required this.roomId, + required this.classId, + }); + + factory CustomInputRequestModel.fromJson(json) => CustomInputRequestModel( + text: json['text'], + customInput: json['custom_input'], + sourceLangCode: json[ModelKey.srcLang], + targetLangCode: json[ModelKey.tgtLang], + userId: json['user_id'], + roomId: json['room_id'], + classId: json['class_id'], + ); + + toJson() => { + 'text': text, + 'custom_input': customInput, + ModelKey.srcLang: sourceLangCode, + ModelKey.tgtLang: targetLangCode, + 'user_id': userId, + 'room_id': roomId, + 'class_id': classId + }; +} diff --git a/lib/pangea/models/exchange_model.dart b/lib/pangea/models/exchange_model.dart new file mode 100644 index 000000000..41c54d763 --- /dev/null +++ b/lib/pangea/models/exchange_model.dart @@ -0,0 +1,40 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; + +import 'class_model.dart'; + +class ExchangeModel { + PangeaRoomRules permissions; + + ExchangeModel({ + required this.permissions, + }); + + factory ExchangeModel.fromJson(Map json) { + return ExchangeModel( + permissions: PangeaRoomRules.fromJson(json[ModelKey.permissions]), + ); + } + + Map toJson() { + final data = {}; + try { + data[ModelKey.permissions] = permissions.toJson(); + return data; + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s); + return data; + } + } + + updateEditableClassField(String key, dynamic value) { + switch (key) { + default: + throw Exception('Invalid key for setting permissions - $key'); + } + } +} diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart new file mode 100644 index 000000000..e27d9a215 --- /dev/null +++ b/lib/pangea/models/headwords.dart @@ -0,0 +1,180 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/models/constructs_analytics_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../enum/vocab_proficiency_enum.dart'; + +class VocabHeadwords { + List lists; + + VocabHeadwords({ + required this.lists, + }); + + /// in json parameter, keys are the names of the VocabList + /// values are the words in the VocabList + factory VocabHeadwords.fromJson(Map json) { + final List lists = []; + for (final entry in json.entries) { + lists.add(VocabList( + name: entry.key, + lemmas: (entry.value as Iterable).cast().toList(), + )); + } + return VocabHeadwords(lists: lists); + } + + static Future getHeadwords(String langCode) async { + final String data = + await rootBundle.loadString('${langCode}_headwords.json'); + final decoded = jsonDecode(data); + final VocabHeadwords headwords = VocabHeadwords.fromJson(decoded); + return headwords; + } +} + +class VocabList { + String name; + + /// key is lemma + Map words = {}; + + VocabList({ + required this.name, + required List lemmas, + }) { + for (final lemma in lemmas) { + words[lemma] = VocabTotals.newTotals; + } + } + + void addVocabUse(String lemma, List use) { + words[lemma.toUpperCase()]?.addVocabUseBasedOnUseType(use); + } + + ListTotals calculuateTotals() { + final ListTotals listTotals = ListTotals.empty; + for (final word in words.entries) { + debugger(when: kDebugMode && word.key == "baloncesto".toLowerCase()); + listTotals.addByType(word.value.proficiencyLevel); + } + return listTotals; + } +} + +class ListTotals { + int low; + int medium; + int high; + int unknown; + + ListTotals({ + required this.low, + required this.medium, + required this.high, + required this.unknown, + }); + + static get empty => ListTotals(low: 0, medium: 0, high: 0, unknown: 0); + + void addByType(VocabProficiencyEnum prof) { + switch (prof) { + case VocabProficiencyEnum.low: + low++; + break; + case VocabProficiencyEnum.medium: + medium++; + break; + case VocabProficiencyEnum.high: + high++; + break; + case VocabProficiencyEnum.unk: + unknown++; + break; + } + } +} + +class VocabTotals { + num ga; + + num wa; + + num corIt; + + num incIt; + + num ignIt; + + VocabTotals({ + required this.ga, + required this.wa, + required this.corIt, + required this.incIt, + required this.ignIt, + }); + + num get calculateEstimatedVocabProficiency { + const num gaWeight = -1; + const num waWeight = 1; + const num corItWeight = 0.5; + const num incItWeight = -0.5; + const num ignItWeight = 0.1; + + final num gaScore = ga * gaWeight; + final num waScore = wa * waWeight; + final num corItScore = corIt * corItWeight; + final num incItScore = incIt * incItWeight; + final num ignItScore = ignIt * ignItWeight; + + final num totalScore = + gaScore + waScore + corItScore + incItScore + ignItScore; + + return totalScore; + } + + VocabProficiencyEnum get proficiencyLevel => + VocabProficiencyUtil.proficiency(calculateEstimatedVocabProficiency); + + static VocabTotals get newTotals { + return VocabTotals( + ga: 0, + wa: 0, + corIt: 0, + incIt: 0, + ignIt: 0, + ); + } + + void addVocabUseBasedOnUseType(List uses) { + for (final use in uses) { + switch (use.useType) { + case ConstructUseType.ga: + ga++; + break; + case ConstructUseType.wa: + wa++; + break; + case ConstructUseType.corIt: + corIt++; + break; + case ConstructUseType.incIt: + incIt++; + break; + case ConstructUseType.ignIt: + ignIt++; + break; + //TODO - these shouldn't be counted as such + case ConstructUseType.ignIGC: + ignIt++; + break; + case ConstructUseType.corIGC: + corIt++; + break; + } + } + } +} diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart new file mode 100644 index 000000000..a65cbefc7 --- /dev/null +++ b/lib/pangea/models/igc_text_data_model.dart @@ -0,0 +1,331 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/models/span_card_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +// import 'package:language_tool/language_tool.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../constants/model_keys.dart'; +import '../utils/overlay.dart'; +import '../widgets/igc/span_card.dart'; +import '../widgets/igc/word_data_card.dart'; +import 'language_detection_model.dart'; + +class IGCTextData { + List detections; + String originalInput; + String? fullTextCorrection; + List tokens; + List matches; + String userL1; + String userL2; + bool enableIT; + bool enableIGC; + bool loading = false; + + IGCTextData({ + required this.detections, + required this.originalInput, + required this.fullTextCorrection, + required this.tokens, + required this.matches, + required this.userL1, + required this.userL2, + required this.enableIT, + required this.enableIGC, + }); + + factory IGCTextData.fromJson(Map json) { + return IGCTextData( + tokens: (json[_tokensKey] as Iterable) + .map( + (e) => PangeaToken.fromJson(e as Map), + ) + .toList() + .cast(), + matches: json[_matchesKey] != null + ? (json[_matchesKey] as Iterable) + .map( + (e) { + return PangeaMatch.fromJson(e as Map); + }, + ) + .toList() + .cast() + : [], + detections: (json[_detectionsKey] as Iterable) + .map( + (e) => LanguageDetection.fromJson(e as Map), + ) + .toList() + .cast(), + originalInput: json["original_input"], + fullTextCorrection: json["full_text_correction"], + userL1: json[ModelKey.userL1], + userL2: json[ModelKey.userL2], + enableIT: json["enable_it"], + enableIGC: json["enable_igc"], + ); + } + + static const String _tokensKey = "tokens"; + static const String _matchesKey = "matches"; + static const String _detectionsKey = "detections"; + + Map toJson() => { + _detectionsKey: detections.map((e) => e.toJson()).toList(), + "original_input": originalInput, + "full_text_correction": fullTextCorrection, + _tokensKey: tokens.map((e) => e.toJson()).toList(), + _matchesKey: matches.map((e) => e.toJson()).toList(), + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, + "enable_it": enableIT, + "enable_igc": enableIGC, + }; + + // reconstruct fullText based on accepted match + //update offsets in existing matches to reflect the change + //if existing matches overlap with the accepted one, remove them?? + void acceptReplacement( + int matchIndex, + int choiceIndex, + ) async { + //should be already added to choreoRecord + //TODO - that should be done in the same function to avoid error potential + final PangeaMatch pangeaMatch = matches[matchIndex]; + + if (pangeaMatch.match.choices == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "pangeaMatch.match.choices is null in acceptReplacement", + ); + return; + } + + final String replacement = pangeaMatch.match.choices![choiceIndex].value; + + originalInput = originalInput.replaceRange( + pangeaMatch.match.offset, + pangeaMatch.match.offset + pangeaMatch.match.length, + replacement, + ); + + //update offsets in existing matches to reflect the change + //Question - remove matches that overlap with the accepted one? + // see case of "quiero ver un fix" + matches.removeAt(matchIndex); + + for (final match in matches) { + final matchOffset = match.match.offset; + final matchLength = match.match.length; + match.match.fullText = originalInput; + if (match.match.offset > pangeaMatch.match.offset) { + match.match.offset += replacement.length - pangeaMatch.match.length; + } + } + //quiero ver un fix + //match offset zero and length of full text or 16 + //fix is repplaced by arreglo and now the length needs to be 20 + //if the accepted span is within another span, then the length of that span needs + //needs to be increased by the difference between the new and old length + //if the two spans are overlapping, what happens? + //------ + // ----- -> --- + //if there is any overlap, maybe igc needs to run again? + } + + void removeMatchByOffset(int offset) { + final int index = getTopMatchIndexForOffset(offset); + if (index != -1) { + matches.removeAt(index); + } + } + + int tokenIndexByOffset( + cursorOffset, + ) => + tokens.indexWhere( + (token) => + token.text.offset <= cursorOffset && + cursorOffset <= token.text.offset + token.text.length, + ); + + List getMatchIndicesForToken(PangeaToken token) => + matchIndicesByOffset(token.text.offset); + + int getTopMatchIndexForOffset(int offset) { + final List matchesForToken = matchIndicesByOffset(offset); + if (matchesForToken.isEmpty) return -1; + for (final matchIndex in matchesForToken) { + final match = matches[matchIndex]; + if (enableIT) { + if (match.isITStart || match.isl1SpanMatch) { + return matchIndex; + } + } + if (enableIGC) { + if (match.isGrammarMatch) { + return matchIndex; + } + } + } + return -1; + } + + PangeaMatch? getTopMatchForToken(PangeaToken token) { + final int topMatchIndex = getTopMatchIndexForOffset(token.text.offset); + if (topMatchIndex == -1) return null; + return matches[topMatchIndex]; + } + + List matchIndicesByOffset(int offset) { + final List matchesForOffset = []; + + for (final (index, match) in matches.indexed) { + if (match.isOffsetInMatchSpan(offset)) { + matchesForOffset.add(index); + } + } + + return matchesForOffset; + } + + int getAfterTokenSpacingByIndex( + int tokenIndex, + ) { + final int endOfToken = + tokens[tokenIndex].text.offset + tokens[tokenIndex].text.length; + + if (tokenIndex + 1 < tokens.length) { + final spaceBetween = tokens[tokenIndex + 1].text.offset - endOfToken; + + if (spaceBetween < 0) { + Sentry.addBreadcrumb( + Breadcrumb.fromJson( + { + "fullText": originalInput, + "tokens": tokens.map((e) => e.toJson()).toString() + }, + ), + ); + ErrorHandler.logError( + m: "wierd token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}", + ); + return 0; + } + return spaceBetween; + } else { + return originalInput.length - endOfToken; + } + } + + static TextStyle underlineStyle(Color color) => TextStyle( + decoration: TextDecoration.underline, + decorationColor: color, + decorationThickness: 5, + ); + + static const _hasDefinitionStyle = TextStyle( + decoration: TextDecoration.underline, + decorationColor: Color.fromARGB(148, 83, 97, 255), + decorationThickness: 4, + ); + static TextStyle hasDefinitionStyle(TextStyle? existingStyle) => + existingStyle?.merge(_hasDefinitionStyle) ?? _hasDefinitionStyle; + + //PTODO - handle multitoken spans + List constructTokenSpan({ + required BuildContext context, + TextStyle? defaultStyle, + required SpanCardModel? spanCardModel, + required bool showTokens, + required bool handleClick, + required String transformTargetId, + required Room room, + }) { + final List items = []; + + if (loading) { + return [ + TextSpan( + text: originalInput, + style: defaultStyle, + ), + ]; + } + + // or could make big strings for non-match text and therefore make less textspans. + // would that be more performant? + tokens.asMap().forEach( + (index, token) { + final PangeaMatch? topTokenMatch = getTopMatchForToken( + tokens[index], + ); + // if (index == 3) { + // debugPrint( + // "constructing span with topTokenMatch: ${topTokenMatch?.match.rule.id}"); + // } + + final Widget cardToShow = spanCardModel != null && topTokenMatch != null + ? SpanCard( + scm: spanCardModel, + ) + : WordDataCard( + fullText: originalInput, + fullTextLang: detections.first.langCode, + word: token.text.content, + wordLang: detections.first.langCode, + hasInfo: token.hasInfo, + room: room, + ); + + final TextStyle tokenStyle = topTokenMatch != null + ? topTokenMatch.textStyle(defaultStyle) + : hasDefinitionStyle(defaultStyle); + + items.add(TextSpan( + text: token.text.content, + style: tokenStyle, + recognizer: handleClick + ? (TapGestureRecognizer() + ..onTapDown = (details) => OverlayUtil.showPositionedCard( + context: context, + cardToShow: cardToShow, + cardSize: topTokenMatch?.isITStart ?? false + ? const Size(350, 220) + : const Size(350, 400), + transformTargetId: transformTargetId, + )) + : null, + )); + + final int charBetween = getAfterTokenSpacingByIndex( + index, + ); + + if (charBetween > 0) { + items.add( + TextSpan( + text: " " * charBetween, + style: topTokenMatch != null && + token.text.offset + token.text.length + charBetween <= + topTokenMatch.match.offset + + topTokenMatch.match.length + ? tokenStyle + : defaultStyle, + ), + ); + } + }, + ); + + return items; + } +} diff --git a/lib/pangea/models/it_response_model.dart b/lib/pangea/models/it_response_model.dart new file mode 100644 index 000000000..13344c8db --- /dev/null +++ b/lib/pangea/models/it_response_model.dart @@ -0,0 +1,177 @@ +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/extensions/my_list_extionsion.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'lemma.dart'; + +class ITResponseModel { + String fullTextTranslation; + List continuances; + List? goldContinuances; + bool isFinal; + String? translationId; + int payloadId; + + ITResponseModel({ + required this.fullTextTranslation, + required this.continuances, + required this.translationId, + required this.goldContinuances, + required this.isFinal, + required this.payloadId, + }); + + factory ITResponseModel.fromJson(Map json) { + //PTODO - is continuances a variable type? can we change that? + if (json['continuances'].runtimeType == String) { + debugPrint("continuances was string - ${json['continuances']}"); + json['continuances'] = []; + json['finished'] = true; + } + + final List interimCont = (json['continuances'] as List) + .mapIndexed((index, e) { + e["index"] = index; + return Continuance.fromJson(e); + }) + .toList() + .take(ChoreoConstants.numberOfITChoices) + .toList() + .shuffleReturn() + .cast() + //can't do this on the backend because step translation can't filter them out + .where((element) => element.inDictionary) + .toList(); + + return ITResponseModel( + fullTextTranslation: json["full_text_translation"] ?? json["translation"], + continuances: interimCont, + translationId: json['translation_id'], + payloadId: json['payload_id'] ?? 0, + isFinal: json['finished'] ?? false, + goldContinuances: json['gold_continuances'] != null + ? (json['gold_continuances'] as Iterable).map((e) { + e["gold"] = true; + return Continuance.fromJson(e); + }).toList() + : null, + ); + } + + Map toJson() { + final Map data = {}; + data['full_text_translation'] = fullTextTranslation; + data['continuances'] = continuances.map((v) => v.toJson()).toList(); + if (translationId != null) { + data['translation_id'] = translationId; + } + data['payload_id'] = payloadId; + data["finished"] = isFinal; + return data; + } +} + +class Continuance { + /// only saving this top set in a condensed json form + double probability; + int level; + String text; + List lemmas; + + /// saving this in a full json form + String description; + int? indexSavedByServer; + bool wasClicked; + bool inDictionary; + bool hasInfo; + bool gold; + + Continuance({ + required this.probability, + required this.level, + required this.text, + required this.description, + required this.indexSavedByServer, + required this.wasClicked, + required this.inDictionary, + required this.hasInfo, + required this.gold, + required this.lemmas, + }); + + factory Continuance.fromJson(Map json) { + final List lemmaInternal = + (json[ModelKey.lemma] != null && json[ModelKey.lemma] is Iterable) + ? (json[ModelKey.lemma] as Iterable) + .map( + (e) => Lemma.fromJson(e as Map), + ) + .toList() + .cast() + : []; + return Continuance( + probability: json['probability'], + level: json['level'], + text: json['text'], + description: json['description'] ?? "", + indexSavedByServer: json["index"], + inDictionary: json['in_dictionary'] ?? true, + wasClicked: json['clkd'] ?? false, + hasInfo: json['has_info'] ?? false, + gold: json['gold'] ?? false, + lemmas: lemmaInternal, + ); + } + + Map toJson([bool condensed = false]) { + final Map data = {}; + data['probability'] = probability; + data['level'] = level; + data['text'] = text; + data['clkd'] = wasClicked; + data[ModelKey.lemma] = lemmas.map((e) => e.toJson()).toList(); + + if (!condensed) { + data['description'] = description; + data['in_dictionary'] = inDictionary; + data['has_info'] = hasInfo; + data["index"] = indexSavedByServer; + data['gold'] = gold; + } + return data; + } + + Color? get color { + if (!wasClicked) return null; + switch (level) { + case ChoreoConstants.levelThresholdForGreen: + return ChoreoConstants.green; + case ChoreoConstants.levelThresholdForYellow: + return ChoreoConstants.yellow; + case ChoreoConstants.levelThresholdForRed: + return ChoreoConstants.red; + default: + return null; + } + } + + String? feedbackText(BuildContext context) { + final L10n? l10n = L10n.of(context); + if (l10n == null) { + return null; + } + switch (level) { + case ChoreoConstants.levelThresholdForGreen: + return l10n.greenFeedback; + case ChoreoConstants.levelThresholdForYellow: + return l10n.yellowFeedback; + case ChoreoConstants.levelThresholdForRed: + return l10n.redFeedback; + default: + return null; + } + } +} diff --git a/lib/pangea/models/it_step.dart b/lib/pangea/models/it_step.dart new file mode 100644 index 000000000..f06bd7401 --- /dev/null +++ b/lib/pangea/models/it_step.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../constants/choreo_constants.dart'; +import 'it_response_model.dart'; + +class ITStep { + List continuances; + int? chosen; + String? customInput; + bool showAlternativeTranslationOption = false; + + ITStep( + this.continuances, { + this.chosen, + this.customInput, + }) { + if (chosen == null && customInput == null) { + throw Exception("ITStep must have either chosen or customInput"); + } + if (chosen != null && customInput != null) { + throw Exception("ITStep must have only chosen or customInput"); + } + } + + Continuance? get chosenContinuance { + if (chosen == null) return null; + return continuances[chosen!]; + } + + String choiceFeedback(BuildContext context) { + if (continuances.length == 1) return ''; + return chosenContinuance?.feedbackText(context) ?? ""; + } + + bool get isCorrect => + chosenContinuance != null && + (chosenContinuance!.level == ChoreoConstants.levelThresholdForGreen || + chosenContinuance!.gold); + + bool get isYellow => + chosenContinuance != null && + chosenContinuance!.level == ChoreoConstants.levelThresholdForYellow; + + bool get isWrong { + return chosenContinuance != null && + chosenContinuance!.level == ChoreoConstants.levelThresholdForRed; + } + + bool get isCustom => chosenContinuance == null; + + Map toJson() { + final Map data = {}; + data['continuances'] = continuances.map((e) => e.toJson(true)).toList(); + data['chosen'] = chosen; + data['custom_input'] = customInput; + return data; + } + + factory ITStep.fromJson(Map json) { + final List continuances = []; + for (final Map continuance in json['continuances']) { + continuances.add(Continuance.fromJson(continuance)); + } + return ITStep( + continuances, + chosen: json['chosen'], + customInput: json['custom_input'], + ); + } +} diff --git a/lib/pangea/models/language_detection_model.dart b/lib/pangea/models/language_detection_model.dart new file mode 100644 index 000000000..6fa3d7299 --- /dev/null +++ b/lib/pangea/models/language_detection_model.dart @@ -0,0 +1,19 @@ +class LanguageDetection { + String langCode; + + LanguageDetection({ + required this.langCode, + }); + + factory LanguageDetection.fromJson(Map json) { + return LanguageDetection( + langCode: json[_langCodeKey], + ); + } + + static const _langCodeKey = "lang_code"; + + Map toJson() => { + _langCodeKey: langCode, + }; +} diff --git a/lib/pangea/models/language_model.dart b/lib/pangea/models/language_model.dart new file mode 100644 index 000000000..a0764bd98 --- /dev/null +++ b/lib/pangea/models/language_model.dart @@ -0,0 +1,739 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../utils/error_handler.dart'; + +class LanguageModel { + final String langCode; + final int languageType; + final String languageFlag; + final String displayName; + final bool l2; + final bool l1; + + LanguageModel({ + required this.langCode, + required this.languageType, + required this.languageFlag, + required this.displayName, + required this.l2, + required this.l1, + }); + + factory LanguageModel.fromJson(json) { + final String code = json['language_code'] ?? + codeFromNameOrCode( + json['language_name'], + json['language_flag'], + ); + + return LanguageModel( + langCode: code, + languageType: json['language_type'] is String && + (json['language_type'] as String).isNotEmpty + ? int.parse(json['language_type']) + : json['language_type'], + languageFlag: json['language_flag'] ?? "", + displayName: _LanguageLocal.getDisplayName( + code != LanguageKeys.unknownLanguage ? code : json['language_name'], + ), + l2: json["l2"] ?? code.contains("es") || code.contains("en"), + l1: json["l1"] ?? !code.contains("es") && !code.contains("en"), + ); + } + + toJson() => { + 'language_code': langCode, + 'language_name': displayName, + 'language_type': languageType, + 'language_flag': languageFlag, + 'l2': l2, + 'l1': l1, + }; + + // Discuss with Jordan - adding langCode field to language objects as separate from displayName + static String codeFromNameOrCode(String codeOrName, [String? url]) { + if (codeOrName.isEmpty) return LanguageKeys.unknownLanguage; + if (codeOrName == LanguageKeys.unknownLanguage) return codeOrName; + + if (_LanguageLocal.isoLangs.containsKey(codeOrName)) return codeOrName; + + final String code = _LanguageLocal.langCodeFromName(codeOrName); + if (code != LanguageKeys.unknownLanguage) return code; + + if (url == null) return LanguageKeys.unknownLanguage; + + final List split = url.split('/'); + return split.last.split('.').first; + } + + //PTODO - add flag for unknown + static LanguageModel get unknown => LanguageModel( + langCode: LanguageKeys.unknownLanguage, + languageType: 1, + languageFlag: "", + displayName: "Unknown", + l2: false, + l1: false); + + static LanguageModel multiLingual([BuildContext? context]) => LanguageModel( + displayName: context != null + ? L10n.of(context)!.multiLingualClass + : "Multilingual Class", + l2: false, + l1: false, + langCode: LanguageKeys.multiLanguage, + languageFlag: 'assets/colors.png', + languageType: 3, + ); + + // Discuss with Jordan + bool get hasContextualDefinitionSupport => languageType == 2; + + String? getDisplayName(BuildContext context) { + switch (langCode) { + case 'ab': + return L10n.of(context)!.abDisplayName; + case 'aa': + return L10n.of(context)!.aaDisplayName; + case 'af': + return L10n.of(context)!.afDisplayName; + case 'ak': + return L10n.of(context)!.akDisplayName; + case 'sq': + return L10n.of(context)!.sqDisplayName; + case 'am': + return L10n.of(context)!.amDisplayName; + case 'ar': + return L10n.of(context)!.arDisplayName; + case 'an': + return L10n.of(context)!.anDisplayName; + case 'hy': + return L10n.of(context)!.hyDisplayName; + case 'as': + return L10n.of(context)!.asDisplayName; + case 'av': + return L10n.of(context)!.avDisplayName; + case 'ae': + return L10n.of(context)!.aeDisplayName; + case 'ay': + return L10n.of(context)!.ayDisplayName; + case 'az': + return L10n.of(context)!.azDisplayName; + case 'bm': + return L10n.of(context)!.bmDisplayName; + case 'ba': + return L10n.of(context)!.baDisplayName; + case 'eu': + return L10n.of(context)!.euDisplayName; + case 'be': + return L10n.of(context)!.beDisplayName; + case 'bn': + return L10n.of(context)!.bnDisplayName; + case 'bh': + return L10n.of(context)!.bhDisplayName; + case 'bi': + return L10n.of(context)!.biDisplayName; + case 'bs': + return L10n.of(context)!.bsDisplayName; + case 'br': + return L10n.of(context)!.brDisplayName; + case 'bg': + return L10n.of(context)!.bgDisplayName; + case 'my': + return L10n.of(context)!.myDisplayName; + case 'ca': + return L10n.of(context)!.caDisplayName; + case 'ch': + return L10n.of(context)!.chDisplayName; + case 'ce': + return L10n.of(context)!.ceDisplayName; + case 'ny': + return L10n.of(context)!.nyDisplayName; + case 'zh': + return L10n.of(context)!.zhDisplayName; + case 'cv': + return L10n.of(context)!.cvDisplayName; + case 'kw': + return L10n.of(context)!.kwDisplayName; + case 'co': + return L10n.of(context)!.coDisplayName; + case 'cr': + return L10n.of(context)!.crDisplayName; + case 'hr': + return L10n.of(context)!.hrDisplayName; + case 'cs': + return L10n.of(context)!.csDisplayName; + case 'da': + return L10n.of(context)!.daDisplayName; + case 'dv': + return L10n.of(context)!.dvDisplayName; + case 'nl': + return L10n.of(context)!.nlDisplayName; + case 'en': + return L10n.of(context)!.enDisplayName; + case 'eo': + return L10n.of(context)!.eoDisplayName; + case 'et': + return L10n.of(context)!.etDisplayName; + case 'ee': + return L10n.of(context)!.eeDisplayName; + case 'fo': + return L10n.of(context)!.foDisplayName; + case 'fj': + return L10n.of(context)!.fjDisplayName; + case 'fi': + return L10n.of(context)!.fiDisplayName; + case 'fr': + return L10n.of(context)!.frDisplayName; + case 'ff': + return L10n.of(context)!.ffDisplayName; + case 'gl': + return L10n.of(context)!.glDisplayName; + case 'ka': + return L10n.of(context)!.kaDisplayName; + case 'de': + return L10n.of(context)!.deDisplayName; + case 'el': + return L10n.of(context)!.elDisplayName; + case 'gn': + return L10n.of(context)!.gnDisplayName; + case 'gu': + return L10n.of(context)!.guDisplayName; + case 'ht': + return L10n.of(context)!.htDisplayName; + case 'ha': + return L10n.of(context)!.haDisplayName; + case 'he': + return L10n.of(context)!.heDisplayName; + case 'hz': + return L10n.of(context)!.hzDisplayName; + case 'hi': + return L10n.of(context)!.hiDisplayName; + case 'ho': + return L10n.of(context)!.hoDisplayName; + case 'hu': + return L10n.of(context)!.huDisplayName; + case 'ia': + return L10n.of(context)!.iaDisplayName; + case 'id': + return L10n.of(context)!.idDisplayName; + case 'ie': + return L10n.of(context)!.ieDisplayName; + case 'ga': + return L10n.of(context)!.gaDisplayName; + case 'ig': + return L10n.of(context)!.igDisplayName; + case 'ik': + return L10n.of(context)!.ikDisplayName; + case 'io': + return L10n.of(context)!.ioDisplayName; + case 'is': + return L10n.of(context)!.isDisplayName; + case 'it': + return L10n.of(context)!.itDisplayName; + case 'iu': + return L10n.of(context)!.iuDisplayName; + case 'ja': + return L10n.of(context)!.jaDisplayName; + case 'jv': + return L10n.of(context)!.jvDisplayName; + case 'kl': + return L10n.of(context)!.klDisplayName; + case 'kn': + return L10n.of(context)!.knDisplayName; + case 'kr': + return L10n.of(context)!.krDisplayName; + case 'ks': + return L10n.of(context)!.ksDisplayName; + case 'kk': + return L10n.of(context)!.kkDisplayName; + case 'km': + return L10n.of(context)!.kmDisplayName; + case 'ki': + return L10n.of(context)!.kiDisplayName; + case 'rw': + return L10n.of(context)!.rwDisplayName; + case 'ky': + return L10n.of(context)!.kyDisplayName; + case 'kv': + return L10n.of(context)!.kvDisplayName; + case 'kg': + return L10n.of(context)!.kgDisplayName; + case 'ko': + return L10n.of(context)!.koDisplayName; + case 'ku': + return L10n.of(context)!.kuDisplayName; + case 'kj': + return L10n.of(context)!.kjDisplayName; + case 'la': + return L10n.of(context)!.laDisplayName; + case 'lb': + return L10n.of(context)!.lbDisplayName; + case 'lg': + return L10n.of(context)!.lgDisplayName; + case 'li': + return L10n.of(context)!.liDisplayName; + case 'ln': + return L10n.of(context)!.lnDisplayName; + case 'lo': + return L10n.of(context)!.loDisplayName; + case 'lt': + return L10n.of(context)!.ltDisplayName; + case 'lu': + return L10n.of(context)!.luDisplayName; + case 'lv': + return L10n.of(context)!.lvDisplayName; + case 'gv': + return L10n.of(context)!.gvDisplayName; + case 'mk': + return L10n.of(context)!.mkDisplayName; + case 'mg': + return L10n.of(context)!.mgDisplayName; + case 'ms': + return L10n.of(context)!.msDisplayName; + case 'ml': + return L10n.of(context)!.mlDisplayName; + case 'mt': + return L10n.of(context)!.mtDisplayName; + case 'mi': + return L10n.of(context)!.miDisplayName; + case 'mr': + return L10n.of(context)!.mrDisplayName; + case 'mh': + return L10n.of(context)!.mhDisplayName; + case 'mn': + return L10n.of(context)!.mnDisplayName; + case 'na': + return L10n.of(context)!.naDisplayName; + case 'nv': + return L10n.of(context)!.nvDisplayName; + case 'nb': + return L10n.of(context)!.nbDisplayName; + case 'nd': + return L10n.of(context)!.ndDisplayName; + case 'ne': + return L10n.of(context)!.neDisplayName; + case 'ng': + return L10n.of(context)!.ngDisplayName; + case 'nn': + return L10n.of(context)!.nnDisplayName; + case 'no': + return L10n.of(context)!.noDisplayName; + case 'ii': + return L10n.of(context)!.iiDisplayName; + case 'nr': + return L10n.of(context)!.nrDisplayName; + case 'oc': + return L10n.of(context)!.ocDisplayName; + case 'oj': + return L10n.of(context)!.ojDisplayName; + case 'cu': + return L10n.of(context)!.cuDisplayName; + case 'om': + return L10n.of(context)!.omDisplayName; + case 'or': + return L10n.of(context)!.orDisplayName; + case 'os': + return L10n.of(context)!.osDisplayName; + case 'pa': + return L10n.of(context)!.paDisplayName; + case 'pi': + return L10n.of(context)!.piDisplayName; + case 'fa': + return L10n.of(context)!.faDisplayName; + case 'pl': + return L10n.of(context)!.plDisplayName; + case 'ps': + return L10n.of(context)!.psDisplayName; + case 'pt': + return L10n.of(context)!.ptDisplayName; + case 'qu': + return L10n.of(context)!.quDisplayName; + case 'rm': + return L10n.of(context)!.rmDisplayName; + case 'rn': + return L10n.of(context)!.rnDisplayName; + case 'ro': + return L10n.of(context)!.roDisplayName; + case 'ru': + return L10n.of(context)!.ruDisplayName; + case 'sa': + return L10n.of(context)!.saDisplayName; + case 'sc': + return L10n.of(context)!.scDisplayName; + case 'sd': + return L10n.of(context)!.sdDisplayName; + case 'se': + return L10n.of(context)!.seDisplayName; + case 'sm': + return L10n.of(context)!.smDisplayName; + case 'sg': + return L10n.of(context)!.sgDisplayName; + case 'sr': + return L10n.of(context)!.srDisplayName; + case 'gd': + return L10n.of(context)!.gdDisplayName; + case 'sn': + return L10n.of(context)!.snDisplayName; + case 'si': + return L10n.of(context)!.siDisplayName; + case 'sk': + return L10n.of(context)!.skDisplayName; + case 'sl': + return L10n.of(context)!.slDisplayName; + case 'so': + return L10n.of(context)!.soDisplayName; + case 'st': + return L10n.of(context)!.stDisplayName; + case 'es': + return L10n.of(context)!.esDisplayName; + case 'su': + return L10n.of(context)!.suDisplayName; + case 'sw': + return L10n.of(context)!.swDisplayName; + case 'ss': + return L10n.of(context)!.ssDisplayName; + case 'sv': + return L10n.of(context)!.svDisplayName; + case 'ta': + return L10n.of(context)!.taDisplayName; + case 'te': + return L10n.of(context)!.teDisplayName; + case 'tg': + return L10n.of(context)!.tgDisplayName; + case 'th': + return L10n.of(context)!.thDisplayName; + case 'ti': + return L10n.of(context)!.tiDisplayName; + case 'bo': + return L10n.of(context)!.boDisplayName; + case 'tk': + return L10n.of(context)!.tkDisplayName; + case 'tl': + return L10n.of(context)!.tlDisplayName; + case 'tn': + return L10n.of(context)!.tnDisplayName; + case 'to': + return L10n.of(context)!.toDisplayName; + case 'tr': + return L10n.of(context)!.trDisplayName; + case 'ts': + return L10n.of(context)!.tsDisplayName; + case 'tt': + return L10n.of(context)!.ttDisplayName; + case 'tw': + return L10n.of(context)!.twDisplayName; + case 'ty': + return L10n.of(context)!.tyDisplayName; + case 'ug': + return L10n.of(context)!.ugDisplayName; + case 'uk': + return L10n.of(context)!.ukDisplayName; + case 'ur': + return L10n.of(context)!.urDisplayName; + case 'uz': + return L10n.of(context)!.uzDisplayName; + case 've': + return L10n.of(context)!.veDisplayName; + case 'vi': + return L10n.of(context)!.viDisplayName; + case 'vo': + return L10n.of(context)!.voDisplayName; + case 'wa': + return L10n.of(context)!.waDisplayName; + case 'cy': + return L10n.of(context)!.cyDisplayName; + case 'wo': + return L10n.of(context)!.woDisplayName; + case 'fy': + return L10n.of(context)!.fyDisplayName; + case 'xh': + return L10n.of(context)!.xhDisplayName; + case 'yi': + return L10n.of(context)!.yiDisplayName; + case 'yo': + return L10n.of(context)!.yoDisplayName; + case 'za': + return L10n.of(context)!.zaDisplayName; + case 'unk': + return L10n.of(context)!.unkDisplayName; + case 'zu': + return L10n.of(context)!.zuDisplayName; + case 'haw': + return L10n.of(context)!.hawDisplayName; + case 'hmn': + return L10n.of(context)!.hmnDisplayName; + case 'multi': + return L10n.of(context)!.multiDisplayName; + case 'ceb': + return L10n.of(context)!.cebDisplayName; + case 'dz': + return L10n.of(context)!.dzDisplayName; + case 'iw': + return L10n.of(context)!.iwDisplayName; + case 'jw': + return L10n.of(context)!.jwDisplayName; + case 'mo': + return L10n.of(context)!.moDisplayName; + case 'sh': + return L10n.of(context)!.shDisplayName; + } + debugger(when: kDebugMode); + ErrorHandler.logError(m: "No Display name found", s: StackTrace.current); + return null; + } +} + +class _LanguageLocal { + static const isoLangs = { + "ab": {"name": "Abkhaz", "nativeName": "аҧсуа"}, + "aa": {"name": "Afar", "nativeName": "Afaraf"}, + "af": {"name": "Afrikaans", "nativeName": "Afrikaans"}, + "ak": {"name": "Akan", "nativeName": "Akan"}, + "sq": {"name": "Albanian", "nativeName": "Shqip"}, + "am": {"name": "Amharic", "nativeName": "አማርኛ"}, + "ar": {"name": "Arabic", "nativeName": "العربية"}, + "an": {"name": "Aragonese", "nativeName": "Aragonés"}, + "hy": {"name": "Armenian", "nativeName": "Հայերեն"}, + "as": {"name": "Assamese", "nativeName": "অসমীয়া"}, + "av": {"name": "Avaric", "nativeName": "авар мацӀ, магӀарул мацӀ"}, + "ae": {"name": "Avestan", "nativeName": "avesta"}, + "ay": {"name": "Aymara", "nativeName": "aymar aru"}, + "az": {"name": "Azerbaijani", "nativeName": "azərbaycan dili"}, + "bm": {"name": "Bambara", "nativeName": "bamanankan"}, + "ba": {"name": "Bashkir", "nativeName": "башҡорт теле"}, + "eu": {"name": "Basque", "nativeName": "euskara, euskera"}, + "be": {"name": "Belarusian", "nativeName": "Беларуская"}, + "bn": {"name": "Bengali", "nativeName": "বাংলা"}, + "bh": {"name": "Bihari", "nativeName": "भोजपुरी"}, + "bi": {"name": "Bislama", "nativeName": "Bislama"}, + "bs": {"name": "Bosnian", "nativeName": "bosanski jezik"}, + "br": {"name": "Breton", "nativeName": "brezhoneg"}, + "bg": {"name": "Bulgarian", "nativeName": "български език"}, + "my": {"name": "Burmese", "nativeName": "ဗမာစာ"}, + "ca": {"name": "Catalan, Valencian", "nativeName": "Català"}, + "ch": {"name": "Chamorro", "nativeName": "Chamoru"}, + "ce": {"name": "Chechen", "nativeName": "нохчийн мотт"}, + "ny": { + "name": "Chichewa, Chewa, Nyanja", + "nativeName": "chiCheŵa, chinyanja" + }, + "zh": {"name": "Chinese", "nativeName": "中文 (Zhōngwén), 汉语, 漢語"}, + "cv": {"name": "Chuvash", "nativeName": "чӑваш чӗлхи"}, + "kw": {"name": "Cornish", "nativeName": "Kernewek"}, + "co": {"name": "Corsican", "nativeName": "corsu, lingua corsa"}, + "cr": {"name": "Cree", "nativeName": "ᓀᐦᐃᔭᐍᐏᐣ"}, + "hr": {"name": "Croatian", "nativeName": "hrvatski"}, + "cs": {"name": "Czech", "nativeName": "česky, čeština"}, + "da": {"name": "Danish", "nativeName": "dansk"}, + "dv": {"name": "Divehi; Dhivehi; Maldivian;", "nativeName": "ދިވެހި"}, + "nl": {"name": "Dutch", "nativeName": "Nederlands, Vlaams"}, + "en": {"name": "English", "nativeName": "English"}, + "eo": {"name": "Esperanto", "nativeName": "Esperanto"}, + "et": {"name": "Estonian", "nativeName": "eesti, eesti keel"}, + "ee": {"name": "Ewe", "nativeName": "Evegbe"}, + "fo": {"name": "Faroese", "nativeName": "føroyskt"}, + "fj": {"name": "Fijian", "nativeName": "vosa Vakaviti"}, + "fi": {"name": "Finnish", "nativeName": "suomi, suomen kieli"}, + "fr": {"name": "French", "nativeName": "français, langue française"}, + "ff": { + "name": "Fula; Fulah; Pulaar; Pular", + "nativeName": "Fulfulde, Pulaar, Pular" + }, + "gl": {"name": "Galician", "nativeName": "Galego"}, + "ka": {"name": "Georgian", "nativeName": "ქართული"}, + "de": {"name": "German", "nativeName": "Deutsch"}, + "el": {"name": "Greek, Modern", "nativeName": "Ελληνικά"}, + "gn": {"name": "Guaraní", "nativeName": "Avañeẽ"}, + "gu": {"name": "Gujarati", "nativeName": "ગુજરાતી"}, + "ht": {"name": "Haitian, Haitian Creole", "nativeName": "Kreyòl ayisyen"}, + "ha": {"name": "Hausa", "nativeName": "Hausa, هَوُسَ"}, + "he": {"name": "Hebrew (modern)", "nativeName": "עברית"}, + "hz": {"name": "Herero", "nativeName": "Otjiherero"}, + "hi": {"name": "Hindi", "nativeName": "हिन्दी, हिंदी"}, + "ho": {"name": "Hiri Motu", "nativeName": "Hiri Motu"}, + "hu": {"name": "Hungarian", "nativeName": "Magyar"}, + "ia": {"name": "Interlingua", "nativeName": "Interlingua"}, + "id": {"name": "Indonesian", "nativeName": "Bahasa Indonesia"}, + "ie": { + "name": "Interlingue", + "nativeName": "Originally called Occidental; then Interlingue after WWII" + }, + "ga": {"name": "Irish", "nativeName": "Gaeilge"}, + "ig": {"name": "Igbo", "nativeName": "Asụsụ Igbo"}, + "ik": {"name": "Inupiaq", "nativeName": "Iñupiaq, Iñupiatun"}, + "io": {"name": "Ido", "nativeName": "Ido"}, + "is": {"name": "Icelandic", "nativeName": "Íslenska"}, + "it": {"name": "Italian", "nativeName": "Italiano"}, + "iu": {"name": "Inuktitut", "nativeName": "ᐃᓄᒃᑎᑐᑦ"}, + "ja": {"name": "Japanese", "nativeName": "日本語 (にほんご/にっぽんご)"}, + "jv": {"name": "Javanese", "nativeName": "basa Jawa"}, + "kl": { + "name": "Kalaallisut, Greenlandic", + "nativeName": "kalaallisut, kalaallit oqaasii" + }, + "kn": {"name": "Kannada", "nativeName": "ಕನ್ನಡ"}, + "kr": {"name": "Kanuri", "nativeName": "Kanuri"}, + "ks": {"name": "Kashmiri", "nativeName": "कश्मीरी, كشميري"}, + "kk": {"name": "Kazakh", "nativeName": "Қазақ тілі"}, + "km": {"name": "Khmer", "nativeName": "ភាសាខ្មែរ"}, + "ki": {"name": "Kikuyu, Gikuyu", "nativeName": "Gĩkũyũ"}, + "rw": {"name": "Kinyarwanda", "nativeName": "Ikinyarwanda"}, + "ky": {"name": "Kirghiz, Kyrgyz", "nativeName": "кыргыз тили"}, + "kv": {"name": "Komi", "nativeName": "коми кыв"}, + "kg": {"name": "Kongo", "nativeName": "KiKongo"}, + "ko": {"name": "Korean", "nativeName": "한국어 (韓國語), 조선말 (朝鮮語)"}, + "ku": {"name": "Kurdish", "nativeName": "Kurdî, كوردی"}, + "kj": {"name": "Kwanyama, Kuanyama", "nativeName": "Kuanyama"}, + "la": {"name": "Latin", "nativeName": "latine, lingua latina"}, + "lb": { + "name": "Luxembourgish, Letzeburgesch", + "nativeName": "Lëtzebuergesch" + }, + "lg": {"name": "Luganda", "nativeName": "Luganda"}, + "li": { + "name": "Limburgish, Limburgan, Limburger", + "nativeName": "Limburgs" + }, + "ln": {"name": "Lingala", "nativeName": "Lingála"}, + "lo": {"name": "Lao", "nativeName": "ພາສາລາວ"}, + "lt": {"name": "Lithuanian", "nativeName": "lietuvių kalba"}, + "lu": {"name": "Luba-Katanga", "nativeName": ""}, + "lv": {"name": "Latvian", "nativeName": "latviešu valoda"}, + "gv": {"name": "Manx", "nativeName": "Gaelg, Gailck"}, + "mk": {"name": "Macedonian", "nativeName": "македонски јазик"}, + "mg": {"name": "Malagasy", "nativeName": "Malagasy fiteny"}, + "ms": {"name": "Malay", "nativeName": "bahasa Melayu, بهاس ملايو"}, + "ml": {"name": "Malayalam", "nativeName": "മലയാളം"}, + "mt": {"name": "Maltese", "nativeName": "Malti"}, + "mi": {"name": "Māori", "nativeName": "te reo Māori"}, + "mr": {"name": "Marathi (Marāṭhī)", "nativeName": "मराठी"}, + "mh": {"name": "Marshallese", "nativeName": "Kajin M̧ajeļ"}, + "mn": {"name": "Mongolian", "nativeName": "монгол"}, + "na": {"name": "Nauru", "nativeName": "Ekakairũ Naoero"}, + "nv": {"name": "Navajo, Navaho", "nativeName": "Diné bizaad, Dinék'ehǰí"}, + "nb": {"name": "Norwegian Bokmål", "nativeName": "Norsk bokmål"}, + "nd": {"name": "North Ndebele", "nativeName": "isiNdebele"}, + "ne": {"name": "Nepali", "nativeName": "नेपाली"}, + "ng": {"name": "Ndonga", "nativeName": "Owambo"}, + "nn": {"name": "Norwegian Nynorsk", "nativeName": "Norsk nynorsk"}, + "no": {"name": "Norwegian", "nativeName": "Norsk"}, + "ii": {"name": "Nuosu", "nativeName": "ꆈꌠ꒿ Nuosuhxop"}, + "nr": {"name": "South Ndebele", "nativeName": "isiNdebele"}, + "oc": {"name": "Occitan", "nativeName": "Occitan"}, + "oj": {"name": "Ojibwe, Ojibwa", "nativeName": "ᐊᓂᔑᓈᐯᒧᐎᓐ"}, + "cu": { + "name": + "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic", + "nativeName": "ѩзыкъ словѣньскъ" + }, + "om": {"name": "Oromo", "nativeName": "Afaan Oromoo"}, + "or": {"name": "Oriya", "nativeName": "ଓଡ଼ିଆ"}, + "os": {"name": "Ossetian, Ossetic", "nativeName": "ирон æвзаг"}, + "pa": {"name": "Panjabi, Punjabi", "nativeName": "ਪੰਜਾਬੀ, پنجابی"}, + "pi": {"name": "Pāli", "nativeName": "पाऴि"}, + "fa": {"name": "Persian", "nativeName": "فارسی"}, + "pl": {"name": "Polish", "nativeName": "polski"}, + "ps": {"name": "Pashto, Pushto", "nativeName": "پښتو"}, + "pt": {"name": "Portuguese", "nativeName": "Português"}, + "qu": {"name": "Quechua", "nativeName": "Runa Simi, Kichwa"}, + "rm": {"name": "Romansh", "nativeName": "rumantsch grischun"}, + "rn": {"name": "Kirundi", "nativeName": "kiRundi"}, + "ro": {"name": "Romanian, Moldavian, Moldovan", "nativeName": "română"}, + "ru": {"name": "Russian", "nativeName": "русский язык"}, + "sa": {"name": "Sanskrit (Saṁskṛta)", "nativeName": "संस्कृतम्"}, + "sc": {"name": "Sardinian", "nativeName": "sardu"}, + "sd": {"name": "Sindhi", "nativeName": "सिन्धी, سنڌي، سندھی"}, + "se": {"name": "Northern Sami", "nativeName": "Davvisámegiella"}, + "sm": {"name": "Samoan", "nativeName": "gagana faa Samoa"}, + "sg": {"name": "Sango", "nativeName": "yângâ tî sängö"}, + "sr": {"name": "Serbian", "nativeName": "српски језик"}, + "gd": {"name": "Scottish Gaelic, Gaelic", "nativeName": "Gàidhlig"}, + "sn": {"name": "Shona", "nativeName": "chiShona"}, + "si": {"name": "Sinhala, Sinhalese", "nativeName": "සිංහල"}, + "sk": {"name": "Slovak", "nativeName": "Slovenčina"}, + "sl": {"name": "Slovene", "nativeName": "Slovenščina"}, + "so": {"name": "Somali", "nativeName": "Soomaaliga, af Soomaali"}, + "st": {"name": "Southern Sotho", "nativeName": "Sesotho"}, + "es": {"name": "Spanish, Castilian", "nativeName": "Español, Castellano"}, + "su": {"name": "Sundanese", "nativeName": "Basa Sunda"}, + "sw": {"name": "Swahili", "nativeName": "Kiswahili"}, + "ss": {"name": "Swati", "nativeName": "SiSwati"}, + "sv": {"name": "Swedish", "nativeName": "svenska"}, + "ta": {"name": "Tamil", "nativeName": "தமிழ்"}, + "te": {"name": "Telugu", "nativeName": "తెలుగు"}, + "tg": {"name": "Tajik", "nativeName": "тоҷикӣ, toğikī, تاجیکی"}, + "th": {"name": "Thai", "nativeName": "ไทย"}, + "ti": {"name": "Tigrinya", "nativeName": "ትግርኛ"}, + "bo": { + "name": "Tibetan Standard, Tibetan, Central", + "nativeName": "བོད་ཡིག" + }, + "tk": {"name": "Turkmen", "nativeName": "Türkmen, Түркмен"}, + "tl": {"name": "Tagalog", "nativeName": "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"}, + "tn": {"name": "Tswana", "nativeName": "Setswana"}, + "to": {"name": "Tonga (Tonga Islands)", "nativeName": "faka Tonga"}, + "tr": {"name": "Turkish", "nativeName": "Türkçe"}, + "ts": {"name": "Tsonga", "nativeName": "Xitsonga"}, + "tt": {"name": "Tatar", "nativeName": "татарча, tatarça, تاتارچا"}, + "tw": {"name": "Twi", "nativeName": "Twi"}, + "ty": {"name": "Tahitian", "nativeName": "Reo Tahiti"}, + "ug": {"name": "Uighur, Uyghur", "nativeName": "Uyƣurqə, ئۇيغۇرچە"}, + "uk": {"name": "Ukrainian", "nativeName": "українська"}, + "ur": {"name": "Urdu", "nativeName": "اردو"}, + "uz": {"name": "Uzbek", "nativeName": "zbek, Ўзбек, أۇزبېك"}, + "ve": {"name": "Venda", "nativeName": "Tshivenḓa"}, + "vi": {"name": "Vietnamese", "nativeName": "Tiếng Việt"}, + "vo": {"name": "Volapük", "nativeName": "Volapük"}, + "wa": {"name": "Walloon", "nativeName": "Walon"}, + "cy": {"name": "Welsh", "nativeName": "Cymraeg"}, + "wo": {"name": "Wolof", "nativeName": "Wollof"}, + "fy": {"name": "Western Frisian", "nativeName": "Frysk"}, + "xh": {"name": "Xhosa", "nativeName": "isiXhosa"}, + "yi": {"name": "Yiddish", "nativeName": "ייִדיש"}, + "yo": {"name": "Yoruba", "nativeName": "Yorùbá"}, + "za": {"name": "Zhuang, Chuang", "nativeName": "Saw cueŋƅ, Saw cuengh"}, + "unk": {"name": "Unknown", "nativeName": "Saw cueŋƅ, Saw cuengh"}, + "zu": {"name": "Zulu", "nativeName": "Zulu"}, + "haw": {"name": "Hawaiian", "nativeName": "Hawaiian"}, + "hmn": {"name": "Hmong", "nativeName": "Hmong"}, + 'multi': {"name": "Multi", "nativeName": "Multi"}, + "ceb": {"name": "Cebuano", "nativeName": "Cebuano"}, + "dz": {"name": "Dzongkha", "nativeName": "Dzongkha"}, + "iw": {"name": "Hebrew", "nativeName": "Hebrew"}, + "jw": {"name": "Javanese", "nativeName": "Javanese"}, + "mo": {"name": "Moldavian", "nativeName": "Moldavian"}, + "sh": {"name": "Serbo-Croatian", "nativeName": "Serbo-Croatian"}, + }; + + static String getDisplayName(String key, [native = false]) { + final Map? item = isoLangs[key]; + if (item == null) { + // debugger(when: kDebugMode); + // ErrorHandler.logError(m: "Bad language key $key", s: StackTrace.current); + } + if (item == null) return key; + + return (native ? item["nativeName"]! : item["name"]!).split(",")[0]; + } + + static String langCodeFromName(String? name) { + if (name == null) return LanguageKeys.unknownLanguage; + if (isoLangs.containsKey(name)) return name; + + final String searchName = name.toLowerCase().split(" ")[0]; + for (final entry in isoLangs.entries) { + if (entry.value["name"]!.toLowerCase().contains(searchName)) { + return entry.key; + } + } + // debugger(when: kDebugMode); + // ErrorHandler.logError(m: "Bad language name $name", s: StackTrace.current); + return LanguageKeys.unknownLanguage; + } +} diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart new file mode 100644 index 000000000..2ad0b2950 --- /dev/null +++ b/lib/pangea/models/lemma.dart @@ -0,0 +1,24 @@ +class Lemma { + final String text; + final String form; + final bool saveVocab; + + Lemma({required this.text, required this.saveVocab, required this.form}); + + factory Lemma.fromJson(Map json) { + return Lemma( + text: json['text'], + saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false, + form: json["form"] ?? json['text'], + ); + } + + toJson() { + return {'text': text, 'save_vocab': saveVocab, 'form': form}; + } + + static Lemma get empty => Lemma(text: '', saveVocab: true, form: ''); + + static Lemma create(String form) => + Lemma(text: '', saveVocab: true, form: form); +} diff --git a/lib/pangea/models/message_data_models.dart b/lib/pangea/models/message_data_models.dart new file mode 100644 index 000000000..014c27c81 --- /dev/null +++ b/lib/pangea/models/message_data_models.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:matrix/matrix.dart'; + +/// this class is contained within a [RepresentationEvent] +/// this event is the child of a [EventTypes.Message] +/// the event has two potential children events - +/// [PangeaTokensEvent] and [PangeaIGCEvent] +/// these events contain [PangeaMessageTokens] and [ChoreoRecord], respectively. +class PangeaRepresentation { + /// system-detected language, possibly condensed from a list, + /// but only with high certainty + /// cannot be "unk" + String langCode; + + /// final sent text + /// if this was a process, a [PangeaIGCEvent] will contain changes + String text; + + bool originalSent; + bool originalWritten; + + // how do we know which representation was sent by author? + // RepresentationEvent.text == PangeaMessageEvent.event.body + // use: to know whether directUse + + // how do we know which representation was original L1 message that was translated (if it exists)? + // (of l2 rep) RepresentationEvent.igc.steps.first.text = RepresentationEvent.text (of L1 rep) + // use: for base text for future translations + + // os = true and ow = false + // rep that went through IGC/IT + + // os = false and ow = false + // rep added by other user + + // os = true and ow = true + // potentially L1 language use, maybe with limited IGC, and ignored out of target cries + // potentially perfect L2 use + + // os = false and ow = true + // L1 message that then went through significant IGC and/or IT + // L2 message with errors that went through IGC + + PangeaRepresentation({ + required this.langCode, + required this.text, + required this.originalSent, + required this.originalWritten, + }); + + factory PangeaRepresentation.fromJson(Map json) => + PangeaRepresentation( + langCode: json[_langCodeKey], + text: json[_textKey], + originalSent: json[_originalSentKey] ?? false, + originalWritten: json[_originalWrittenKey] ?? false, + ); + + static const _textKey = "txt"; + static const _langCodeKey = "lang"; + static const _originalSentKey = "snt"; + static const _originalWrittenKey = "wrttn"; + + Map toJson() { + final data = {}; + data[_textKey] = text; + data[_langCodeKey] = langCode; + if (originalSent) data[_originalSentKey] = originalSent; + if (originalWritten) data[_originalWrittenKey] = originalWritten; + return data; + } +} + +/// this class lives within a [PangeaTokensEvent] +/// it always has a [RepresentationEvent] parent +/// These live as separate event so that anyone can add and edit tokens to +/// representation +class PangeaMessageTokens { + List tokens; + + PangeaMessageTokens({ + required this.tokens, + }); + + factory PangeaMessageTokens.fromJson(Map json) { + return PangeaMessageTokens( + tokens: (jsonDecode(json[_tokensKey] ?? "[]") as Iterable) + .map((e) => PangeaToken.fromJson(e)) + .toList() + .cast(), + ); + } + + static const _tokensKey = "tkns"; + + Map toJson() { + final data = {}; + data[_tokensKey] = jsonEncode(tokens.map((e) => e.toJson()).toList()); + return data; + } +} diff --git a/lib/pangea/models/mobile_subscriptions.dart b/lib/pangea/models/mobile_subscriptions.dart new file mode 100644 index 000000000..8ae224a4b --- /dev/null +++ b/lib/pangea/models/mobile_subscriptions.dart @@ -0,0 +1,212 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/models/base_subscription_info.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:purchases_flutter/purchases_flutter.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class MobileSubscriptionInfo extends SubscriptionInfo { + MobileSubscriptionInfo({required super.pangeaController}) : super(); + + @override + Future configure() async { + final PurchasesConfiguration configuration = Platform.isAndroid + ? PurchasesConfiguration(Environment.rcGoogleKey) + : PurchasesConfiguration(Environment.rcIosKey); + try { + await Purchases.configure( + configuration..appUserID = pangeaController.userController.userId, + ); + } catch (err) { + ErrorHandler.logError( + m: "Failed to configure revenuecat SDK for user ${pangeaController.userController.userId}", + s: StackTrace.current, + ); + debugPrint( + "Failed to configure revenuecat SDK for user ${pangeaController.userController.userId}", + ); + return; + } + await setAppIds(); + await setAllProducts(); + await setCustomerInfo(); + await setMobilePackages(); + if (allProducts != null && appIds != null) { + availableSubscriptions = allProducts! + .where((product) => product.appId == appIds!.currentAppId) + .toList(); + availableSubscriptions.sort((a, b) => a.price.compareTo(b.price)); + + if (currentSubscriptionId == null && !hasSubscribed) { + //@Gabby - temporary solution to add trial to list + final id = availableSubscriptions[0].id; + final package = availableSubscriptions[0].package; + final duration = availableSubscriptions[0].duration; + availableSubscriptions.insert( + 0, + SubscriptionDetails( + price: 0, + id: id, + duration: duration, + package: package, + periodType: 'trial', + ), + ); + } + } else { + ErrorHandler.logError(e: Exception("allProducts null || appIds null")); + } + } + + Future setMobilePackages() async { + if (allProducts == null) { + ErrorHandler.logError( + m: "Null appProducts in setMobilePrices", + s: StackTrace.current, + ); + debugPrint( + "Null appProducts in setMobilePrices", + ); + return; + } + Offerings offerings; + try { + offerings = await Purchases.getOfferings(); + } catch (err) { + ErrorHandler.logError( + m: "Failed to fetch revenuecat offerings from revenuecat", + s: StackTrace.current, + ); + debugPrint( + "Failed to fetch revenuecat offerings from revenuecat", + ); + return; + } + final Offering? offering = offerings.all[Environment.rcOfferingName]; + if (offering != null) { + final List mobileSubscriptions = + offering.availablePackages + .map( + (package) { + return SubscriptionDetails( + price: package.storeProduct.price, + id: package.storeProduct.identifier, + package: package, + ); + }, + ) + .toList() + .cast(); + for (final SubscriptionDetails mobileSub in mobileSubscriptions) { + final int productIndex = allProducts! + .indexWhere((product) => product.id.contains(mobileSub.id)); + if (productIndex >= 0) { + final SubscriptionDetails updated = allProducts![productIndex]; + updated.package = mobileSub.package; + allProducts![productIndex] = updated; + } + } + } + } + + @override + Future setCustomerInfo() async { + if (allProducts == null) { + ErrorHandler.logError( + m: "Null appProducts in setCustomerInfo", + s: StackTrace.current, + ); + debugPrint( + "Null appProducts in setCustomerInfo", + ); + return; + } + + CustomerInfo info; + try { + // await Purchases.syncPurchases(); + info = await Purchases.getCustomerInfo(); + } catch (err) { + ErrorHandler.logError( + m: "Failed to fetch revenuecat customer info for user ${pangeaController.userController.userId}", + s: StackTrace.current, + ); + debugPrint( + "Failed to fetch revenuecat customer info for user ${pangeaController.userController.userId}", + ); + return; + } + final List noExpirations = + getEntitlementsWithoutExpiration(info); + + if (noExpirations.isNotEmpty) { + Sentry.addBreadcrumb( + Breadcrumb( + message: + "Found revenuecat entitlement(s) without expiration date for user ${pangeaController.userController.userId}: ${noExpirations.map( + (entry) => + "Entitlement Id: ${entry.identifier}, Purchase Date: ${entry.originalPurchaseDate}", + )}", + ), + ); + } + + final List activeEntitlements = info + .entitlements.all.entries + .where((MapEntry entry) => + entry.value.expirationDate == null || + DateTime.parse(entry.value.expirationDate!).isAfter(DateTime.now())) + .map((MapEntry entry) => entry.value) + .toList(); + + allEntitlements = info.entitlements.all.entries + .map((MapEntry entry) => + entry.value.productIdentifier) + .cast() + .toList(); + + if (activeEntitlements.length > 1) { + debugPrint( + "User has more than one active entitlement.", + ); + } else if (activeEntitlements.isEmpty) { + debugPrint("User has no active entitlements"); + resetSubscription(); + return; + } + final EntitlementInfo activeEntitlement = activeEntitlements[0]; + currentSubscriptionId = activeEntitlement.productIdentifier; + currentSubscription = allProducts!.firstWhereOrNull( + (SubscriptionDetails sub) => + sub.id.contains(currentSubscriptionId!) || + currentSubscriptionId!.contains(sub.id), + ); + expirationDate = activeEntitlement.expirationDate != null + ? DateTime.parse(activeEntitlement.expirationDate!) + : null; + + if (activeEntitlement.periodType == PeriodType.trial) { + currentSubscription?.makeTrial(); + } + if (currentSubscriptionId != null && currentSubscription == null) { + Sentry.addBreadcrumb( + Breadcrumb(message: "mismatch of productIds and currentSubscriptionID"), + ); + } + } + + List getEntitlementsWithoutExpiration(CustomerInfo info) { + final List noExpirations = info.entitlements.all.entries + .where( + (MapEntry entry) => + entry.value.expirationDate == null, + ) + .map((MapEntry entry) => entry.value) + .toList(); + return noExpirations; + } +} diff --git a/lib/pangea/models/pangea_choreo_event.dart b/lib/pangea/models/pangea_choreo_event.dart new file mode 100644 index 000000000..a56c798e9 --- /dev/null +++ b/lib/pangea/models/pangea_choreo_event.dart @@ -0,0 +1,54 @@ +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; +import 'choreo_record.dart'; + +class ChoreoEvent { + Event event; + ChoreoRecord? _content; + + ChoreoEvent({required this.event}) { + if (event.type != PangeaEventTypes.choreoRecord) { + throw Exception( + "${event.type} should not be used to make a ChoreoEvent", + ); + } + } + + ChoreoRecord? get content { + try { + _content ??= event.getPangeaContent(); + return _content; + } catch (err, s) { + if (kDebugMode) rethrow; + ErrorHandler.logError(e: err, s: s); + return null; + } + } + + // bool get hasAcceptedMatches => + // content?.steps.any( + // (element) => + // element.acceptedOrIgnoredMatch?.status == + // PangeaMatchStatus.accepted, + // ) ?? + // false; + + // bool get hasIgnoredMatches => + // content?.steps.any( + // (element) => + // element.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.ignored, + // ) ?? + // false; + + // bool get includedIT => + // content?.steps.any((step) { + // return step.acceptedOrIgnoredMatch?.status == + // PangeaMatchStatus.accepted && + // (step.acceptedOrIgnoredMatch?.isITStart ?? false); + // }) ?? + // false; +} diff --git a/lib/pangea/models/pangea_match_model.dart b/lib/pangea/models/pangea_match_model.dart new file mode 100644 index 000000000..6f7db12b9 --- /dev/null +++ b/lib/pangea/models/pangea_match_model.dart @@ -0,0 +1,129 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/enum/span_data_type.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../constants/match_rule_ids.dart'; +import 'igc_text_data_model.dart'; +import 'span_data.dart'; + +enum PangeaMatchStatus { open, ignored, accepted, unknown } + +class PangeaMatch { + SpanData match; + + PangeaMatchStatus status; + + // String source; + + PangeaMatch({ + required this.match, + required this.status, + // required this.source, + }); + + factory PangeaMatch.fromJson(Map json) { + // try { + return PangeaMatch( + match: SpanData.fromJson(json[_matchKey] as Map), + status: json[_statusKey] != null + ? _statusStringToEnum(json[_statusKey]) + : PangeaMatchStatus.open, + // source: json[_matchKey]["source"] ?? "unk", + ); + // } catch (err) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // m: "unknown error in PangeaMatch.fromJson", data: json); + // rethrow; + // } + } + + String _statusEnumToString(dynamic status) => + status.toString().split('.').last; + + static PangeaMatchStatus _statusStringToEnum(String status) { + final String lastPart = status.toString().split('.').last; + switch (lastPart) { + case 'open': + return PangeaMatchStatus.open; + case 'ignored': + return PangeaMatchStatus.ignored; + case 'accepted': + return PangeaMatchStatus.accepted; + default: + return PangeaMatchStatus.unknown; + } + } + + static const _matchKey = "match"; + static const _statusKey = "status"; + + bool get isl1SpanMatch => needsTranslation; + + bool get isITStart => + match.rule?.id == MatchRuleIds.interactiveTranslation || + match.type.typeName == SpanDataTypeEnum.itStart; + + bool get needsTranslation => match.rule?.id != null + ? [ + MatchRuleIds.tokenNeedsTranslation, + MatchRuleIds.tokenSpanNeedsTranslation + ].contains(match.rule!.id) + : false; + + bool get isOutOfTargetMatch => isITStart || needsTranslation; + + bool get isGrammarMatch => !isOutOfTargetMatch; + + Map toJson() => { + _matchKey: match.toJson(), + // _detectionsKey: detections.map((e) => e.toJson()).toList(), + _statusKey: _statusEnumToString(status) + }; + + String get matchContent { + late int beginning; + late int end; + if (match.offset < 0) { + beginning = 0; + debugger(when: kDebugMode); + ErrorHandler.logError(m: "match.offset < 0", data: match.toJson()); + } else { + beginning = match.offset; + } + if (match.offset + match.length > match.fullText.length) { + end = match.fullText.length; + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "match.offset + match.length > match.fullText.length", + data: match.toJson()); + } else { + end = match.offset + match.length; + } + return match.fullText.substring(beginning, end); + } + + bool isOffsetInMatchSpan(int offset) => + offset >= match.offset && offset <= match.offset + match.length; + + Color get underlineColor { + switch (match.rule?.id ?? "unknown") { + case MatchRuleIds.interactiveTranslation: + return const Color.fromARGB(187, 132, 96, 224); + case MatchRuleIds.tokenNeedsTranslation: + case MatchRuleIds.tokenSpanNeedsTranslation: + return const Color.fromARGB(186, 255, 132, 0); + default: + return const Color.fromARGB(149, 255, 17, 0); + } + } + + TextStyle textStyle(TextStyle? existingStyle) => + existingStyle?.merge(IGCTextData.underlineStyle(underlineColor)) ?? + IGCTextData.underlineStyle(underlineColor); + + PangeaMatch get copyWith => PangeaMatch.fromJson(toJson()); +} diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart new file mode 100644 index 000000000..bb177d830 --- /dev/null +++ b/lib/pangea/models/pangea_message_event.dart @@ -0,0 +1,269 @@ +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; +import 'package:fluffychat/pangea/utils/bot_name.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import '../constants/language_keys.dart'; +import '../constants/model_keys.dart'; +import '../constants/pangea_event_types.dart'; +import '../enum/use_type.dart'; +import '../utils/error_handler.dart'; + +class PangeaMessageEvent { + late Event _event; + final Timeline timeline; + final bool ownMessage; + final bool selected; + bool _isValidPangeaMessageEvent = true; + + PangeaMessageEvent({ + required Event event, + required this.timeline, + required this.ownMessage, + required this.selected, + }) { + if (event.type != EventTypes.Message) { + _isValidPangeaMessageEvent = false; + ErrorHandler.logError( + m: "${event.type} should not be used to make a PangeaMessageEvent", + ); + } + _event = event; + } + + //the timeline filters the edits and uses the original events + //so this event will always be the original and the sdk getter body + //handles getting the latest text from the aggregated events + String get body => _event.body; + + String get senderId => _event.senderId; + + DateTime get originServerTs => _event.originServerTs; + + String get eventId => _event.eventId; + + Room get room => _event.room; + + Event? _latestEditCache; + Event get _latestEdit => _latestEditCache ??= _event + .aggregatedEvents( + timeline, + RelationshipTypes.edit, + ) + //sort by event.originServerTs to get the most recent first + .sorted( + (a, b) => b.originServerTs.compareTo(a.originServerTs), + ) + .firstOrNull ?? + _event; + + bool get showRichText { + 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) return false; + + return true; + } + + List? _representations; + List get representations { + if (_representations != null) return _representations!; + + _representations = []; + + if (_latestEdit.content[ModelKey.originalSent] != null) { + try { + _representations!.add( + RepresentationEvent( + content: PangeaRepresentation.fromJson( + _latestEdit.content[ModelKey.originalSent] + as Map, + ), + tokens: _latestEdit.content[ModelKey.tokensSent] != null + ? PangeaMessageTokens.fromJson( + _latestEdit.content[ModelKey.tokensSent] + as Map, + ) + : null, + choreo: _latestEdit.content[ModelKey.choreoRecord] != null + ? ChoreoRecord.fromJson( + _latestEdit.content[ModelKey.choreoRecord] + as Map, + ) + : null, + timeline: timeline, + ), + ); + } catch (err, s) { + ErrorHandler.logError( + e: err, + s: s, + ); + } + } + + if (_latestEdit.content[ModelKey.originalWritten] != null) { + _representations!.add( + RepresentationEvent( + content: PangeaRepresentation.fromJson( + _latestEdit.content[ModelKey.originalWritten] + as Map, + ), + tokens: _latestEdit.content[ModelKey.tokensWritten] != null + ? PangeaMessageTokens.fromJson( + _latestEdit.content[ModelKey.tokensWritten] + as Map, + ) + : null, + timeline: timeline, + ), + ); + } + + _representations!.addAll( + _latestEdit + .aggregatedEvents( + timeline, + PangeaEventTypes.representation, + ) + .map( + (e) => RepresentationEvent(event: e, timeline: timeline), + ) + .sorted( + (a, b) { + //TODO - test with edited events to make sure this is working + if (a.event == null) return -1; + if (b.event == null) return 1; + return b.event!.originServerTs.compareTo(a.event!.originServerTs); + }, + ).toList(), + ); + + return _representations!; + } + + RepresentationEvent? representationByLanguage(String langCode) => + representations.firstWhereOrNull( + (element) => element.langCode == langCode, + ); + + int translationIndex(String langCode) => representations.indexWhere( + (element) => element.langCode == langCode, + ); + + String translationTextSafe(String langCode) { + return representationByLanguage(langCode)?.text ?? body; + } + + bool get isNew => + DateTime.now().difference(originServerTs.toLocal()).inSeconds < 8; + + Future _repLocal(String langCode) async { + int tries = 0; + + RepresentationEvent? rep = representationByLanguage(langCode); + + while ((isNew || eventId.contains("web")) && tries < 20) { + if (rep != null) return rep; + await Future.delayed(const Duration(milliseconds: 500)); + rep = representationByLanguage(langCode); + tries += 1; + } + return rep; + } + + Future representationByLanguageGlobal({ + required BuildContext context, + required String langCode, + }) async { + // try { + final RepresentationEvent? repLocal = await _repLocal(langCode); + + if (repLocal != null || + langCode == LanguageKeys.unknownLanguage || + langCode == LanguageKeys.mixedLanguage || + langCode == LanguageKeys.multiLanguage) return repLocal; + + if (eventId.contains("web")) return null; + + // should this just be the original event body? + // worth a conversation with the team + final PangeaRepresentation? basis = + (originalWritten ?? originalSent)?.content; + + final Event? repEvent = await MatrixState.pangeaController.messageData + .getRepresentationMatrixEvent( + context: context, + messageEventId: _latestEdit.eventId, + text: basis?.text ?? _latestEdit.body, + target: langCode, + source: basis?.langCode, + room: _latestEdit.room, + ); + + // PTODO - if res.source different from langCode, save rep for source + + return repEvent != null + ? RepresentationEvent( + event: repEvent, + timeline: timeline, + ) + : null; + // } catch (err, s) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // e: err, + // s: s, + // ); + // return null; + // } + } + + RepresentationEvent? get originalSent => representations + .firstWhereOrNull((element) => element.content.originalSent); + + RepresentationEvent? get originalWritten => representations + .firstWhereOrNull((element) => element.content.originalWritten); + + PangeaRepresentation get defaultRepresentation => PangeaRepresentation( + langCode: LanguageKeys.unknownLanguage, + text: body, + originalSent: false, + originalWritten: false, + ); + + UseType get useType => useTypeCalculator(originalSent?.choreo); + + bool get showUseType => + !ownMessage && + _event.room.isSpaceAdmin && + _event.senderId != BotName.byEnvironment && + !room.isUserSpaceAdmin(_event.senderId) && + _event.messageType != PangeaMessageTypes.report; + + // List get activities => + //each match is turned into an activity that other students can access + //they're not told the answer but have to find it themselves + //the message has a blank piece which they fill in themselves +} + +class URLFinder { + static Iterable getMatches(String text) { + final RegExp exp = + RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'); + return exp.allMatches(text); + } +} diff --git a/lib/pangea/models/pangea_representation_event.dart b/lib/pangea/models/pangea_representation_event.dart new file mode 100644 index 000000000..31fce7252 --- /dev/null +++ b/lib/pangea/models/pangea_representation_event.dart @@ -0,0 +1,160 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/models/pangea_choreo_event.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/repo/tokens_repo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../widgets/matrix.dart'; +import '../constants/language_keys.dart'; +import '../constants/pangea_event_types.dart'; +import '../utils/error_handler.dart'; +import 'choreo_record.dart'; +import 'message_data_models.dart'; +import 'pangea_tokens_event.dart'; + +class RepresentationEvent { + Event? _event; + PangeaRepresentation? _content; + PangeaMessageTokens? _tokens; + ChoreoRecord? _choreo; + Timeline timeline; + + RepresentationEvent({ + required this.timeline, + Event? event, + PangeaRepresentation? content, + PangeaMessageTokens? tokens, + ChoreoRecord? choreo, + }) { + if (event != null && event.type != PangeaEventTypes.representation) { + throw Exception( + "${event.type} should not be used to make a RepresentationEvent", + ); + } + _event = event; + _content = content; + _tokens = tokens; + _choreo = choreo; + } + + Event? get event => _event; + + PangeaRepresentation get content { + if (_content != null) return _content!; + _content = _event?.getPangeaContent(); + return _content!; + } + + String get text => content.text; + + String get langCode => content.langCode; + + bool get botAuthored => + content.originalSent == false && content.originalWritten == false; + + List? get tokens { + if (_tokens != null) return _tokens!.tokens; + + if (_event == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: '_event and _tokens both null', + s: StackTrace.current, + ); + return null; + } + + final Set tokenEvents = + _event!.aggregatedEvents(timeline, PangeaEventTypes.tokens); + + if (tokenEvents.isEmpty) return null; + + if (tokenEvents.length > 1) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: "Token events for representation ${_event!.eventId}: " + "Content: ${tokenEvents.map((e) => e.content).toString()}" + "Type: ${tokenEvents.map((e) => e.type).toString()}", + ), + ); + ErrorHandler.logError( + m: 'should not have more than one tokenEvent per representation ${_event!.eventId}', + s: StackTrace.current, + ); + } + + _tokens = tokenEvents.first.getPangeaContent(); + + return _tokens!.tokens; + } + + Future?> tokensGlobal(BuildContext context) async { + if (tokens != null) return tokens!; + if (_event == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: '_event and _tokens both null', + s: StackTrace.current, + ); + return null; + } + + final Event? tokensEvent = + await MatrixState.pangeaController.messageData.getTokenEvent( + context: context, + repEventId: _event!.eventId, + room: _event!.room, + // Jordan - for just tokens, it's not clear which languages to pass + req: TokensRequestModel( + fullText: text, + userL1: + MatrixState.pangeaController.languageController.userL1?.langCode ?? + LanguageKeys.unknownLanguage, + userL2: langCode, + ), + ); + + if (tokensEvent == null) return null; + + _tokens = TokensEvent(event: tokensEvent).tokens; + + return _tokens?.tokens; + } + + ChoreoRecord? get choreo { + if (_choreo != null) return _choreo; + + if (_event == null) { + // debugger(when: kDebugMode); + ErrorHandler.logError( + m: '_event and _choreo both null', + s: StackTrace.current, + ); + return null; + } + + final Set choreoMatrixEvents = + _event!.aggregatedEvents(timeline, PangeaEventTypes.choreoRecord); + + if (choreoMatrixEvents.isEmpty) return null; + + if (choreoMatrixEvents.length > 1) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: 'should not have more than one choreoEvent per representation ${_event!.eventId}', + s: StackTrace.current, + data: _event!.toJson(), + ); + } + + _choreo = ChoreoEvent(event: choreoMatrixEvents.first).content; + + return _choreo; + } +} diff --git a/lib/pangea/models/pangea_text_tap.dart b/lib/pangea/models/pangea_text_tap.dart new file mode 100644 index 000000000..d409f6e85 --- /dev/null +++ b/lib/pangea/models/pangea_text_tap.dart @@ -0,0 +1,19 @@ +class PTextTapModel { + late int cursorOffset; + late String word; + late bool isHighLighted; + late int textAtOffSet; + PTextTapModel( + {required this.cursorOffset, + required this.isHighLighted, + required this.textAtOffSet, + required this.word}); + toJson() { + return { + 'cursorOffset': cursorOffset, + 'word': word, + 'isHighlighted': isHighLighted, + 'textAtOffSet': textAtOffSet + }; + } +} diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart new file mode 100644 index 000000000..637808d30 --- /dev/null +++ b/lib/pangea/models/pangea_token_model.dart @@ -0,0 +1,91 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../constants/model_keys.dart'; +import '../utils/error_handler.dart'; +import 'lemma.dart'; + +class PangeaToken { + PangeaTokenText text; + bool hasInfo; + List lemmas; + + PangeaToken({ + required this.text, + required this.hasInfo, + required this.lemmas, + }); + + static getLemmas(String text, Iterable? json) { + if (json != null) { + return json + .map( + (e) => Lemma.fromJson(e as Map), + ) + .toList() + .cast(); + } else { + return [Lemma(text: text, saveVocab: false, form: text)]; + } + } + + factory PangeaToken.fromJson(Map json) { + try { + final PangeaTokenText text = + PangeaTokenText.fromJson(json[_textKey] as Map); + return PangeaToken( + text: text, + hasInfo: json[_hasInfoKey] ?? text.length > 2, + lemmas: getLemmas(text.content, json[_lemmaKey]), + ); + } catch (err, s) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: "PangeaToken.fromJson error", + data: { + "json": json, + }, + ), + ); + ErrorHandler.logError(e: err, s: s); + rethrow; + } + } + + static const String _textKey = "text"; + static const String _hasInfoKey = "has_info"; + static const String _lemmaKey = ModelKey.lemma; + + Map toJson() => { + _textKey: text, + _hasInfoKey: hasInfo, + _lemmaKey: lemmas.map((e) => e.toJson()).toList(), + }; +} + +class PangeaTokenText { + int offset; + String content; + int length; + + PangeaTokenText( + {required this.offset, required this.content, required this.length}); + + factory PangeaTokenText.fromJson(Map json) { + debugger(when: kDebugMode && json[_offsetKey] == null); + return PangeaTokenText( + offset: json[_offsetKey], + content: json[_contentKey], + length: json[_lengthKey] ?? (json[_contentKey] as String).length); + } + + static const String _offsetKey = "offset"; + static const String _contentKey = "content"; + static const String _lengthKey = "length"; + + Map toJson() => + {_offsetKey: offset, _contentKey: content, _lengthKey: length}; +} diff --git a/lib/pangea/models/pangea_tokens_event.dart b/lib/pangea/models/pangea_tokens_event.dart new file mode 100644 index 000000000..8d38e9950 --- /dev/null +++ b/lib/pangea/models/pangea_tokens_event.dart @@ -0,0 +1,31 @@ +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; +import 'message_data_models.dart'; + +class TokensEvent { + Event event; + PangeaMessageTokens? _content; + + TokensEvent({required this.event}) { + if (event.type != PangeaEventTypes.tokens) { + throw Exception( + "${event.type} should not be used to make a TokensEvent", + ); + } + } + + PangeaMessageTokens? get _pangeaMessageTokens { + try { + _content ??= event.getPangeaContent(); + return _content!; + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); + return null; + } + } + + PangeaMessageTokens? get tokens => _pangeaMessageTokens; +} diff --git a/lib/pangea/models/removed_translation.dart b/lib/pangea/models/removed_translation.dart new file mode 100644 index 000000000..6887476fe --- /dev/null +++ b/lib/pangea/models/removed_translation.dart @@ -0,0 +1,6 @@ +import 'it_response_model.dart'; + +class RemovedTranslation { + List lastSelectedContinuance = []; + Continuance? removedContinuance; +} diff --git a/lib/pangea/models/span_card_model.dart b/lib/pangea/models/span_card_model.dart new file mode 100644 index 000000000..8a252e67b --- /dev/null +++ b/lib/pangea/models/span_card_model.dart @@ -0,0 +1,26 @@ +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; + +class SpanCardModel { + // IGCTextData igcTextData; + int matchIndex; + Future Function({required int matchIndex, required int choiceIndex}) + onReplacementSelect; + Future Function(String) onSentenceRewrite; + void Function() onIgnore; + void Function() onITStart; + Choreographer choreographer; + + SpanCardModel({ + // required this.igcTextData, + required this.matchIndex, + required this.onReplacementSelect, + required this.onSentenceRewrite, + required this.onIgnore, + required this.onITStart, + required this.choreographer, + }); + + PangeaMatch? get pangeaMatch => + choreographer.igc.igcTextData?.matches[matchIndex]; +} diff --git a/lib/pangea/models/span_data.dart b/lib/pangea/models/span_data.dart new file mode 100644 index 000000000..9c68ef071 --- /dev/null +++ b/lib/pangea/models/span_data.dart @@ -0,0 +1,183 @@ +//Possible actions/effects from cards +// Nothing +// useType of viewed definitions +// SpanChoice of text in message from options +// Call to server for additional/followup info + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +import '../enum/span_choice_type.dart'; +import '../enum/span_data_type.dart'; + +class SpanData { + SpanData({ + required this.message, + required this.shortMessage, + required this.choices, + required this.offset, + required this.length, + required this.context, + required this.fullText, + required this.type, + required this.rule, + }); + + factory SpanData.fromJson(Map json) { + final Iterable? choices = json['choices'] ?? json['replacements']; + return SpanData( + message: json['message'], + shortMessage: json['shortMessage'] ?? json['short_message'], + choices: choices + ?.map( + (e) => SpanChoice.fromJson(e as Map), + ) + .toList(), + offset: json['offset'] as int, + length: json['length'] as int, + context: + json['context'] != null ? Context.fromJson(json['context']) : null, + fullText: + json['sentence'] ?? json['full_text'] ?? json['fullText'] as String, + type: SpanDataType.fromJson(json['type'] as Map), + rule: json['rule'] != null + ? Rule.fromJson(json['rule'] as Map) + : null, + ); + } + + String? message; + String? shortMessage; + List? choices; + int offset; + int length; + Context? context; + String fullText; + SpanDataType type; + Rule? rule; + + Map toJson() => { + 'message': message, + 'short_message': shortMessage, + 'choices': choices != null + ? List.from(choices!.map((x) => x.toJson())) + : null, + 'offset': offset, + 'length': length, + 'context': context?.toJson(), + 'full_text': fullText, + 'type': type.toJson(), + 'rule': rule?.toJson(), + }; +} + +class Context { + Context({ + required this.text, + required this.offset, + required this.length, + }); + + factory Context.fromJson(Map json) { + return Context( + text: json['text'] as String, + offset: json['offset'] as int, + length: json['length'] as int, + ); + } + + /// full text of the message + String text; + int offset; + int length; + + Map toJson() => { + 'text': text, + 'offset': offset, + 'length': length, + }; +} + +class SpanChoice { + SpanChoice({ + required this.value, + required this.type, + this.feedback, + this.selected = false, + }); + factory SpanChoice.fromJson(Map json) { + return SpanChoice( + value: json['value'] as String, + type: json['type'] != null + ? SpanChoiceType.values.firstWhereOrNull( + (element) => element.name == json['type']) ?? + SpanChoiceType.bestCorrection + : SpanChoiceType.bestCorrection, + feedback: json['feedback'], + selected: json['selected'] ?? false, + ); + } + + String value; + SpanChoiceType type; + bool selected; + String? feedback; + + Map toJson() => { + 'value': value, + 'type': type.name, + 'selected': selected, + 'feedback': feedback, + }; + + String feedbackToDisplay(BuildContext context) { + if (feedback == null) { + return type.defaultFeedback(context); + } + return feedback!; + } + + bool get isDistractor => type == SpanChoiceType.distractor; + + bool get isBestCorrection => type == SpanChoiceType.bestCorrection; + + Color get color => type.color; +} + +class Rule { + Rule({ + required this.id, + }); + factory Rule.fromJson(Map json) => Rule( + id: json['id'] as String, + ); + + String id; + + Map toJson() => { + 'id': id, + }; +} + +class SpanDataType { + SpanDataType({ + required this.typeName, + }); + + factory SpanDataType.fromJson(Map json) { + final String? type = + json['typeName'] ?? json['type'] ?? json['type_name'] as String?; + return SpanDataType( + typeName: type != null + ? SpanDataTypeEnum.values + .firstWhereOrNull((element) => element.name == type) ?? + SpanDataTypeEnum.correction + : SpanDataTypeEnum.correction, + ); + } + SpanDataTypeEnum typeName; + + Map toJson() => { + 'type_name': typeName.name, + }; +} diff --git a/lib/pangea/models/student_analytics_event.dart b/lib/pangea/models/student_analytics_event.dart new file mode 100644 index 000000000..f98412a92 --- /dev/null +++ b/lib/pangea/models/student_analytics_event.dart @@ -0,0 +1,129 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; +import 'chart_analytics_model.dart'; + +class StudentAnalyticsEvent { + late Event _event; + StudentAnalyticsSummary? _contentCache; + List _messagesToSave = []; + + StudentAnalyticsEvent({required Event event}) { + if (event.type != PangeaEventTypes.studentAnalyticsSummary) { + throw Exception( + "${event.type} should not be used to make a StudentAnalyticsEvent", + ); + } + _event = event; + if (!classRoom.isSpace) { + throw Exception( + "non-class room should not be used to make a StudentAnalyticsEvent", + ); + } + _event = event; + + _messagesToSave = []; + } + + Room get classRoom => _event.room; + + Event get event => _event; + + StudentAnalyticsSummary get content { + _contentCache ??= StudentAnalyticsSummary.fromJson(event.content); + return _contentCache!; + } + + Future handleNewMessage(RecentMessageRecord message) async { + debugPrint("handle new message"); + if (classRoom.client.userID != _event.stateKey) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "should not be in handleNewMessage ${classRoom.client.userID} != ${_event.stateKey}", + ); + return; + } + _addMessage(message); + + if (DateTime.now().difference(content.lastUpdated).inMinutes > + ClassDefaultValues.minutesDelayToUpdateMyAnalytics) { + _updateStudentAnalytics(); + } + } + + Future bulkUpdate(List messages) async { + if (classRoom.client.userID != _event.stateKey) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "should not be in bulkUpdate ${classRoom.client.userID} != ${_event.stateKey}", + ); + return; + } + _messagesToSave.addAll(messages); + _updateStudentAnalytics(); + } + + Future _updateStudentAnalytics() async { + content.lastUpdated = DateTime.now(); + content.addAll(_messagesToSave); + debugPrint("updating student analytics"); + _clearMessages(); + await classRoom.client.setRoomStateWithKey( + classRoom.id, + _event.type, + _event.stateKey!, + content.toJson(), + ); + } + + _addMessage(RecentMessageRecord message) { + if (_messagesToSave.every((e) => e.eventId != message.eventId)) { + _messagesToSave.add(message); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "adding message twice in StudentAnalyticsEvent._addMessage", + ); + } + //PTODO - save to local storagge + } + + _clearMessages() { + _messagesToSave.clear(); + //PTODO - clear local storagge + } + + Future getTotals(String? chatId) async { + final TimeSeriesTotals totals = TimeSeriesTotals.empty; + final msgs = chatId == null + ? content.messages + : content.messages.where((msg) => msg.chatId == chatId); + for (final msg in msgs) { + totals.increment(msg); + } + return totals; + } + + Future getTimeServiesInterval( + DateTime start, DateTime end, String? chatId) async { + final TimeSeriesInterval interval = TimeSeriesInterval( + start: start, + end: end, + totals: TimeSeriesTotals.empty, + ); + for (final msg in content.messages) { + if (msg.time.isAfter(start) && + msg.time.isBefore(end) && + (chatId == null || chatId == msg.chatId)) { + interval.totals.increment(msg); + } + } + return interval; + } +} diff --git a/lib/pangea/models/student_analytics_event_old.dart b/lib/pangea/models/student_analytics_event_old.dart new file mode 100644 index 000000000..d2696eb01 --- /dev/null +++ b/lib/pangea/models/student_analytics_event_old.dart @@ -0,0 +1,51 @@ +// import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +// import 'package:fluffychat/pangea/models/analytics_model_older.dart'; +// import 'package:matrix/matrix.dart'; + +// import '../constants/pangea_event_types.dart'; + +// class StudentAnalyticsEvent { +// late Event _event; +// StudentAnalyticsSummary? _contentCache; + +// StudentAnalyticsEvent({required Event event}) { +// if (event.type != PangeaEventTypes.studentAnalyticsSummary) { +// throw Exception( +// "${event.type} should not be used to make a StudentAnalyticsEvent", +// ); +// } +// _event = event; +// } + +// Event get event => _event; + +// StudentAnalyticsSummary get _content { +// _contentCache ??= event.getPangeaContent(); +// return _contentCache!; +// } + +// List get monthly => _content.monthlyTotalsForAllTime; +// List get daily => _content.dailyTotalsForLast30Days; +// List get hourly => _content.hourlyTotalsForLast24Hours; + +// // updateLocal +// // updateServer +// handleNewMessage() {} + +// /// if monthly.isNotEmpty && last.end.month < now.month +// /// push empty intervals until last.end.month >= now.month +// /// if daily.isEmpty +// /// push empty intervals until last.end.day >= now.day +// /// else if daily.where(e => e.month < now.month) +// /// sum and add to monthly +// /// +// /// if hourly.isEmpty || last.end.hour < now.hour +// /// push empty intervals until last.end.hour >= now.hour +// /// increment hourly + +// updateLocal() {} + +// // if server copy is older than x, push local version +// // get new server copy, local version = server copy +// updateServer() {} +// } diff --git a/lib/pangea/models/student_analytics_summary_model.dart b/lib/pangea/models/student_analytics_summary_model.dart new file mode 100644 index 000000000..f0fa2642f --- /dev/null +++ b/lib/pangea/models/student_analytics_summary_model.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; + +import '../enum/use_type.dart'; + +class RecentMessageRecord { + String eventId; + String chatId; + UseType useType; + DateTime time; + + RecentMessageRecord({ + required this.eventId, + required this.chatId, + required this.useType, + required this.time, + }); + + factory RecentMessageRecord.fromJson(Map json) => + RecentMessageRecord( + eventId: json[_eventIdKey], + chatId: json[_chatIdKey], + useType: _typeStringToEnum(json[_typeOfUseKey]), + time: DateTime.parse(json[_timeKey]), + ); + + Map toJson() => { + _eventIdKey: eventId, + _chatIdKey: chatId, + _typeOfUseKey: _typeEnumToString(useType), + _timeKey: time.toIso8601String(), + }; + + String _typeEnumToString(dynamic status) => status.toString().split('.').last; + + static UseType _typeStringToEnum(String useType) { + final String lastPart = useType.toString().split('.').last; + switch (lastPart) { + case 'ta': + return UseType.ta; + case 'ga': + return UseType.ga; + case 'wa': + return UseType.wa; + default: + return UseType.un; + } + } + + static const _eventIdKey = "m.id"; + static const _chatIdKey = "c.id"; + static const _typeOfUseKey = "typ"; + static const _timeKey = "t"; +} + +class StudentAnalyticsSummary { + late List _messages; + DateTime lastUpdated; + + StudentAnalyticsSummary({ + required List messages, + required this.lastUpdated, + }) { + _messages = messages; + } + + void addAll(List msgs) { + for (final msg in msgs) { + if (_messages.any((element) => element.eventId == msg.eventId)) { + ErrorHandler.logError( + m: "adding message twice in StudentAnalyticsSummary.add", + ); + } else { + _messages.add(msg); + } + } + } + + List get messages => _messages; + + static const _messagesKey = "msgs"; + static const _lastUpdatedKey = "lupt"; + + Map toJson() => { + _messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()), + _lastUpdatedKey: lastUpdated.toIso8601String() + }; + + factory StudentAnalyticsSummary.fromJson(json) { + List savedMessages = []; + try { + savedMessages = json[_messagesKey] != null + ? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable) + .map((e) => RecentMessageRecord.fromJson(e)) + .toList() + .cast() + : []; + } catch (err, stack) { + if (kDebugMode) rethrow; + ErrorHandler.logError(e: err, s: stack); + } + return StudentAnalyticsSummary( + messages: savedMessages, + lastUpdated: DateTime.parse(json[_lastUpdatedKey]), + ); + } +} diff --git a/lib/pangea/models/system_choice_translation_model.dart b/lib/pangea/models/system_choice_translation_model.dart new file mode 100644 index 000000000..6914044d3 --- /dev/null +++ b/lib/pangea/models/system_choice_translation_model.dart @@ -0,0 +1,45 @@ +import '../constants/model_keys.dart'; + +class SystemChoiceRequestModel { + String translationId; + int? nextWordIndex; + String? customInput; + String userId; + String roomId; + String targetLangCode; + String sourceLangCode; + String? classId; + + SystemChoiceRequestModel({ + required this.translationId, + this.nextWordIndex, + this.customInput, + required this.userId, + required this.roomId, + required this.targetLangCode, + required this.sourceLangCode, + this.classId, + }); + + toJson() => { + 'translation_id': translationId, + 'next_word_index': nextWordIndex, + 'custom_input': customInput, + 'user_id': userId, + 'room_id': roomId, + ModelKey.tgtLang: targetLangCode, + ModelKey.srcLang: sourceLangCode, + 'class_id': classId, + }; + + factory SystemChoiceRequestModel.fromJson(json) => SystemChoiceRequestModel( + translationId: json['translation_id'], + nextWordIndex: json['next_word_index'], + customInput: json['custom_input'], + userId: json['user_id'], + roomId: json['room_id'], + targetLangCode: json[ModelKey.tgtLang], + sourceLangCode: json[ModelKey.srcLang], + classId: json['class_id'], + ); +} diff --git a/lib/pangea/models/textChangeModel.dart b/lib/pangea/models/textChangeModel.dart new file mode 100644 index 000000000..38a988df8 --- /dev/null +++ b/lib/pangea/models/textChangeModel.dart @@ -0,0 +1,11 @@ +import '../enum/direction.dart'; +import '../enum/edit_type.dart'; + +class TextChangeModel { + EditType? editType; + EditDirection? editDirection; + String? text; + + toJson() => + {'editType': editType, 'editDirection': editDirection, 'text': text}; +} diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart new file mode 100644 index 000000000..7394cbc09 --- /dev/null +++ b/lib/pangea/models/user_model.dart @@ -0,0 +1,606 @@ +import 'dart:convert'; + +import 'package:country_picker/country_picker.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../constants/language_keys.dart'; +import 'language_model.dart'; + +PUserModel pUserModelFromJson(String str) => + PUserModel.fromJson(json.decode(str)); + +String pUserModelToJson(PUserModel data) => json.encode(data.toJson()); + +class PUserModel { + String access; + String refresh; + Profile? profile; + + PUserModel({required this.access, required this.refresh, this.profile}); + + factory PUserModel.fromJson(Map json) => PUserModel( + access: json[ModelKey.userAccess], + refresh: json[ModelKey.userRefresh], + profile: json[ModelKey.userProfile] != null + ? Profile.fromJson(json[ModelKey.userProfile]) + : null, + ); + + Map toJson() { + final Map data = {}; + data[ModelKey.userAccess] = access; + data[ModelKey.userRefresh] = refresh; + if (profile != null) { + data[ModelKey.userProfile] = profile!.toJson(); + } + return data; + } +} + +class Profile { + // i'm considering removing this field because it's duplicating info in the + // matrix database + // String? fullName; + final String createdAt; + final String pangeaUserId; + String? dateOfBirth; + String? targetLanguage; + String? sourceLanguage; + + String? country; + bool publicProfile; + + Profile({ + // this.fullName, + required this.createdAt, + required this.pangeaUserId, + this.dateOfBirth, + this.targetLanguage, + this.sourceLanguage, + this.country, + this.publicProfile = false, + }); + + factory Profile.fromJson(Map json) { + final l2 = LanguageModel.codeFromNameOrCode( + json[ModelKey.l2LanguageKey] ?? LanguageKeys.unknownLanguage); + final l1 = LanguageModel.codeFromNameOrCode( + json[ModelKey.l1LanguageKey] ?? LanguageKeys.unknownLanguage); + + return Profile( + // fullName: json[ModelKey.userFullName], + createdAt: json[ModelKey.userCreatedAt], + pangeaUserId: json[ModelKey.userPangeaUserId], + dateOfBirth: json[ModelKey.userDateOfBirth], + targetLanguage: l2, + sourceLanguage: l1, + publicProfile: json[ModelKey.publicProfile] ?? false, + country: json[ModelKey.userCountry], + ); + } + + Map toJson() { + final Map data = {}; + // data[ModelKey.userFullName] = fullName; + data[ModelKey.userCreatedAt] = createdAt; + data[ModelKey.userPangeaUserId] = pangeaUserId; + data[ModelKey.userDateOfBirth] = dateOfBirth; + data[ModelKey.l2LanguageKey] = targetLanguage; + data[ModelKey.l1LanguageKey] = sourceLanguage; + data[ModelKey.publicProfile] = publicProfile; + data[ModelKey.userCountry] = country; + return data; + } + + /// used in find a partner page for display partner's country + String get flagEmoji { + final String? countryName = this.country?.split(' (')[0]; + final Country? country = CountryService().findByName(countryName); + return country?.flagEmoji ?? ""; + } + + String? countryDisplayName(BuildContext context) { + final String? countryName = this.country?.split(' (')[0]; + final Country? country = CountryService().findByName(countryName); + if (country?.countryCode == null) return null; + switch (country!.countryCode) { + case 'WW': + return L10n.of(context)!.wwCountryDisplayName; + case 'AF': + return L10n.of(context)!.afCountryDisplayName; + case 'AX': + return L10n.of(context)!.axCountryDisplayName; + case 'AL': + return L10n.of(context)!.alCountryDisplayName; + case 'DZ': + return L10n.of(context)!.dzCountryDisplayName; + case 'AS': + return L10n.of(context)!.asCountryDisplayName; + case 'AD': + return L10n.of(context)!.adCountryDisplayName; + case 'AO': + return L10n.of(context)!.aoCountryDisplayName; + case 'AI': + return L10n.of(context)!.aiCountryDisplayName; + case 'AG': + return L10n.of(context)!.agCountryDisplayName; + case 'AR': + return L10n.of(context)!.arCountryDisplayName; + case 'AM': + return L10n.of(context)!.amCountryDisplayName; + case 'AW': + return L10n.of(context)!.awCountryDisplayName; + case 'AC': + return L10n.of(context)!.acCountryDisplayName; + case 'AU': + return L10n.of(context)!.auCountryDisplayName; + case 'AT': + return L10n.of(context)!.atCountryDisplayName; + case 'AZ': + return L10n.of(context)!.azCountryDisplayName; + case 'BS': + return L10n.of(context)!.bsCountryDisplayName; + case 'BH': + return L10n.of(context)!.bhCountryDisplayName; + case 'BD': + return L10n.of(context)!.bdCountryDisplayName; + case 'BB': + return L10n.of(context)!.bbCountryDisplayName; + case 'BY': + return L10n.of(context)!.byCountryDisplayName; + case 'BE': + return L10n.of(context)!.beCountryDisplayName; + case 'BZ': + return L10n.of(context)!.bzCountryDisplayName; + case 'BJ': + return L10n.of(context)!.bjCountryDisplayName; + case 'BM': + return L10n.of(context)!.bmCountryDisplayName; + case 'BT': + return L10n.of(context)!.btCountryDisplayName; + case 'BO': + return L10n.of(context)!.boCountryDisplayName; + case 'BA': + return L10n.of(context)!.baCountryDisplayName; + case 'BW': + return L10n.of(context)!.bwCountryDisplayName; + case 'BR': + return L10n.of(context)!.brCountryDisplayName; + case 'IO': + return L10n.of(context)!.ioCountryDisplayName; + case 'VG': + return L10n.of(context)!.vgCountryDisplayName; + case 'BN': + return L10n.of(context)!.bnCountryDisplayName; + case 'BG': + return L10n.of(context)!.bgCountryDisplayName; + case 'BF': + return L10n.of(context)!.bfCountryDisplayName; + case 'BI': + return L10n.of(context)!.biCountryDisplayName; + case 'KH': + return L10n.of(context)!.khCountryDisplayName; + case 'CM': + return L10n.of(context)!.cmCountryDisplayName; + case 'CA': + return L10n.of(context)!.caCountryDisplayName; + case 'CV': + return L10n.of(context)!.cvCountryDisplayName; + case 'BQ': + return L10n.of(context)!.bqCountryDisplayName; + case 'KY': + return L10n.of(context)!.kyCountryDisplayName; + case 'CF': + return L10n.of(context)!.cfCountryDisplayName; + case 'TD': + return L10n.of(context)!.tdCountryDisplayName; + case 'CL': + return L10n.of(context)!.clCountryDisplayName; + case 'CN': + return L10n.of(context)!.cnCountryDisplayName; + case 'CX': + return L10n.of(context)!.cxCountryDisplayName; + case 'CC': + return L10n.of(context)!.ccCountryDisplayName; + case 'CO': + return L10n.of(context)!.coCountryDisplayName; + case 'KM': + return L10n.of(context)!.kmCountryDisplayName; + case 'CD': + return L10n.of(context)!.cdCountryDisplayName; + case 'CG': + return L10n.of(context)!.cgCountryDisplayName; + case 'CK': + return L10n.of(context)!.ckCountryDisplayName; + case 'CR': + return L10n.of(context)!.crCountryDisplayName; + case 'CI': + return L10n.of(context)!.ciCountryDisplayName; + case 'HR': + return L10n.of(context)!.hrCountryDisplayName; + case 'CU': + return L10n.of(context)!.cuCountryDisplayName; + case 'CW': + return L10n.of(context)!.cwCountryDisplayName; + case 'CY': + return L10n.of(context)!.cyCountryDisplayName; + case 'CZ': + return L10n.of(context)!.czCountryDisplayName; + case 'DK': + return L10n.of(context)!.dkCountryDisplayName; + case 'DJ': + return L10n.of(context)!.djCountryDisplayName; + case 'DM': + return L10n.of(context)!.dmCountryDisplayName; + case 'DO': + return L10n.of(context)!.doCountryDisplayName; + case 'TL': + return L10n.of(context)!.tlCountryDisplayName; + case 'EC': + return L10n.of(context)!.ecCountryDisplayName; + case 'EG': + return L10n.of(context)!.egCountryDisplayName; + case 'SV': + return L10n.of(context)!.svCountryDisplayName; + case 'GQ': + return L10n.of(context)!.gqCountryDisplayName; + case 'ER': + return L10n.of(context)!.erCountryDisplayName; + case 'EE': + return L10n.of(context)!.eeCountryDisplayName; + case 'SZ': + return L10n.of(context)!.szCountryDisplayName; + case 'ET': + return L10n.of(context)!.etCountryDisplayName; + case 'FK': + return L10n.of(context)!.fkCountryDisplayName; + case 'FO': + return L10n.of(context)!.foCountryDisplayName; + case 'FJ': + return L10n.of(context)!.fjCountryDisplayName; + case 'FI': + return L10n.of(context)!.fiCountryDisplayName; + case 'FR': + return L10n.of(context)!.frCountryDisplayName; + case 'GF': + return L10n.of(context)!.gfCountryDisplayName; + case 'PF': + return L10n.of(context)!.pfCountryDisplayName; + case 'GA': + return L10n.of(context)!.gaCountryDisplayName; + case 'GM': + return L10n.of(context)!.gmCountryDisplayName; + case 'GE': + return L10n.of(context)!.geCountryDisplayName; + case 'DE': + return L10n.of(context)!.deCountryDisplayName; + case 'GH': + return L10n.of(context)!.ghCountryDisplayName; + case 'GI': + return L10n.of(context)!.giCountryDisplayName; + case 'GR': + return L10n.of(context)!.grCountryDisplayName; + case 'GL': + return L10n.of(context)!.glCountryDisplayName; + case 'GD': + return L10n.of(context)!.gdCountryDisplayName; + case 'GP': + return L10n.of(context)!.gpCountryDisplayName; + case 'GU': + return L10n.of(context)!.guCountryDisplayName; + case 'GT': + return L10n.of(context)!.gtCountryDisplayName; + case 'GG': + return L10n.of(context)!.ggCountryDisplayName; + case 'GN': + return L10n.of(context)!.gnCountryDisplayName; + case 'GW': + return L10n.of(context)!.gwCountryDisplayName; + case 'GY': + return L10n.of(context)!.gyCountryDisplayName; + case 'HT': + return L10n.of(context)!.htCountryDisplayName; + case 'HM': + return L10n.of(context)!.hmCountryDisplayName; + case 'HN': + return L10n.of(context)!.hnCountryDisplayName; + case 'HK': + return L10n.of(context)!.hkCountryDisplayName; + case 'HU': + return L10n.of(context)!.huCountryDisplayName; + case 'IS': + return L10n.of(context)!.isCountryDisplayName; + case 'IN': + return L10n.of(context)!.inCountryDisplayName; + case 'ID': + return L10n.of(context)!.idCountryDisplayName; + case 'IR': + return L10n.of(context)!.irCountryDisplayName; + case 'IQ': + return L10n.of(context)!.iqCountryDisplayName; + case 'IE': + return L10n.of(context)!.ieCountryDisplayName; + case 'IM': + return L10n.of(context)!.imCountryDisplayName; + case 'IL': + return L10n.of(context)!.ilCountryDisplayName; + case 'IT': + return L10n.of(context)!.itCountryDisplayName; + case 'JM': + return L10n.of(context)!.jmCountryDisplayName; + case 'JP': + return L10n.of(context)!.jpCountryDisplayName; + case 'JE': + return L10n.of(context)!.jeCountryDisplayName; + case 'JO': + return L10n.of(context)!.joCountryDisplayName; + case 'KZ': + return L10n.of(context)!.kzCountryDisplayName; + case 'KE': + return L10n.of(context)!.keCountryDisplayName; + case 'KI': + return L10n.of(context)!.kiCountryDisplayName; + case 'XK': + return L10n.of(context)!.xkCountryDisplayName; + case 'KW': + return L10n.of(context)!.kwCountryDisplayName; + case 'KG': + return L10n.of(context)!.kgCountryDisplayName; + case 'LA': + return L10n.of(context)!.laCountryDisplayName; + case 'LV': + return L10n.of(context)!.lvCountryDisplayName; + case 'LB': + return L10n.of(context)!.lbCountryDisplayName; + case 'LS': + return L10n.of(context)!.lsCountryDisplayName; + case 'LR': + return L10n.of(context)!.lrCountryDisplayName; + case 'LY': + return L10n.of(context)!.lyCountryDisplayName; + case 'LI': + return L10n.of(context)!.liCountryDisplayName; + case 'LT': + return L10n.of(context)!.ltCountryDisplayName; + case 'LU': + return L10n.of(context)!.luCountryDisplayName; + case 'MO': + return L10n.of(context)!.moCountryDisplayName; + case 'MK': + return L10n.of(context)!.mkCountryDisplayName; + case 'MG': + return L10n.of(context)!.mgCountryDisplayName; + case 'MW': + return L10n.of(context)!.mwCountryDisplayName; + case 'MY': + return L10n.of(context)!.myCountryDisplayName; + case 'MV': + return L10n.of(context)!.mvCountryDisplayName; + case 'ML': + return L10n.of(context)!.mlCountryDisplayName; + case 'MT': + return L10n.of(context)!.mtCountryDisplayName; + case 'MH': + return L10n.of(context)!.mhCountryDisplayName; + case 'MQ': + return L10n.of(context)!.mqCountryDisplayName; + case 'MR': + return L10n.of(context)!.mrCountryDisplayName; + case 'MU': + return L10n.of(context)!.muCountryDisplayName; + case 'YT': + return L10n.of(context)!.ytCountryDisplayName; + case 'MX': + return L10n.of(context)!.mxCountryDisplayName; + case 'FM': + return L10n.of(context)!.fmCountryDisplayName; + case 'MD': + return L10n.of(context)!.mdCountryDisplayName; + case 'MC': + return L10n.of(context)!.mcCountryDisplayName; + case 'MN': + return L10n.of(context)!.mnCountryDisplayName; + case 'ME': + return L10n.of(context)!.meCountryDisplayName; + case 'MS': + return L10n.of(context)!.msCountryDisplayName; + case 'MA': + return L10n.of(context)!.maCountryDisplayName; + case 'MZ': + return L10n.of(context)!.mzCountryDisplayName; + case 'MM': + return L10n.of(context)!.mmCountryDisplayName; + case 'NA': + return L10n.of(context)!.naCountryDisplayName; + case 'NR': + return L10n.of(context)!.nrCountryDisplayName; + case 'NP': + return L10n.of(context)!.npCountryDisplayName; + case 'NL': + return L10n.of(context)!.nlCountryDisplayName; + case 'NC': + return L10n.of(context)!.ncCountryDisplayName; + case 'NZ': + return L10n.of(context)!.nzCountryDisplayName; + case 'NI': + return L10n.of(context)!.niCountryDisplayName; + case 'NE': + return L10n.of(context)!.neCountryDisplayName; + case 'NG': + return L10n.of(context)!.ngCountryDisplayName; + case 'NU': + return L10n.of(context)!.nuCountryDisplayName; + case 'NF': + return L10n.of(context)!.nfCountryDisplayName; + case 'KP': + return L10n.of(context)!.kpCountryDisplayName; + case 'MP': + return L10n.of(context)!.mpCountryDisplayName; + case 'NO': + return L10n.of(context)!.noCountryDisplayName; + case 'OM': + return L10n.of(context)!.omCountryDisplayName; + case 'PK': + return L10n.of(context)!.pkCountryDisplayName; + case 'PW': + return L10n.of(context)!.pwCountryDisplayName; + case 'PS': + return L10n.of(context)!.psCountryDisplayName; + case 'PA': + return L10n.of(context)!.paCountryDisplayName; + case 'PG': + return L10n.of(context)!.pgCountryDisplayName; + case 'PY': + return L10n.of(context)!.pyCountryDisplayName; + case 'PE': + return L10n.of(context)!.peCountryDisplayName; + case 'PH': + return L10n.of(context)!.phCountryDisplayName; + case 'PL': + return L10n.of(context)!.plCountryDisplayName; + case 'PT': + return L10n.of(context)!.ptCountryDisplayName; + case 'PR': + return L10n.of(context)!.prCountryDisplayName; + case 'QA': + return L10n.of(context)!.qaCountryDisplayName; + case 'RE': + return L10n.of(context)!.reCountryDisplayName; + case 'RO': + return L10n.of(context)!.roCountryDisplayName; + case 'RU': + return L10n.of(context)!.ruCountryDisplayName; + case 'RW': + return L10n.of(context)!.rwCountryDisplayName; + case 'BL': + return L10n.of(context)!.blCountryDisplayName; + case 'SH': + return L10n.of(context)!.shCountryDisplayName; + case 'KN': + return L10n.of(context)!.knCountryDisplayName; + case 'LC': + return L10n.of(context)!.lcCountryDisplayName; + case 'MF': + return L10n.of(context)!.mfCountryDisplayName; + case 'PM': + return L10n.of(context)!.pmCountryDisplayName; + case 'VC': + return L10n.of(context)!.vcCountryDisplayName; + case 'WS': + return L10n.of(context)!.wsCountryDisplayName; + case 'SM': + return L10n.of(context)!.smCountryDisplayName; + case 'ST': + return L10n.of(context)!.stCountryDisplayName; + case 'SA': + return L10n.of(context)!.saCountryDisplayName; + case 'SN': + return L10n.of(context)!.snCountryDisplayName; + case 'RS': + return L10n.of(context)!.rsCountryDisplayName; + case 'SC': + return L10n.of(context)!.scCountryDisplayName; + case 'SL': + return L10n.of(context)!.slCountryDisplayName; + case 'SG': + return L10n.of(context)!.sgCountryDisplayName; + case 'SX': + return L10n.of(context)!.sxCountryDisplayName; + case 'SK': + return L10n.of(context)!.skCountryDisplayName; + case 'SI': + return L10n.of(context)!.siCountryDisplayName; + case 'SB': + return L10n.of(context)!.sbCountryDisplayName; + case 'SO': + return L10n.of(context)!.soCountryDisplayName; + case 'ZA': + return L10n.of(context)!.zaCountryDisplayName; + case 'GS': + return L10n.of(context)!.gsCountryDisplayName; + case 'KR': + return L10n.of(context)!.krCountryDisplayName; + case 'SS': + return L10n.of(context)!.ssCountryDisplayName; + case 'ES': + return L10n.of(context)!.esCountryDisplayName; + case 'LK': + return L10n.of(context)!.lkCountryDisplayName; + case 'SD': + return L10n.of(context)!.sdCountryDisplayName; + case 'SR': + return L10n.of(context)!.srCountryDisplayName; + case 'SJ': + return L10n.of(context)!.sjCountryDisplayName; + case 'SE': + return L10n.of(context)!.seCountryDisplayName; + case 'CH': + return L10n.of(context)!.chCountryDisplayName; + case 'SY': + return L10n.of(context)!.syCountryDisplayName; + case 'TW': + return L10n.of(context)!.twCountryDisplayName; + case 'TJ': + return L10n.of(context)!.tjCountryDisplayName; + case 'TZ': + return L10n.of(context)!.tzCountryDisplayName; + case 'TH': + return L10n.of(context)!.thCountryDisplayName; + case 'TG': + return L10n.of(context)!.tgCountryDisplayName; + case 'TK': + return L10n.of(context)!.tkCountryDisplayName; + case 'TO': + return L10n.of(context)!.toCountryDisplayName; + case 'TT': + return L10n.of(context)!.ttCountryDisplayName; + case 'TN': + return L10n.of(context)!.tnCountryDisplayName; + case 'TR': + return L10n.of(context)!.trCountryDisplayName; + case 'TM': + return L10n.of(context)!.tmCountryDisplayName; + case 'TC': + return L10n.of(context)!.tcCountryDisplayName; + case 'TV': + return L10n.of(context)!.tvCountryDisplayName; + case 'VI': + return L10n.of(context)!.viCountryDisplayName; + case 'UG': + return L10n.of(context)!.ugCountryDisplayName; + case 'UA': + return L10n.of(context)!.uaCountryDisplayName; + case 'AE': + return L10n.of(context)!.aeCountryDisplayName; + case 'GB': + return L10n.of(context)!.gbCountryDisplayName; + case 'US': + return L10n.of(context)!.usCountryDisplayName; + case 'UY': + return L10n.of(context)!.uyCountryDisplayName; + case 'UZ': + return L10n.of(context)!.uzCountryDisplayName; + case 'VU': + return L10n.of(context)!.vuCountryDisplayName; + case 'VA': + return L10n.of(context)!.vaCountryDisplayName; + case 'VE': + return L10n.of(context)!.veCountryDisplayName; + case 'VN': + return L10n.of(context)!.vnCountryDisplayName; + case 'WF': + return L10n.of(context)!.wfCountryDisplayName; + case 'EH': + return L10n.of(context)!.ehCountryDisplayName; + case 'YE': + return L10n.of(context)!.yeCountryDisplayName; + case 'ZM': + return L10n.of(context)!.zmCountryDisplayName; + case 'ZW': + return L10n.of(context)!.zwCountryDisplayName; + } + return null; + } +} diff --git a/lib/pangea/models/user_profile_search_model.dart b/lib/pangea/models/user_profile_search_model.dart new file mode 100644 index 000000000..97a7e91c1 --- /dev/null +++ b/lib/pangea/models/user_profile_search_model.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'user_model.dart'; + +class UserProfileSearchResponse { + int count; + String? next; + String? previous; + List results; + + UserProfileSearchResponse({ + required this.count, + required this.next, + required this.previous, + required this.results, + }); + + factory UserProfileSearchResponse.fromJson(Map json) { + return UserProfileSearchResponse( + count: json["count"], + next: json["next"], + previous: json["previous"], + results: json["results"] + .map((p) => Profile.fromJson(p)) + .toList() + .cast(), + ); + } +} diff --git a/lib/pangea/models/web_subscriptions.dart b/lib/pangea/models/web_subscriptions.dart new file mode 100644 index 000000000..0b23cd5ae --- /dev/null +++ b/lib/pangea/models/web_subscriptions.dart @@ -0,0 +1,58 @@ +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/models/base_subscription_info.dart'; +import 'package:fluffychat/pangea/repo/subscription_repo.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class WebSubscriptionInfo extends SubscriptionInfo { + WebSubscriptionInfo({required super.pangeaController}) : super(); + + @override + Future configure() async { + await setAppIds(); + await setAllProducts(); + await setCustomerInfo(); + availableSubscriptions = allProducts! + .where((product) => product.appId == appIds!.currentAppId) + .toList(); + availableSubscriptions.sort((a, b) => a.price.compareTo(b.price)); + //@Gabby - temporary solution to add trial to list + if (currentSubscriptionId == null && !hasSubscribed) { + final id = availableSubscriptions[0].id; + final package = availableSubscriptions[0].package; + final duration = availableSubscriptions[0].duration; + availableSubscriptions.insert( + 0, + SubscriptionDetails( + price: 0, + id: id, + duration: duration, + package: package, + periodType: 'trial', + ), + ); + } + } + + @override + Future setCustomerInfo() async { + if (currentSubscriptionId != null && currentSubscription != null) { + return; + } + final RCSubscriptionResponseModel currentSubscriptionInfo = + await SubscriptionRepo.getCurrentSubscriptionInfo( + pangeaController.matrixState.client.userID, + allProducts, + ); + + currentSubscriptionId = currentSubscriptionInfo.currentSubscriptionId; + currentSubscription = currentSubscriptionInfo.currentSubscription; + allEntitlements = currentSubscriptionInfo.allEntitlements ?? []; + expirationDate = currentSubscriptionInfo.expirationDate; + + if (currentSubscriptionId != null && currentSubscription == null) { + Sentry.addBreadcrumb( + Breadcrumb(message: "mismatch of productIds and currentSubscriptionID"), + ); + } + } +} diff --git a/lib/pangea/models/widget_measurement.dart b/lib/pangea/models/widget_measurement.dart new file mode 100644 index 000000000..851abec62 --- /dev/null +++ b/lib/pangea/models/widget_measurement.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class WidgetMeasurements { + static final Map _fromKey = {}; + static dispose() => _fromKey.clear(); + static WidgetMeasurements defaultFromKey(String key) { + if (_fromKey[key] == null) { + _fromKey[key] = WidgetMeasurements( + position: const Offset(0, 0), size: const Size(0, 0), uid: key); + } + + final WidgetMeasurements? weg = _fromKey[key]; + return _fromKey[key]!; + } + + Offset? position; + Size? size; + String? uid; + WidgetMeasurements( + {required this.position, required this.size, required this.uid}); + + toJson() => {'position': position, 'size': size, 'uid': uid}; +} diff --git a/lib/pangea/models/word_data_model.dart b/lib/pangea/models/word_data_model.dart new file mode 100644 index 000000000..d2e03bc69 --- /dev/null +++ b/lib/pangea/models/word_data_model.dart @@ -0,0 +1,200 @@ +import 'dart:developer'; + +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; + final String fullText; + final String? userL1; + final String? userL2; + // final List languageSenses; + + final String targetPartOfSpeech; + final String basePartOfSpeech; + final String partOfSpeech; + final String targetDefinition; + final String baseDefinition; + final String targetWord; + final String baseWord; + final String baseExampleSentence; + final String targetExampleSentence; + + WordData({ + // required this.languageSenses, + required this.fullText, + required this.word, + required this.userL1, + required this.userL2, + required this.baseDefinition, + required this.targetDefinition, + required this.basePartOfSpeech, + required this.targetPartOfSpeech, + required this.partOfSpeech, + required this.baseWord, + required this.targetWord, + required this.baseExampleSentence, + required this.targetExampleSentence, + }); + + // static const String _languageSensesKey = 'sense_responses'; + static const String _dataFullKey = 'data_full'; + + Map toJson() => { + // _languageSensesKey: languageSenses.map((e) => e.toJson()).toList(), + ModelKey.word: word, + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, + ModelKey.baseDefinition: baseDefinition, + ModelKey.targetDefinition: targetDefinition, + ModelKey.basePartOfSpeech: basePartOfSpeech, + ModelKey.targetPartOfSpeech: targetPartOfSpeech, + ModelKey.partOfSpeech: partOfSpeech, + ModelKey.baseWord: baseWord, + ModelKey.targetWord: targetWord, + ModelKey.baseExampleSentence: baseExampleSentence, + ModelKey.targetExampleSentence: targetExampleSentence, + }; + + factory WordData.fromJson( + Map json, { + required String word, + required String fullText, + required String userL1, + required String userL2, + }) { + try { + return WordData( + // languageSenses: (json[_languageSensesKey] as List) + // .map( + // (e) => LanguageSense.fromJson(e as Map), + // ) + // .toList() + // .cast(), + baseDefinition: json[_dataFullKey][ModelKey.baseDefinition], + targetDefinition: json[_dataFullKey][ModelKey.targetDefinition], + basePartOfSpeech: json[_dataFullKey][ModelKey.basePartOfSpeech], + targetPartOfSpeech: json[_dataFullKey][ModelKey.targetPartOfSpeech], + partOfSpeech: json[_dataFullKey][ModelKey.partOfSpeech], + baseWord: json[_dataFullKey][ModelKey.baseWord], + targetWord: json[_dataFullKey][ModelKey.targetWord], + baseExampleSentence: json[_dataFullKey][ModelKey.baseExampleSentence], + targetExampleSentence: json[_dataFullKey] + [ModelKey.targetExampleSentence], + word: word, + userL1: userL1, + userL2: userL2, + fullText: fullText, + ); + } catch (err) { + debugger(when: kDebugMode); + return [] as WordData; + } + } + + bool isMatch({ + required String w, + required String f, + required String? l1, + required String? l2, + }) => + word == w && userL1 == l1 && userL2 == l2 && fullText == f; + + String formattedPartOfSpeech(LanguageType languageType) { + final String pos = languageType == LanguageType.base + ? basePartOfSpeech + : targetPartOfSpeech; + return pos[0].toUpperCase() + pos.substring(1); + } + + // List sensesForLanguage(String code) => + // languageSenses.where((langSense) => langSense.langCode == code).toList(); +} + +// class LanguageSense { +// List senses; +// String langCode; + +// LanguageSense({ +// required this.senses, +// required this.langCode, +// }); + +// static const String _sensesKey = "senses"; +// static const String _langCodeKey = "lang_code"; + +// Map toJson() => { +// _sensesKey: senses.map((e) => e.toJson()).toList(), +// _langCodeKey: langCode, +// }; + +// factory LanguageSense.fromJson(Map json) => LanguageSense( +// senses: (json[_sensesKey] as List) +// .map( +// (e) => Sense.fromJson(e as Map), +// ) +// .toList() +// .cast(), +// langCode: json[_langCodeKey], +// ); + +// List get partsOfSpeech => +// senses.map((sense) => sense.partOfSpeech).toSet().toList(); + +// List definitionsForPartOfSpeech(String partOfSpeech) { +// final List definitions = []; +// for (final Sense sense in senses) { +// if (sense.partOfSpeech == partOfSpeech && +// sense.definition != null && +// sense.definition!.isNotEmpty) { +// definitions.add(sense.definition!); +// } +// } +// return definitions; +// } + +// // List partOfSpeechSense(partOfSpeech) { +// // return senses +// // .where((sense) => sense.partOfSpeech == partOfSpeech) +// // .map((sense) => sense.lemmas.join(', ')) +// // .toSet() +// // .toList(); +// // } + +// // Map> get partOfSpeechSenses { +// // final Map> senses = {}; +// // for (final partOfSpeech in partsOfSpeech) { +// // senses[partOfSpeech] = partOfSpeechSense(partOfSpeech); +// // } +// // return senses; +// // } +// } + +// class Sense { +// String partOfSpeech; +// List lemmas; +// String? definition; + +// Sense({ +// required this.partOfSpeech, +// required this.lemmas, +// required this.definition, +// }); + +// static const String _posKey = "pos"; +// static const String _lemmasKey = "lemmas"; +// static const String _definitionKey = "definition"; + +// Map toJson() => { +// _posKey: partOfSpeech, +// _lemmasKey: lemmas.toString(), +// _definitionKey: definition +// }; + +// factory Sense.fromJson(Map json) => Sense( +// partOfSpeech: json[_posKey], +// lemmas: (json[_lemmasKey] as List).cast(), +// definition: json[_definitionKey], +// ); +// } diff --git a/lib/pangea/network/p_api_exception.dart b/lib/pangea/network/p_api_exception.dart new file mode 100644 index 000000000..536e5d97b --- /dev/null +++ b/lib/pangea/network/p_api_exception.dart @@ -0,0 +1,79 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +import '../utils/p_toast.dart'; + +class ApiException { + static exception({required int statusCode, required String body}) { + switch (statusCode) { + case 400: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg(msg: "Unknown error accrued", success: false); + return; + case 401: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Exception: Unauthorized access", success: false); + + return; + case 403: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Exception: Don't have permissions!", success: false); + return; + case 500: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Exception: Internal Server Error", success: false); + return; + case 502: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Exception: Bad Gateway", success: false); + + return; + case 503: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Exception: Service Unavailable", success: false); + + return; + case 504: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Exception: Gateway timeout error!", success: false); + + return; + default: + if (kDebugMode) { + debugPrint(body); + debugPrint(statusCode.toString()); + } + PToastController.toastMsg( + msg: "Unknown exception accrued!", success: false); + return; + } + } +} diff --git a/lib/pangea/network/requests.dart b/lib/pangea/network/requests.dart new file mode 100644 index 000000000..549474bb5 --- /dev/null +++ b/lib/pangea/network/requests.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class Requests { + late String? baseUrl; + late String? accessToken; + late String? matrixAccessToken; + late String? choreoApiKey; + //Question: How can we make baseUrl optional? + Requests( + {this.accessToken, + this.baseUrl = '', + this.matrixAccessToken, + this.choreoApiKey}); + + Future post( + {required String url, required Map body}) async { + dynamic encoded; + encoded = jsonEncode(body); + + debugPrint(baseUrl! + url); + + final http.Response response = await http.post( + _uriBuilder(url), + body: encoded, + headers: _headers, + ); + handleError(response, body: body); + + return response; + } + + Future put( + {required String url, required Map body}) async { + dynamic encoded; + encoded = jsonEncode(body); + + debugPrint(baseUrl! + url); + + final http.Response response = await http.put( + _uriBuilder(url), + body: encoded, + headers: _headers, + ); + + handleError(response, body: body); + + return response; + } + + Future get({required String url, String objectId = ""}) async { + final http.Response response = + await http.get(_uriBuilder(url + objectId), headers: _headers); + + handleError(response, objectId: objectId); + + return response; + } + + Uri _uriBuilder(url) => + baseUrl != null ? Uri.parse(baseUrl! + url) : Uri.parse(url); + + Map _parseEachToString(Map json) { + for (final String key in json.keys) { + if (json[key].runtimeType != String) { + if (json[key].runtimeType == List) { + json[key].forEach((item) { + _parseEachToString(json[key]); + }); + } + if (json[key].runtimeType == Map) { + _parseEachToString(json[key]); + } + if (json[key].runtimeType == int || json[key].runtimeType == double) { + json[key] = json[key].toString(); + } + } + } + + return json; + } + + void handleError(http.Response response, + {Map? body, String? objectId}) { + //PTODO - handle 401 error - unauthorized call + //kick them back to login? + + addBreadcrumb() { + debugPrint("Error - code: ${response.statusCode}"); + debugPrint("api: ${response.request?.url}"); + debugPrint("request body: ${body ?? objectId}"); + Sentry.addBreadcrumb( + Breadcrumb.http( + url: response.request?.url ?? Uri(path: "not available"), + method: response.request?.method ?? "not available", + statusCode: response.statusCode, + ), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"body": body, "objectId": objectId}), + ); + } + + switch (response.statusCode) { + case 200: + case 201: + break; + case 502: + case 504: + addBreadcrumb(); + throw response; + default: + addBreadcrumb(); + throw response; + } + } + + get _headers { + final Map headers = { + "Content-Type": "application/json", + "Accept": "application/json", + }; + if (accessToken != null) { + headers["Authorization"] = 'Bearer ${accessToken!}'; + } + if (matrixAccessToken != null) { + headers["Matrix-Access-Token"] = matrixAccessToken!; + } + if (choreoApiKey != null) { + headers['api_key'] = choreoApiKey!; + } + return headers; + } +} diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart new file mode 100644 index 000000000..0cd4c2e9c --- /dev/null +++ b/lib/pangea/network/urls.dart @@ -0,0 +1,74 @@ +//TODO move baseAPI addition to request function + +import 'package:fluffychat/pangea/config/environment.dart'; + +/// autodocs +/// https://api.staging.pangea.chat/choreo/docs +/// username: admin +/// password: admin +/// +/// https://api.staging.pangea.chat/api/v1/ +class PApiUrls { + static String baseAPI = Environment.baseAPI; + static String choreoBaseApi = Environment.choreoApi; + + /// ---------------------- Languages -------------------------------------- + static String getLanguages = "/language/list"; + + /// ---------------------- Users -------------------------------------- + static String createUser = "/account/create"; + static String userDetails = "/account/get_user_access_token?pangea_user_id="; + static String updateUserProfile = "/account/update"; + static String paymentLink = "/account/payment_link"; + static String subscriptionExpiration = "/account/premium_expires_date"; + + /// ---------------------- Conversation Partner ------------------------- + static String searchUserProfiles = "/account/search"; + + /// ---------------------- Deprecated Class API ------------------------- + static String classListBySpaceIds = "/class/listbyspaceids"; + static String getClassByClassCode = "/class/class_by_code?class_code="; + + /// ---------------------- Exchange ----------------------------------- + static String exchangeClassValidate = "/class/validate_exchange"; + static String exchangeClass = "/class/class_exchange"; + static String isExchange = "/class/get_exchange?exchange_pangea_id="; + static String exchangeParticipantsStore = "/class/exchange/participant"; + static String exchangeInfoStore = "/class/exchange/create"; + static String fetchExchangeInfo = "/class/exchange/data?exchange_pangea_id="; + static String exchangeAcceptRequest = "/class/exchange/accept"; + static String makeAdminInExchange = "/class/exchange/admin/create"; + + ///-------------------------------- Deprecated analytics -------------------- + static String classAnalytics = "${Environment.choreoApi}/class_analytics"; + static String messageService = "/message_service"; + + ///-------------------------------- choreo -------------------------- + static String igc = "${Environment.choreoApi}/grammar"; + + static String igcLite = "${Environment.choreoApi}/grammar_lite"; + static String spanDetails = "${Environment.choreoApi}/span_details"; + + static String wordNet = "${Environment.choreoApi}/wordnet"; + static String contextualizedTranslation = + "${Environment.choreoApi}/translation/contextual"; + static String simpleTranslation = + "${Environment.choreoApi}/translation/direct"; + static String tokenize = "${Environment.choreoApi}/tokenize"; + static String contextualDefinition = + "${Environment.choreoApi}/contextual_definition"; + static String similarity = "${Environment.choreoApi}/similarity"; + static String topicInfo = "${Environment.choreoApi}/vocab_list"; + + static String firstStep = "/it_initialstep"; + static String subseqStep = "/it_step"; + + ///-------------------------------- revenue cat -------------------------- + static String rcApiV1 = "https://api.revenuecat.com/v1"; + static String rcApiV2 = + "https://api.revenuecat.com/v2/projects/${Environment.rcProjectId}"; + + static String rcApps = "$rcApiV2/apps"; + static String rcProducts = "$rcApiV2/offerings?expand=items.package.product"; + static String rcSubscribers = "$rcApiV1/subscribers"; +} diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart new file mode 100644 index 000000000..8a08e2625 --- /dev/null +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -0,0 +1,107 @@ +import 'package:fluffychat/pangea/extensions/pangea_room_extension.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 '../../../../utils/date_time_extension.dart'; +import '../../../widgets/avatar.dart'; +import '../../../widgets/matrix.dart'; +import '../../models/chart_analytics_model.dart'; +import 'base_analytics_page.dart'; +import 'list_summary_analytics.dart'; + +class AnalyticsListTile extends StatelessWidget { + const AnalyticsListTile({ + Key? key, + required this.model, + required this.displayName, + required this.avatar, + required this.type, + required this.id, + required this.selected, + required this.onTap, + required this.allowNavigateOnSelect, + }) : super(key: key); + + final Uri? avatar; + final String displayName; + final AnalyticsEntryType type; + final String id; + final ChartAnalyticsModel? model; + final bool selected; + final bool allowNavigateOnSelect; + + final void Function(AnalyticsSelected) onTap; + + @override + Widget build(BuildContext context) { + final Room? room = Matrix.of(context).client.getRoomById(id); + return Material( + color: selected + ? Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + child: ListTile( + leading: type == AnalyticsEntryType.privateChats + ? CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + radius: Avatar.defaultSize / 2, + child: const Icon(Icons.forum), + ) + : Avatar( + mxContent: avatar, + name: displayName, + littleIcon: room?.roomTypeIcon, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyLarge!.color, + ), + ), + ), + Tooltip( + message: L10n.of(context)!.timeOfLastMessage, + child: Text( + model?.lastMessage?.localizedTimeShort(context) ?? "", + style: TextStyle( + fontSize: 13, + color: Theme.of(context).textTheme.bodyMedium!.color, + ), + ), + ), + ], + ), + subtitle: ListSummaryAnalytics( + chartAnalytics: model, + ), + selected: selected, + onTap: () => (room?.isSpace ?? false) && allowNavigateOnSelect + ? context.go( + '/rooms/analytics/${room!.id}', + ) + : onTap( + AnalyticsSelected( + id, + type, + displayName, + ), + ), + trailing: (room?.isSpace ?? false) && + type != AnalyticsEntryType.privateChats && + allowNavigateOnSelect + ? const Icon(Icons.chevron_right) + : null, + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/bar_chart_card.dart b/lib/pangea/pages/analytics/bar_chart_card.dart new file mode 100644 index 000000000..bbdac5435 --- /dev/null +++ b/lib/pangea/pages/analytics/bar_chart_card.dart @@ -0,0 +1,52 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class BarChartCard extends StatelessWidget { + const BarChartCard({ + Key? key, + required this.barChartTitle, + required this.barChart, + required this.legend, + required this.loadingData, + }) : super(key: key); + + final String barChartTitle; + final BarChart? barChart; + final Widget legend; + final bool loadingData; + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + color: Theme.of(context).scaffoldBackgroundColor, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 6), + Text( + barChartTitle, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 14), + AspectRatio( + aspectRatio: 2, + child: loadingData + ? const Center( + child: CircularProgressIndicator(), + ) + : barChart, + ), + const SizedBox(height: 10), + legend, + const SizedBox(height: 6), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/bar_chart_placeholder_data.dart b/lib/pangea/pages/analytics/bar_chart_placeholder_data.dart new file mode 100644 index 000000000..31c5f5d52 --- /dev/null +++ b/lib/pangea/pages/analytics/bar_chart_placeholder_data.dart @@ -0,0 +1,287 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +import '../../enum/use_type.dart'; + +class BarChartPlaceHolderData { + static BarChartRodData randomBarChartRodData( + BuildContext context, int index) { + // final total = Random().nextInt(100); + // final it = total != 0 ? Random().nextInt(max(total - index, 1)) : 0; + // final igc = total != 0 ? Random().nextInt(max(total - it - index, 1)) : 0; + // // final direct = Random().nextInt(total - it - igc); + // final y1 = it.toDouble(); + // final y2 = y1 + igc.toDouble(); + // final y3 = total.toDouble(); + // final y4 = y3; + const total = 0; + const double y1 = 0; + const double y2 = 0; + const double y3 = 0; + const double y4 = 0; + + return BarChartRodData( + toY: total.toDouble(), + rodStackItems: [ + BarChartRodStackItem(0, y1, UseType.ta.color(context)), + BarChartRodStackItem(y1, y2, UseType.ga.color(context)), + BarChartRodStackItem(y2, y3, UseType.wa.color(context)), + BarChartRodStackItem(y3, y4, UseType.un.color(context)), + ], + borderRadius: BorderRadius.zero, + ); + } + + static List getRandomData(BuildContext context) { + const double barSpace = 16; + + const indices = [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ]; + + final List barChartGroupData = []; + + indices.asMap().forEach((key, value) { + barChartGroupData.add( + BarChartGroupData( + x: key, + barsSpace: barSpace, + barRods: [ + randomBarChartRodData(context, value), + ], + ), + ); + }); + + return barChartGroupData; + } + + static List getData( + Color dark, Color normal, Color light) { + const double barSpace = 16; + + return [ + BarChartGroupData( + x: 0, + barsSpace: barSpace, + barRods: [ + BarChartRodData( + toY: 17, + rodStackItems: [ + BarChartRodStackItem(0, 2, dark), + BarChartRodStackItem(2, 12, normal), + BarChartRodStackItem(12, 17, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 24, + rodStackItems: [ + BarChartRodStackItem(0, 13, dark), + BarChartRodStackItem(13, 14, normal), + BarChartRodStackItem(14, 24, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 23, + rodStackItems: [ + BarChartRodStackItem(0, 6, dark), + BarChartRodStackItem(6, 18, normal), + BarChartRodStackItem(18, 23, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 29, + rodStackItems: [ + BarChartRodStackItem(0, 9, dark), + BarChartRodStackItem(9, 15, normal), + BarChartRodStackItem(15, 29, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 32, + rodStackItems: [ + BarChartRodStackItem(0, 2, dark), + BarChartRodStackItem(2, 17, normal), + BarChartRodStackItem(17, 32, light), + ], + borderRadius: BorderRadius.zero, + ), + ], + ), + BarChartGroupData( + x: 1, + barsSpace: barSpace, + barRods: [ + BarChartRodData( + toY: 31, + rodStackItems: [ + BarChartRodStackItem(0, 11, dark), + BarChartRodStackItem(11, 18, normal), + BarChartRodStackItem(18, 31, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 35, + rodStackItems: [ + BarChartRodStackItem(0, 14, dark), + BarChartRodStackItem(14, 27, normal), + BarChartRodStackItem(27, 35, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 31, + rodStackItems: [ + BarChartRodStackItem(0, 8, dark), + BarChartRodStackItem(8, 24, normal), + BarChartRodStackItem(24, 31, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 15, + rodStackItems: [ + BarChartRodStackItem(0, 6, dark), + BarChartRodStackItem(6, 12, normal), + BarChartRodStackItem(12, 15, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 17, + rodStackItems: [ + BarChartRodStackItem(0, 9, dark), + BarChartRodStackItem(9, 15, normal), + BarChartRodStackItem(15, 17, light), + ], + borderRadius: BorderRadius.zero, + ), + ], + ), + BarChartGroupData( + x: 2, + barsSpace: barSpace, + barRods: [ + BarChartRodData( + toY: 34, + rodStackItems: [ + BarChartRodStackItem(0, 6, dark), + BarChartRodStackItem(6, 23, normal), + BarChartRodStackItem(23, 34, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 32, + rodStackItems: [ + BarChartRodStackItem(0, 7, dark), + BarChartRodStackItem(7, 24, normal), + BarChartRodStackItem(24, 32, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 14, + rodStackItems: [ + BarChartRodStackItem(0, 1, dark), + BarChartRodStackItem(1, 12, normal), + BarChartRodStackItem(12, 14, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 20, + rodStackItems: [ + BarChartRodStackItem(0, 4, dark), + BarChartRodStackItem(4, 15, normal), + BarChartRodStackItem(15, 20, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 24, + rodStackItems: [ + BarChartRodStackItem(0, 4, dark), + BarChartRodStackItem(4, 15, normal), + BarChartRodStackItem(15, 24, light), + ], + borderRadius: BorderRadius.zero, + ), + ], + ), + BarChartGroupData( + x: 3, + barsSpace: barSpace, + barRods: [ + BarChartRodData( + toY: 14, + rodStackItems: [ + BarChartRodStackItem(0, 1, dark), + BarChartRodStackItem(1, 12, normal), + BarChartRodStackItem(12, 14, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 27, + rodStackItems: [ + BarChartRodStackItem(0, 7, dark), + BarChartRodStackItem(7, 25, normal), + BarChartRodStackItem(25, 27, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 29, + rodStackItems: [ + BarChartRodStackItem(0, 6, dark), + BarChartRodStackItem(6, 23, normal), + BarChartRodStackItem(23, 29, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 16, + rodStackItems: [ + BarChartRodStackItem(0, 9, dark), + BarChartRodStackItem(9, 15, normal), + BarChartRodStackItem(15, 16, light), + ], + borderRadius: BorderRadius.zero, + ), + BarChartRodData( + toY: 15, + rodStackItems: [ + BarChartRodStackItem(0, 7, dark), + BarChartRodStackItem(7, 12, normal), + BarChartRodStackItem(12, 15, light), + ], + borderRadius: BorderRadius.zero, + ), + ], + ), + ]; + } +} diff --git a/lib/pangea/pages/analytics/base_analytics_page.dart b/lib/pangea/pages/analytics/base_analytics_page.dart new file mode 100644 index 000000000..ac664040f --- /dev/null +++ b/lib/pangea/pages/analytics/base_analytics_page.dart @@ -0,0 +1,358 @@ +import 'dart:math'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../widgets/layouts/max_width_body.dart'; +import '../../../widgets/matrix.dart'; +import '../../controllers/pangea_controller.dart'; +import '../../enum/bar_chart_view_enum.dart'; +import '../../enum/time_span.dart'; +import '../../models/chart_analytics_model.dart'; +import 'analytics_list_tile.dart'; +import 'construct_list.dart'; +import 'messages_bar_chart.dart'; +import 'time_span_menu_button.dart'; + +class BaseAnalyticsPage extends StatefulWidget { + final String pageTitle; + final TabData tabData1; + final TabData tabData2; + final Future Function(BuildContext) refreshData; + + final AnalyticsSelected defaultAnalyticsSelected; + final AnalyticsSelected? alwaysSelected; + + const BaseAnalyticsPage({ + Key? key, + required this.pageTitle, + required this.tabData1, + required this.tabData2, + required this.defaultAnalyticsSelected, + required this.refreshData, + required this.alwaysSelected, + }) : super(key: key); + + @override + State createState() => BaseAnalyticsController(); +} + +class BaseAnalyticsController extends State { + final PangeaController _pangeaController = MatrixState.pangeaController; + AnalyticsSelected? selected; + BarChartViewSelection selectedView = BarChartViewSelection.grammar; + + @override + void initState() { + super.initState(); + } + + bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id; + + ChartAnalyticsModel? chartData( + BuildContext context, AnalyticsSelected? selectedParam) { + final AnalyticsSelected analyticsSelected = + selectedParam ?? widget.defaultAnalyticsSelected; + + if (analyticsSelected.type == AnalyticsEntryType.privateChats) { + return _pangeaController.analytics.getAnalyticsLocal( + classId: analyticsSelected.id, + chatId: AnalyticsEntryType.privateChats.toString(), + ); + } + + String? chatId = analyticsSelected.type == AnalyticsEntryType.room + ? analyticsSelected.id + : null; + chatId ??= widget.alwaysSelected?.type == AnalyticsEntryType.room + ? widget.alwaysSelected?.id + : null; + + String? studentId = analyticsSelected.type == AnalyticsEntryType.student + ? analyticsSelected.id + : null; + studentId ??= widget.alwaysSelected?.type == AnalyticsEntryType.student + ? widget.alwaysSelected?.id + : null; + + String? classId = analyticsSelected.type == AnalyticsEntryType.space + ? analyticsSelected.id + : null; + classId ??= widget.alwaysSelected?.type == AnalyticsEntryType.space + ? widget.alwaysSelected?.id + : null; + + final data = _pangeaController.analytics.getAnalyticsLocal( + classId: classId, + chatId: chatId, + studentId: studentId, + ); + return data; + } + + String barTitle(BuildContext context) => + "${selectedView.string(context)}: ${selected == null ? widget.defaultAnalyticsSelected.displayName : selected!.displayName}"; + + TimeSpan get currentTimeSpan => + _pangeaController.analytics.currentAnalyticsTimeSpan; + + void toggleSelection(AnalyticsSelected selectedParam) { + setState(() { + debugPrint("selectedParam.id is ${selectedParam.id}"); + selected = isSelected(selectedParam.id) ? null : selectedParam; + }); + Future.delayed(Duration.zero, () => setState(() {})); + } + + void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) { + _pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan); + setState(() {}); + widget.refreshData(context).then((value) => setState(() {})); + } + + void toggleSelectedView(BarChartViewSelection view) { + selectedView = view; + setState(() {}); + } + + @override + Widget build(BuildContext context) => BaseAnalyticsView(controller: this); +} + +class BaseAnalyticsView extends StatelessWidget { + const BaseAnalyticsView({ + Key? key, + required this.controller, + }) : super(key: key); + + final BaseAnalyticsController controller; + + Widget chartView(BuildContext context) { + switch (controller.selectedView) { + case BarChartViewSelection.messages: + return MessagesBarChart( + chartAnalytics: controller.chartData(context, controller.selected), + barChartTitle: controller.barTitle(context), + ); + // case BarChartViewSelection.vocab: + // return ConstructList( + // selected: controller.selected, + // defaultSelected: controller.widget.defaultAnalyticsSelected, + // constructType: ConstructType.vocab, + // ); + case BarChartViewSelection.grammar: + return ConstructList( + selected: controller.selected, + defaultSelected: controller.widget.defaultAnalyticsSelected, + constructType: ConstructType.grammar, + title: controller.barTitle(context), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + controller.widget.pageTitle, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 18, + fontWeight: FontWeight.w700), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + actions: [ + for (final view in BarChartViewSelection.values) + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: controller.selectedView == view + ? AppConfig.primaryColor + : null, + ), + child: IconButton( + isSelected: controller.selectedView == view, + icon: Icon(view.icon), + tooltip: view.string(context), + onPressed: () => controller.toggleSelectedView(view), + ), + ), + TimeSpanMenuButton( + value: controller.currentTimeSpan, + onChange: (TimeSpan value) => + controller.toggleTimeSpan(context, value), + ), + // ChartViewPickerButton( + // selected: controller.selectedView, + // onChange: controller.toggleSelectedView, + // ), + ], + ), + body: MaxWidthBody( + withScrolling: false, + child: Column( + children: [ + chartView(context), + Expanded( + child: DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + tabs: [ + Tab( + icon: Icon( + controller.widget.tabData1.icon, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Tab( + icon: Icon( + controller.widget.tabData2.icon, + color: + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + Expanded( + child: SingleChildScrollView( + child: SizedBox( + height: max( + controller.widget.tabData1.items.length + 1, + controller.widget.tabData2.items.length) * + 72, + child: TabBarView( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...controller.widget.tabData1.items + .map( + (item) => AnalyticsListTile( + avatar: item.avatar, + model: controller.chartData( + context, + AnalyticsSelected( + item.id, + controller.widget.tabData1.type, + "", + ), + ), + displayName: item.displayName, + id: item.id, + type: controller.widget.tabData1.type, + selected: + controller.isSelected(item.id), + onTap: controller.toggleSelection, + allowNavigateOnSelect: controller + .widget + .tabData1 + .allowNavigateOnSelect, + ), + ) + .toList(), + if (controller.widget.defaultAnalyticsSelected + .type == + AnalyticsEntryType.space) + AnalyticsListTile( + avatar: null, + model: controller.chartData( + context, + AnalyticsSelected( + controller.widget + .defaultAnalyticsSelected.id, + AnalyticsEntryType.privateChats, + L10n.of(context)!.allPrivateChats, + ), + ), + displayName: + L10n.of(context)!.allPrivateChats, + id: controller + .widget.defaultAnalyticsSelected.id, + type: AnalyticsEntryType.privateChats, + selected: controller.isSelected(controller + .widget.defaultAnalyticsSelected.id), + onTap: controller.toggleSelection, + allowNavigateOnSelect: false, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: controller.widget.tabData2.items + .map( + (item) => AnalyticsListTile( + avatar: item.avatar, + model: controller.chartData( + context, + AnalyticsSelected( + item.id, + controller.widget.tabData2.type, + "", + ), + ), + displayName: item.displayName, + id: item.id, + type: controller.widget.tabData2.type, + selected: + controller.isSelected(item.id), + onTap: controller.toggleSelection, + allowNavigateOnSelect: controller.widget + .tabData2.allowNavigateOnSelect, + ), + ) + .toList(), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ) + ], + ), + ), + ); + } +} + +class TabData { + AnalyticsEntryType type; + IconData icon; + List items; + bool allowNavigateOnSelect; + + TabData( + {required this.type, + required this.items, + required this.icon, + this.allowNavigateOnSelect = true}); +} + +class TabItem { + Uri? avatar; + String displayName; + String id; + + TabItem({required this.avatar, required this.displayName, required this.id}); +} + +enum AnalyticsEntryType { student, room, space, privateChats } + +class AnalyticsSelected { + String id; + AnalyticsEntryType type; + String displayName; + + AnalyticsSelected(this.id, this.type, this.displayName); +} diff --git a/lib/pangea/pages/analytics/chart_view_picker_button.dart b/lib/pangea/pages/analytics/chart_view_picker_button.dart new file mode 100644 index 000000000..ad2e289d5 --- /dev/null +++ b/lib/pangea/pages/analytics/chart_view_picker_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../enum/bar_chart_view_enum.dart'; + +class ChartViewPickerButton extends StatelessWidget { + final BarChartViewSelection selected; + final void Function(BarChartViewSelection) onChange; + const ChartViewPickerButton( + {Key? key, required this.selected, required this.onChange}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: Icon(selected.icon), + tooltip: L10n.of(context)!.changeView, + initialValue: selected, + onSelected: (BarChartViewSelection? barChartView) { + if (barChartView == null) { + debugPrint("when is barChartView null?"); + return; + } + onChange(barChartView); + }, + itemBuilder: (BuildContext context) => BarChartViewSelection.values + .map>( + (BarChartViewSelection barChartView) { + return PopupMenuItem( + value: barChartView, + child: Text(barChartView.string(context)), + ); + }).toList(), + ); + } +} diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart new file mode 100644 index 000000000..1b286a222 --- /dev/null +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; +import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../../widgets/matrix.dart'; +import '../../../controllers/pangea_controller.dart'; +import '../../../utils/sync_status_util_v2.dart'; +import 'class_analytics_view.dart'; + +enum AnalyticsPageType { classList, student, classDetails } + +class ClassAnalyticsPage extends StatefulWidget { + // final AnalyticsPageType type; + const ClassAnalyticsPage({Key? key}) : super(key: key); + + @override + State createState() => ClassAnalyticsV2Controller(); +} + +class ClassAnalyticsV2Controller extends State { + final PangeaController _pangeaController = MatrixState.pangeaController; + bool _initialized = false; + StreamSubscription? stateSub; + Timer? refreshTimer; + + List chats = []; + List students = []; + + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () async { + if (classRoom == null || !classRoom!.isSpace) { + context.go('/rooms'); + } + stateSub = _pangeaController.matrixState.client.onRoomState.stream + .where( + (event) => + event.type == PangeaEventTypes.studentAnalyticsSummary && + event.roomId == classId, + ) + .listen(onStateUpdate); + getChatAndStudents(); + }); + } + + Future getChatAndStudents() async { + try { + await classRoom!.requestParticipants(); + + students = classRoom!.students; + + chats = classRoom!.spaceChildren + .where((element) => element.roomId != null) + .map((e) => Matrix.of(context).client.getRoomById(e.roomId!)) + .where((r) => r != null) + .cast() + .toList(); + + setState(() { + _initialized = true; + }); + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + } + } + + void onStateUpdate(Event newState) { + if (!(refreshTimer?.isActive ?? false)) { + refreshTimer = Timer( + const Duration(seconds: 3), + () => getChatAndStudentAnalytics(context, true), + ); + } + } + + @override + void dispose() { + super.dispose(); + refreshTimer?.cancel(); + stateSub?.cancel(); + } + + @override + Widget build(BuildContext context) { + if (!_initialized) return const PCircular(); + return PLoadingStatusV2( + // if we everr want it rebuild the whole thing each time (and run initState again) + // but this is computationally expensive! + // key: UniqueKey(), + shimmerChild: const ListPlaceholder(), + onFinish: () { + getChatAndStudentAnalytics(context); + }, + child: ClassAnalyticsView(this), + ); + } + + Future getChatAndStudentAnalytics( + BuildContext context, [ + forceUpdate = false, + ]) async { + try { + if (classRoom == null) { + debugger(when: kDebugMode); + ErrorHandler.logError(m: 'classroom should not be null'); + } + final List> analyticsFutures = []; + for (final student in students) { + analyticsFutures.add( + _pangeaController.analytics.getAnalytics( + classRoom: classRoom, + studentId: student.id, + forceUpdate: forceUpdate, + ), + ); + } + for (final chat in chats) { + analyticsFutures.add( + _pangeaController.analytics.getAnalytics( + classRoom: classRoom, + chatId: chat.id, + forceUpdate: forceUpdate, + ), + ); + } + analyticsFutures.add( + _pangeaController.analytics.getAnalytics( + classRoom: classRoom, + forceUpdate: forceUpdate, + ), + ); + analyticsFutures.add( + _pangeaController.analytics.getAnalyticsForPrivateChats( + classRoom: classRoom, + forceUpdate: forceUpdate, + ), + ); + await Future.wait(analyticsFutures); + if (mounted) setState(() {}); + } catch (err) { + debugger(when: kDebugMode); + } + } + + String? get classId => GoRouterState.of(context).pathParameters['classid']; + + Room? _classRoom; + Room? get classRoom { + _classRoom ??= classId != null + ? Matrix.of(context).client.getRoomById(classId!) + : null; + return _classRoom; + } + + String className(BuildContext context) { + return classRoom?.name ?? ""; + } +} diff --git a/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart new file mode 100644 index 000000000..e5b5f2dd9 --- /dev/null +++ b/lib/pangea/pages/analytics/class_analytics/class_analytics_view.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart'; +import '../base_analytics_page.dart'; +import 'class_analytics.dart'; + +class ClassAnalyticsView extends StatelessWidget { + final ClassAnalyticsV2Controller controller; + const ClassAnalyticsView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // final String pageTitle = + // "${L10n.of(context)!.classAnalytics}: ${controller.className(context)}"; + final String pageTitle = L10n.of(context)!.classAnalytics; + final TabData tab1 = TabData( + type: AnalyticsEntryType.room, + icon: Icons.chat_bubble_outline, + items: controller.chats + .map( + (room) => TabItem( + avatar: room.avatar, + displayName: + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + id: room.id, + ), + ) + .toList(), + ); + final TabData tab2 = TabData( + type: AnalyticsEntryType.student, + icon: Icons.people_outline, + items: controller.students + .map((s) => TabItem( + avatar: s.avatarUrl, + displayName: s.displayName ?? "unknown", + id: s.id, + )) + .toList(), + ); + + return controller.classId != null + ? BaseAnalyticsPage( + pageTitle: pageTitle, + tabData1: tab1, + tabData2: tab2, + defaultAnalyticsSelected: AnalyticsSelected( + controller.classId!, + AnalyticsEntryType.space, + controller.className(context), + ), + refreshData: controller.getChatAndStudentAnalytics, + alwaysSelected: AnalyticsSelected( + controller.classId!, + AnalyticsEntryType.space, + controller.className(context), + ), + ) + : const SizedBox(); + } +} diff --git a/lib/pangea/pages/analytics/class_list/class_list.dart b/lib/pangea/pages/analytics/class_list/class_list.dart new file mode 100644 index 000000000..85abf48cb --- /dev/null +++ b/lib/pangea/pages/analytics/class_list/class_list.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/enum/time_span.dart'; +import 'package:fluffychat/pangea/pages/analytics/class_list/class_list_view.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../../widgets/matrix.dart'; +import '../../../constants/pangea_event_types.dart'; +import '../../../controllers/pangea_controller.dart'; +import '../../../models/chart_analytics_model.dart'; +import '../../../utils/sync_status_util_v2.dart'; +import '../../../widgets/common/list_placeholder.dart'; + +class AnalyticsClassList extends StatefulWidget { + const AnalyticsClassList({Key? key}) : super(key: key); + + @override + State createState() => AnalyticsClassListController(); +} + +class AnalyticsClassListController extends State { + PangeaController pangeaController = MatrixState.pangeaController; + List models = []; + StreamSubscription? stateSub; + Map refreshTimer = {}; + + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () async { + stateSub = pangeaController.matrixState.client.onRoomState.stream + .where( + (event) => event.type == PangeaEventTypes.studentAnalyticsSummary) + .listen(onStateUpdate); + }); + } + + void onStateUpdate(Event newState) { + if (!(refreshTimer[newState.room.id]?.isActive ?? false)) { + refreshTimer[newState.room.id] = Timer( + const Duration(seconds: 3), + () => updateClassAnalytics(context, newState.room), + ); + } + } + + @override + void dispose() { + super.dispose(); + for (final timer in refreshTimer.values) { + timer.cancel(); + } + stateSub?.cancel(); + } + + @override + Widget build(BuildContext context) { + return PLoadingStatusV2( + shimmerChild: const ListPlaceholder(), + child: AnalyticsClassListView(this), + onFinish: () { + getAllClassAnalytics(context); + }, + ); + } + + Future getAllClassAnalytics(BuildContext context) async { + await pangeaController.analytics.allClassAnalytics(); + setState(() { + debugPrint("class list post getAllClassAnalytics"); + }); + } + + Future updateClassAnalytics( + BuildContext context, + Room classRoom, + ) async { + await pangeaController.analytics + .getAnalytics(classRoom: classRoom, forceUpdate: true); + setState(() { + debugPrint("class list post updateClassAnalytics"); + }); + } + + void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) { + pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan); + setState(() {}); + getAllClassAnalytics(context); + } +} diff --git a/lib/pangea/pages/analytics/class_list/class_list_view.dart b/lib/pangea/pages/analytics/class_list/class_list_view.dart new file mode 100644 index 000000000..552eaff6b --- /dev/null +++ b/lib/pangea/pages/analytics/class_list/class_list_view.dart @@ -0,0 +1,76 @@ +import 'package:fluffychat/pangea/extensions/client_extension.dart'; +import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart'; +import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../../widgets/matrix.dart'; +import '../../../enum/time_span.dart'; +import '../base_analytics_page.dart'; +import 'class_list.dart'; + +class AnalyticsClassListView extends StatelessWidget { + final AnalyticsClassListController controller; + const AnalyticsClassListView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final List classesAndExchanges = + Matrix.of(context).client.classesAndExchangesImTeaching; + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + L10n.of(context)!.classAnalytics, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + leading: IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () => context.pop(), + ), + actions: [ + TimeSpanMenuButton( + value: + controller.pangeaController.analytics.currentAnalyticsTimeSpan, + onChange: (TimeSpan value) => + controller.toggleTimeSpan(context, value), + ), + ], + ), + body: Column( + children: [ + // MessagesBarChart( + // chartAnalytics: controller.chartData(context), + // barChartTitle: "", + // ), + Flexible( + child: ListView.builder( + itemCount: classesAndExchanges.length, + itemBuilder: (context, i) => AnalyticsListTile( + avatar: classesAndExchanges[i].avatar, + model: controller.pangeaController.analytics + .getAnalyticsLocal(classId: classesAndExchanges[i].id), + displayName: classesAndExchanges[i].name, + id: classesAndExchanges[i].id, + type: AnalyticsEntryType.space, + selected: false, + onTap: (selected) => context.go( + '/rooms/analytics/${selected.id}', + ), + allowNavigateOnSelect: true, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/construct_cloud.dart b/lib/pangea/pages/analytics/construct_cloud.dart new file mode 100644 index 000000000..56fb956ce --- /dev/null +++ b/lib/pangea/pages/analytics/construct_cloud.dart @@ -0,0 +1,106 @@ +import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart'; +import 'package:flutter/material.dart'; + +import '../../word_cloud/word_cloud_data.dart'; +import '../../word_cloud/word_cloud_shape.dart'; +import '../../word_cloud/word_cloud_tap.dart'; +import '../../word_cloud/word_cloud_tap_view.dart'; +import '../../word_cloud/word_cloud_view.dart'; + +class ConstructCloud extends StatefulWidget { + final AnalyticsSelected? selected; + final AnalyticsSelected defaultSelected; + + const ConstructCloud({ + Key? key, + required this.selected, + required this.defaultSelected, + }) : super(key: key); + + @override + State createState() => ConstructCloudState(); +} + +class ConstructCloudState extends State { + int count = 0; + String wordstring = ''; + List> wordData = [ + {"word": "loading", 'value': 1}, + {"word": "loading", 'value': 1}, + {"word": "loading", 'value': 1}, + {"word": "loading", 'value': 1} + ]; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final WordCloudData wcdata = WordCloudData(data: wordData); + final WordCloudTap wordtaps = WordCloudTap(); + + for (int i = 0; i < wordData.length; i++) { + void tap() { + setState(() { + count += 1; + wordstring = wordData[i]['word']; + }); + } + + wordtaps.addWordtap(wordData[i]['word'], tap); + } + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Clicked Word : $wordstring', + style: const TextStyle(fontSize: 20), + ), + Text('Clicked Count : $count', style: const TextStyle(fontSize: 20)), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + WordCloudTapView( + data: wcdata, + wordtap: wordtaps, + mapcolor: const Color.fromARGB(255, 174, 183, 235), + mapwidth: 500, + mapheight: 500, + fontWeight: FontWeight.bold, + shape: WordCloudCircle(radius: 250), + colorlist: const [ + Colors.black, + Colors.redAccent, + Colors.indigoAccent + ], + ), + const SizedBox( + height: 15, + width: 30, + ), + WordCloudView( + data: wcdata, + mapcolor: const Color.fromARGB(255, 174, 183, 235), + mapwidth: 500, + mapheight: 500, + fontWeight: FontWeight.bold, + colorlist: const [ + Colors.black, + Colors.redAccent, + Colors.indigoAccent + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart new file mode 100644 index 000000000..bb9ce1da8 --- /dev/null +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -0,0 +1,183 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/constants/match_rule_ids.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.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'; + +import '../../constants/pangea_event_types.dart'; +import '../../models/construct_analytics_event.dart'; +import '../../utils/error_handler.dart'; + +class ConstructList extends StatefulWidget { + final AnalyticsSelected? selected; + final AnalyticsSelected defaultSelected; + final ConstructType constructType; + final String title; + + const ConstructList({ + Key? key, + required this.selected, + required this.defaultSelected, + required this.constructType, + required this.title, + }) : super(key: key); + + @override + State createState() => ConstructListState(); +} + +class ConstructListState extends State { + List constructs = []; + bool initialized = false; + String? langCode; + String? error; + + StreamSubscription? stateSub; + Timer? refreshTimer; + + @override + void initState() { + super.initState(); + + _updateConstructs(); + + stateSub = MatrixState + .pangeaController.matrixState.client.onRoomState.stream + //could optimize here be determing if the vocab event is relevant for + //currently displayed data + .where((event) => event.type == PangeaEventTypes.vocab) + .listen(onStateUpdate); + } + + void onStateUpdate(Event newState) { + if (!(refreshTimer?.isActive ?? false)) { + refreshTimer = Timer( + const Duration(seconds: 3), + () => _updateConstructs(), + ); + } + } + + @override + void dispose() { + super.dispose(); + refreshTimer?.cancel(); + stateSub?.cancel(); + } + + @override + void didUpdateWidget(ConstructList oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.selected?.id != oldWidget.selected?.id) { + _updateConstructs(); + } + } + + void _updateConstructs() { + setState(() { + initialized = false; + }); + MatrixState.pangeaController.analytics + .constuctEventsByAnalyticsSelected( + selected: widget.selected, + defaultSelected: widget.defaultSelected, + constructType: widget.constructType, + ) + .then((value) { + setState(() { + constructs = value; + initialized = true; + error = null; + }); + }).onError((error, stackTrace) { + ErrorHandler.logError(e: error, s: stackTrace); + setState(() { + constructs = []; + initialized = true; + error = error?.toString(); + }); + }); + } + + @override + Widget build(BuildContext context) { + return error != null + ? Center( + child: Text(error!), + ) + : Column( + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.bodyMedium, + ), + ConstructListView( + constructs: constructs.where((element) { + debugPrint("element type is ${element.content.type}"); + return element.content.lemma != + "Try interactive translation" && + element.content.lemma != "itStart" && + element.content.lemma != + MatchRuleIds.interactiveTranslation; + }).toList(), + init: initialized, + ), + ], + ); + } +} + +// list view of construct events +// parameters +// 1) a list of construct events and +// 2) a boolean indicating whether the list has been initialized +// if not initialized, show loading indicator +// for each tile, +// title = construct.content.lemma +// subtitle = total uses, equal to construct.content.uses.length +// list has a fixed height of 400 and is scrollable +class ConstructListView extends StatelessWidget { + final List constructs; + final bool init; + + const ConstructListView({ + Key? key, + required this.constructs, + required this.init, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (!init) { + return const SizedBox( + height: 400, + child: Center(child: CircularProgressIndicator()), + ); + } + if (constructs.isEmpty) { + return SizedBox( + height: 400, + child: Center(child: Text(L10n.of(context)!.noDataFound)), + ); + } + return SizedBox( + height: 400, + child: ListView.builder( + itemCount: constructs.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(constructs[index].content.lemma), + subtitle: Text( + '${L10n.of(context)!.total} ${constructs[index].content.uses.length}', + ), + ); + }, + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/list_summary_analytics.dart b/lib/pangea/pages/analytics/list_summary_analytics.dart new file mode 100644 index 000000000..6a322c189 --- /dev/null +++ b/lib/pangea/pages/analytics/list_summary_analytics.dart @@ -0,0 +1,100 @@ +import 'dart:math'; + +import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../enum/use_type.dart'; + +class ListSummaryAnalytics extends StatelessWidget { + final ChartAnalyticsModel? chartAnalytics; + + const ListSummaryAnalytics({Key? key, this.chartAnalytics}) : super(key: key); + + TimeSeriesTotals? get totals => chartAnalytics?.totals; + + String spacer(int baseLength, int number) => + " " * max(baseLength - number.toString().length, 0); + + WidgetSpan spacerIconText( + String toolTip, + String space, + IconData icon, + int value, + Color? color, [ + percentage = true, + ]) => + WidgetSpan( + child: Tooltip( + message: toolTip, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: space, + ), + WidgetSpan(child: Icon(icon, size: 14, color: color)), + TextSpan( + text: " $value${percentage ? "%" : ""}", + style: TextStyle(color: color), + ), + ], + ), + ), + ), + ); + + @override + Widget build(BuildContext context) { + if (totals == null) { + return const LinearProgressIndicator(); + } + final l10n = L10n.of(context); + + return RichText( + text: TextSpan( + children: [ + spacerIconText( + L10n.of(context) != null + ? L10n.of(context)!.totalMessages + : "Total messages sent", + "", + Icons.chat_bubble, + totals!.all, + Theme.of(context).textTheme.bodyLarge!.color, + false), + if (totals!.all != 0) ...[ + spacerIconText( + l10n != null ? l10n.taTooltip : "With translation assistance", + spacer(8, totals!.all), + UseType.ta.iconData, + totals!.taPercent, + UseType.ta.color(context), + ), + spacerIconText( + l10n != null ? l10n.gaTooltip : "With grammar assistance", + spacer(4, totals!.taPercent), + UseType.ga.iconData, + totals!.gaPercent, + UseType.ga.color(context), + ), + spacerIconText( + l10n != null ? l10n.waTooltip : "Without assistance", + spacer(4, totals!.gaPercent), + UseType.wa.iconData, + totals!.waPercent, + UseType.wa.color(context), + ), + spacerIconText( + l10n != null ? l10n.unTooltip : "Other", + spacer(4, totals!.waPercent), + UseType.un.iconData, + totals!.unPercent, + UseType.un.color(context), + ), + ] + ], + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/messages_bar_chart.dart b/lib/pangea/pages/analytics/messages_bar_chart.dart new file mode 100644 index 000000000..077bc3b33 --- /dev/null +++ b/lib/pangea/pages/analytics/messages_bar_chart.dart @@ -0,0 +1,352 @@ +import 'dart:developer'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fluffychat/pangea/pages/analytics/bar_chart_placeholder_data.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../enum/time_span.dart'; +import '../../enum/use_type.dart'; +import '../../models/chart_analytics_model.dart'; +import 'bar_chart_card.dart'; +import 'messages_legend_widget.dart'; + +class MessagesBarChart extends StatefulWidget { + final ChartAnalyticsModel? chartAnalytics; + final String barChartTitle; + + const MessagesBarChart( + {Key? key, required this.chartAnalytics, required this.barChartTitle}) + : super(key: key); + + @override + State createState() => MessagesBarChartState(); +} + +class MessagesBarChartState extends State { + final double barSpace = 16; + final List> intervalGroupings = []; + + @override + initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final flLine = FlLine( + color: Theme.of(context).dividerColor, + strokeWidth: 1, + ); + + final flTitlesData = FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + getTitlesWidget: bottomTitles, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: leftTitles, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ); + final barChartData = BarChartData( + alignment: BarChartAlignment.spaceEvenly, + barTouchData: BarTouchData( + enabled: false, + ), + // barTouchData: barTouchData, + titlesData: flTitlesData, + gridData: FlGridData( + show: true, + // checkToShowHorizontalLine: (value) => value % 10 == 0, + checkToShowHorizontalLine: (value) => true, + getDrawingHorizontalLine: (value) => flLine, + checkToShowVerticalLine: (value) => false, + getDrawingVerticalLine: (value) => flLine, + ), + borderData: FlBorderData( + show: false, + ), + groupsSpace: barSpace, + barGroups: barChartGroupData, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + ); + final barChart = BarChart( + barChartData, + swapAnimationDuration: const Duration(milliseconds: 250), + ); + + return BarChartCard( + barChartTitle: widget.barChartTitle, + barChart: barChart, + loadingData: widget.chartAnalytics == null, + legend: const MessagesLegendsListWidget(), + ); + } + + String getLabelBasedOnTimeSpan( + TimeSpan timeSpan, + TimeSeriesInterval current, + TimeSeriesInterval? last, + int labelIndex, + ) { + if (widget.chartAnalytics == null) { + return ""; + } + if (isInSameGroup(last, current, timeSpan)) { + return "-"; + } + switch (widget.chartAnalytics?.timeSpan ?? TimeSpan.month) { + case TimeSpan.day: + return current.end.hour % 3 == 0 + ? DateFormat(DateFormat.HOUR).format(current.end) + : ""; + // return current.end.hour.toString(); + case TimeSpan.week: + return DateFormat(DateFormat.ABBR_WEEKDAY).format(current.end); + case TimeSpan.month: + // return current.end.month != last?.end.month + // ? DateFormat(DateFormat.ABBR_MONTH_DAY).format(current.end) + // : current.end.day % 5 == 0 && + // labelIndex != 1 && + // current.end.day != 30 + // ? DateFormat(DateFormat.DAY).format(current.end) + // : "'"; + return current.end.month != last?.end.month + ? DateFormat(DateFormat.ABBR_MONTH).format(current.end) + : DateFormat(DateFormat.DAY).format(current.end); + // return current.end.day.toString(); + // text = DateFormat('DAY').format(timeSeriesIntervalStart); + case TimeSpan.sixmonths: + case TimeSpan.year: + return DateFormat(DateFormat.ABBR_STANDALONE_MONTH).format(current.end); + default: + return ''; + } + } + + Widget bottomTitles(double value, TitleMeta meta) { + if (widget.chartAnalytics == null) { + return Container(); + } + String text; + final index = value.toInt(); + final TimeSpan timeSpan = widget.chartAnalytics?.timeSpan ?? TimeSpan.month; + final TimeSeriesInterval? last = + index != 0 ? intervalGroupings[index - 1].last : null; + final TimeSeriesInterval current = intervalGroupings[index].last; + + text = getLabelBasedOnTimeSpan(timeSpan, current, last, index); + + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + text, + style: titleTextStyle(context), + ), + ); + } + + TextStyle titleTextStyle(context) => TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 10, + ); + + Widget leftTitles(double value, TitleMeta meta) { + Widget textWidget; + if (value != meta.max) { + textWidget = Text(meta.formattedValue, style: titleTextStyle(context)); + } else { + textWidget = const Icon(Icons.chat_bubble, size: 14); + } + return SideTitleWidget( + axisSide: meta.axisSide, + child: textWidget, + ); + } + + bool isInSameGroup( + TimeSeriesInterval? t1, TimeSeriesInterval t2, TimeSpan timeSpan) { + final DateTime? date1 = t1?.end; + final DateTime date2 = t2.end; + if (timeSpan == TimeSpan.sixmonths || timeSpan == TimeSpan.year) { + return date1?.month == date2.month; + } else if (timeSpan == TimeSpan.week) { + return date1?.day == date2.day; + } else { + return false; + } + } + + void makeIntervalGroupings() { + intervalGroupings.clear(); + try { + for (final timeSeriesInterval + in widget.chartAnalytics?.timeSeries ?? []) { + //Note: if we decide we'd like to do some sort of grouping in the future, + // this is where that could happen. Currently, we're just putting one + // BarChartRod in each BarChartGroup + final TimeSeriesInterval? last = + intervalGroupings.isNotEmpty ? intervalGroupings.last.last : null; + + if (isInSameGroup( + last, + timeSeriesInterval, + widget.chartAnalytics!.timeSpan, + )) { + intervalGroupings.last.add(timeSeriesInterval); + } else { + intervalGroupings.add([timeSeriesInterval]); + } + } + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + List get barChartGroupData { + if (widget.chartAnalytics == null) { + return BarChartPlaceHolderData.getRandomData(context); + } + + makeIntervalGroupings(); + + final List chartData = []; + + intervalGroupings.asMap().forEach((index, intervalGroup) { + chartData.add(BarChartGroupData( + x: index, + barsSpace: barSpace, + // barRods: intervalGroup.map(constructBarChartRodData).toList(), + barRods: constructBarChartRodData(intervalGroup), + )); + }); + return chartData; + } + + // BarChartRodData constructBarChartRodData(TimeSeriesInterval timeSeriesInterval) { + // final double y1 = timeSeriesInterval.spanIT.toDouble(); + // final double y2 = + // (timeSeriesInterval.spanIT + timeSeriesInterval.spanIGC).toDouble(); + // final double y3 = timeSeriesInterval.spanTotal.toDouble(); + // return BarChartRodData( + // toY: y3, + // width: 10.toDouble(), + // rodStackItems: [ + // BarChartRodStackItem(0, y1, UseType.ta.color(context)), + // BarChartRodStackItem(y1, y2, UseType.ga.color(context)), + // BarChartRodStackItem(y2, y3, UseType.wa.color(context)), + // ], + // borderRadius: BorderRadius.zero, + // ); + // } + + List constructBarChartRodData( + List timeSeriesIntervalGroup) { + int y1 = 0; + int y2 = 0; + int y3 = 0; + int y4 = 0; + for (final e in timeSeriesIntervalGroup) { + y1 += e.totals.ta; + y2 += y1 + e.totals.ga; + y3 += y2 + e.totals.wa; + y4 += y3 + e.totals.un; + } + return [ + BarChartRodData( + toY: y4.toDouble(), + width: 10.toDouble(), + rodStackItems: [ + BarChartRodStackItem(0, y1.toDouble(), UseType.ta.color(context)), + BarChartRodStackItem( + y1.toDouble(), y2.toDouble(), UseType.ga.color(context)), + BarChartRodStackItem( + y2.toDouble(), y3.toDouble(), UseType.wa.color(context)), + BarChartRodStackItem( + y3.toDouble(), y4.toDouble(), UseType.un.color(context)), + ], + borderRadius: BorderRadius.zero, + ) + ]; + } + + // BarTouchData get barTouchData => BarTouchData( + // touchTooltipData: BarTouchTooltipData( + // fitInsideVertically: true, + // tooltipBgColor: Colors.blueGrey, + // getTooltipItem: (group, groupIndex, rod, rodIndex) { + // return BarTooltipItem( + // "groupindex $groupIndex rodIndex $rodIndex", + // const TextStyle( + // color: Colors.white, + // fontWeight: FontWeight.bold, + // fontSize: 18, + // ), + // children: [ + // toolTipText(rod), + // ], + // ); + // }, + // ), + // // touchCallback: (FlTouchEvent event, barTouchResponse) { + // // setState(() { + // // if (!event.isInterestedForInteractions || + // // barTouchResponse == null || + // // barTouchResponse.spot == null) { + // // touchedIndex = -1; + // // return; + // // } + // // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; + // // }); + // // }, + // ); + + // TextSpan toolTipText(BarChartRodData rodData) { + // double rodPercentage(int index) { + // return (rodData.rodStackItems[index].toY - + // rodData.rodStackItems[index].fromY) / + // rodData.toY * + // 100; + // } + + // return TextSpan( + // children: [ + // const WidgetSpan( + // child: Icon(Icons.chat_bubble, size: 14), + // ), + // TextSpan( + // text: " ${rodData.toY}", + // ), + // TextSpan( + // text: "/nIT ${rodPercentage(0)}%", + // style: TextStyle(color: UseType.ta.color(context)), + // ), + // TextSpan( + // text: " IGC ${rodPercentage(1)}%", + // style: TextStyle(color: UseType.ga.color(context)), + // ), + // TextSpan( + // text: " Direct ${rodPercentage(2)}%", + // style: TextStyle(color: UseType.wa.color(context)), + // ), + // ], + // ); + // } +} diff --git a/lib/pangea/pages/analytics/messages_legend_widget.dart b/lib/pangea/pages/analytics/messages_legend_widget.dart new file mode 100644 index 000000000..7b9cd5b58 --- /dev/null +++ b/lib/pangea/pages/analytics/messages_legend_widget.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/pangea/enum/use_type.dart'; +import 'package:flutter/material.dart'; + +class MessagesLegendsListWidget extends StatelessWidget { + const MessagesLegendsListWidget({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + children: UseType.values + .map( + // (e) => e.iconView(context, e.color(context), 20), + (e) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e.color(context), + ), + ), + const SizedBox(width: 4), + e.iconView(context, e.color(context), 20) + ], + ), + ) + .toList(), + ); + } +} diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart new file mode 100644 index 000000000..e59f79bdd --- /dev/null +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -0,0 +1,107 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/chart_analytics_model.dart'; +import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../../widgets/matrix.dart'; +import '../../../controllers/pangea_controller.dart'; +import '../../../extensions/client_extension.dart'; +import '../../../utils/sync_status_util_v2.dart'; +import '../base_analytics_page.dart'; +import 'student_analytics_view.dart'; + +class StudentAnalyticsPage extends StatefulWidget { + const StudentAnalyticsPage({Key? key}) : super(key: key); + + @override + State createState() => StudentAnalyticsController(); +} + +class StudentAnalyticsController extends State { + final PangeaController _pangeaController = MatrixState.pangeaController; + + AnalyticsSelected? selected; + + @override + void initState() { + _pangeaController.matrixState.client + .updateMyLearningAnalyticsForAllClassesImIn( + _pangeaController.pStoreService); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PLoadingStatusV2( + // if we everr want it rebuild the whole thing each time (and run initState again) + // but this is computationally expensive! + // key: UniqueKey(), + shimmerChild: const ListPlaceholder(), + onFinish: () { + getClassAndChatAnalytics(context); + }, + child: StudentAnalyticsView(this), + ); + } + + Future getClassAndChatAnalytics(BuildContext context) async { + final List> analyticsFutures = []; + for (final chat in chats(context)) { + analyticsFutures.add( + _pangeaController.analytics.getAnalytics( + chatId: chat.id, + studentId: userId, + ), + ); + } + for (final space in spaces(context)) { + analyticsFutures.add( + _pangeaController.analytics.getAnalytics( + classRoom: space, + studentId: userId, + ), + ); + } + analyticsFutures.add( + _pangeaController.analytics.getAnalytics(studentId: userId), + ); + await Future.wait(analyticsFutures); + setState(() {}); + } + + List spaces(BuildContext context) { + try { + return _pangeaController + .matrixState.client.classesAndExchangesImStudyingIn; + } catch (err) { + debugger(when: kDebugMode); + return []; + } + } + + List chats(BuildContext context) { + try { + return Matrix.of(context) + .client + .rooms + .where((r) => !r.isSpace && !r.isAnalyticsRoom) + .toList(); + } catch (err) { + debugger(when: kDebugMode); + return []; + } + } + + String? get userId { + final id = _pangeaController.matrixState.client.userID; + debugger(when: kDebugMode && id == null); + return id; + } + + String get username => + _pangeaController.matrixState.client.userID?.localpart ?? ""; +} diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart new file mode 100644 index 000000000..0ea479d22 --- /dev/null +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics_view.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart'; +import '../base_analytics_page.dart'; +import 'student_analytics.dart'; + +class StudentAnalyticsView extends StatelessWidget { + final StudentAnalyticsController controller; + const StudentAnalyticsView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final List chats = controller.chats(context); + final List spaces = controller.spaces(context); + + final String pageTitle = L10n.of(context)!.myLearning; + final TabData chatTabData = TabData( + type: AnalyticsEntryType.room, + icon: Icons.chat_bubble_outline, + items: chats + .map((c) => TabItem( + avatar: c.avatar, + displayName: + c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + id: c.id, + )) + .toList(), + allowNavigateOnSelect: false, + ); + final TabData classTabData = TabData( + type: AnalyticsEntryType.space, + icon: Icons.workspaces, + items: spaces + .map((c) => TabItem( + avatar: c.avatar, + displayName: c + .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), + id: c.id, + )) + .toList(), + allowNavigateOnSelect: false); + + return controller.userId != null + ? BaseAnalyticsPage( + pageTitle: pageTitle, + tabData1: chatTabData, + tabData2: classTabData, + defaultAnalyticsSelected: AnalyticsSelected( + controller.userId!, + AnalyticsEntryType.student, + L10n.of(context)!.allChatsAndClasses), + refreshData: controller.getClassAndChatAnalytics, + alwaysSelected: AnalyticsSelected( + controller.userId!, + AnalyticsEntryType.student, + L10n.of(context)!.allChatsAndClasses), + ) + : const SizedBox(); + } +} diff --git a/lib/pangea/pages/analytics/time_span_menu_button.dart b/lib/pangea/pages/analytics/time_span_menu_button.dart new file mode 100644 index 000000000..ec002bc5e --- /dev/null +++ b/lib/pangea/pages/analytics/time_span_menu_button.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../enum/time_span.dart'; + +class TimeSpanMenuButton extends StatelessWidget { + final TimeSpan value; + final void Function(TimeSpan) onChange; + const TimeSpanMenuButton( + {Key? key, required this.value, required this.onChange}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + icon: const Icon(Icons.calendar_month_outlined), + tooltip: L10n.of(context)!.changeDateRange, + initialValue: value, + onSelected: (TimeSpan? timeSpan) { + if (timeSpan == null) { + debugPrint("when is timeSpan null?"); + return; + } + onChange(timeSpan); + }, + itemBuilder: (BuildContext context) => + TimeSpan.values.map>((TimeSpan timeSpan) { + return PopupMenuItem( + value: timeSpan, + child: Text(timeSpan.string(context)), + ); + }).toList(), + ); + } +} diff --git a/lib/pangea/pages/analytics/vocab_bar_chart.dart b/lib/pangea/pages/analytics/vocab_bar_chart.dart new file mode 100644 index 000000000..922ba9592 --- /dev/null +++ b/lib/pangea/pages/analytics/vocab_bar_chart.dart @@ -0,0 +1,171 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/headwords.dart'; +import 'package:fluffychat/pangea/pages/analytics/base_analytics_page.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +import 'bar_chart_card.dart'; +import 'messages_legend_widget.dart'; + +class VocabBarChart extends StatefulWidget { + final AnalyticsSelected? selected; + final AnalyticsSelected defaultSelected; + + const VocabBarChart({ + Key? key, + required this.selected, + required this.defaultSelected, + }) : super(key: key); + + @override + State createState() => VocabBarChartState(); +} + +class VocabBarChartState extends State { + final double barSpace = 16; + + final PangeaController _pangeaController = MatrixState.pangeaController; + + @override + initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _pangeaController.analytics.vocabHeadsByAnalyticsSelected( + selected: widget.selected, + defaultSelected: widget.defaultSelected, + ), + builder: ((context, snapshot) => BarChartCard( + barChartTitle: (widget.selected != null + ? widget.selected! + : widget.defaultSelected) + .displayName, + barChart: snapshot.hasData + ? buildBarChart(context, snapshot.data!) + : null, + loadingData: snapshot.connectionState != ConnectionState.done, + legend: const MessagesLegendsListWidget(), + )), + ); + } + + TextStyle titleTextStyle(BuildContext context) => TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 10, + ); + + BarChart buildBarChart(BuildContext context, VocabHeadwords vocabHeadwords) { + final flLine = FlLine( + color: Theme.of(context).dividerColor, + strokeWidth: 1, + ); + + return BarChart( + BarChartData( + alignment: BarChartAlignment.spaceEvenly, + barTouchData: BarTouchData( + enabled: false, + ), + // barTouchData: barTouchData, + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + getTitlesWidget: (double value, TitleMeta meta) => + SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + vocabHeadwords.lists[value.toInt()].name, + style: titleTextStyle(context), + ), + ), + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: (double value, TitleMeta meta) { + Widget textWidget; + if (value != meta.max) { + textWidget = + Text(meta.formattedValue, style: titleTextStyle(context)); + } else { + textWidget = const Icon(Icons.abc_outlined, size: 14); + } + return SideTitleWidget( + axisSide: meta.axisSide, + child: textWidget, + ); + }, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + // checkToShowHorizontalLine: (value) => value % 10 == 0, + checkToShowHorizontalLine: (value) => true, + getDrawingHorizontalLine: (value) => flLine, + checkToShowVerticalLine: (value) => false, + getDrawingVerticalLine: (value) => flLine, + ), + borderData: FlBorderData( + show: false, + ), + groupsSpace: barSpace, + barGroups: barChartGroupData(vocabHeadwords), + backgroundColor: Colors.transparent, + ), + swapAnimationDuration: const Duration(milliseconds: 250), + ); + } + + List barChartGroupData(VocabHeadwords vocabHeadwords) { + // sort vocab into lists + // calculate levels based on vocab data + + final List chartData = []; + + vocabHeadwords.lists.asMap().forEach((index, intervalGroup) { + chartData.add(BarChartGroupData( + x: index, + barsSpace: barSpace, + // barRods: intervalGroup.map(constructBarChartRodData).toList(), + barRods: constructBarChartRodData(intervalGroup), + )); + }); + return chartData; + } + + List constructBarChartRodData(VocabList vocabList) { + final ListTotals listTotals = vocabList.calculuateTotals(); + final y1 = listTotals.low; + final y2 = y1 + listTotals.medium; + final y3 = y2 + listTotals.high; + + return [ + BarChartRodData( + toY: y3.toDouble(), + width: 10.toDouble(), + rodStackItems: [ + BarChartRodStackItem(0, y1.toDouble(), Colors.red), + BarChartRodStackItem(y1.toDouble(), y2.toDouble(), Colors.grey), + BarChartRodStackItem(y2.toDouble(), y3.toDouble(), Colors.green), + ], + borderRadius: BorderRadius.zero, + ) + ]; + } +} diff --git a/lib/pangea/pages/analytics/vocab_legend_widget.dart b/lib/pangea/pages/analytics/vocab_legend_widget.dart new file mode 100644 index 000000000..07f752685 --- /dev/null +++ b/lib/pangea/pages/analytics/vocab_legend_widget.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/pangea/enum/use_type.dart'; +import 'package:flutter/material.dart'; + +class VocabLegendsListWidget extends StatelessWidget { + const VocabLegendsListWidget({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + children: UseType.values + .where((e) => e != UseType.un) + .map( + (e) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e.color(context), + ), + ), + const SizedBox(width: 4), + e.iconView(context, e.color(context), 20) + ], + ), + ) + .toList(), + ); + } +} diff --git a/lib/pangea/pages/class_analytics/measure_able.dart b/lib/pangea/pages/class_analytics/measure_able.dart new file mode 100644 index 000000000..8e689b628 --- /dev/null +++ b/lib/pangea/pages/class_analytics/measure_able.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class MeasurableWidget extends StatefulWidget { + final Widget child; + + Function? triggerMeasure; + final Function(Size? size, Offset? position) onChange; + + MeasurableWidget({Key? key, required this.onChange, required this.child}) + : super(key: key); + + @override + _WidgetSizeState createState() => _WidgetSizeState(); +} + +class _WidgetSizeState extends State { + var widgetKey = GlobalKey(); + Offset? oldPosition; + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + void postFrameCallback(_) { + final context = widgetKey.currentContext; + if (context == null) return; + + final newSize = context.size; + + final RenderBox? box = + widgetKey.currentContext?.findRenderObject() as RenderBox?; + final Offset? position = box?.localToGlobal(Offset.zero); + + if (oldPosition != null) { + if (oldPosition!.dx == position!.dx && oldPosition!.dy == position.dy) { + return; + } + } + oldPosition = position; + + widget.onChange(newSize, position); + } + + @override + Widget build(BuildContext context) { + SchedulerBinding.instance.addPostFrameCallback(postFrameCallback); + return Container( + key: widgetKey, + child: widget.child, + ); + } +} diff --git a/lib/pangea/pages/class_invitation_selection/class_invitation_selection.dart b/lib/pangea/pages/class_invitation_selection/class_invitation_selection.dart new file mode 100644 index 000000000..128b97131 --- /dev/null +++ b/lib/pangea/pages/class_invitation_selection/class_invitation_selection.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:collection/collection.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'; + +class ClassInvitationSelection extends StatefulWidget { + const ClassInvitationSelection({Key? key}) : super(key: key); + + @override + ClassInvitationSelectionController createState() => + ClassInvitationSelectionController(); +} + +class ClassInvitationSelectionController + extends State { + TextEditingController controller = TextEditingController(); + late String currentSearchTerm; + bool loading = true; + List allClassParticipants = []; + List allChatParticipants = []; + List classParticipantsFilteredByChat = []; + + ///Class participants filtered by chat participants and any search query + List foundProfiles = []; + + Timer? coolDown; + + String? get roomId => GoRouterState.of(context).pathParameters['roomid']; + + StreamSubscription? _spaceSubscription; + + void inviteAction(BuildContext context, String id) async { + final room = Matrix.of(context).client.getRoomById(roomId!)!; + if (OkCancelResult.ok != + await showOkCancelAlertDialog( + context: context, + title: L10n.of(context)!.inviteContactToGroup(id, room.name), + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.cancel, + )) { + return; + } + final success = await showFutureLoadingDialog( + context: context, + future: () => room.invite(id), + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup), + ), + ); + } + } + + void setDisplayListWithCoolDown(String text) { + coolDown?.cancel(); + coolDown = Timer( + const Duration(milliseconds: 0), + () => _setfoundProfiles(context, text), + ); + } + + void _setfoundProfiles(BuildContext context, String text) { + coolDown?.cancel(); + // debugger(when: kDebugMode); + allClassParticipants = getClassParticipants(context); + allChatParticipants = getChatParticipants(context); + classParticipantsFilteredByChat = getClassParticipantsFilteredByChat(); + + currentSearchTerm = text; + + foundProfiles = currentSearchTerm.isNotEmpty + ? classParticipantsFilteredByChat + .where( + (user) => + user.displayName?.contains(text) ?? + false || user.id.contains(text), + ) + .toList() + : classParticipantsFilteredByChat; + + setState(() => loading = false); + } + + Room? _getParentClass(BuildContext context) => Matrix.of(context) + .client + .rooms + .where( + (r) => r.isSpace, + ) + .firstWhereOrNull( + (space) => space.spaceChildren.any( + (ithroom) => ithroom.roomId == roomId, + ), + ); + + List getClassParticipants(BuildContext context) { + final Room? parent = _getParentClass(context); + if (parent == null) return []; + + final List classParticipants = + parent.getParticipants([Membership.join]); + return classParticipants; + } + + List getChatParticipants(BuildContext context) => Matrix.of(context) + .client + .getRoomById(roomId!)! + .getParticipants([Membership.join, Membership.invite]).toList(); + + List getClassParticipantsFilteredByChat() => allClassParticipants + .where( + (profile) => + allChatParticipants.indexWhere((u) => u.id == profile.id) == -1, + ) + .toList(); + + @override + void initState() { + Future.delayed(Duration.zero, () async { + final Room? classParent = _getParentClass(context); + await classParent + ?.requestParticipants([Membership.join, Membership.invite]); + _setfoundProfiles(context, ""); + _spaceSubscription = Matrix.of(context) + .client + .onSync + .stream + .where( + (event) => + event.rooms?.join?.keys + .any((ithRoomId) => ithRoomId == classParent?.id) ?? + false, + ) + .listen( + (SyncUpdate syncUpdate) async { + debugPrint("updating lists"); + await classParent + ?.requestParticipants([Membership.join, Membership.invite]); + setState(() {}); + }, + ); + }); + super.initState(); + } + + @override + void dispose() { + _spaceSubscription?.cancel(); + super.dispose(); + } + + @override + // Widget build(BuildContext context) => InvitationSelectionView(this); + Widget build(BuildContext context) => const SizedBox(); +} diff --git a/lib/pangea/pages/class_settings/class_name_header.dart b/lib/pangea/pages/class_settings/class_name_header.dart new file mode 100644 index 000000000..29d0aa77f --- /dev/null +++ b/lib/pangea/pages/class_settings/class_name_header.dart @@ -0,0 +1,47 @@ +import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/visibility.dart' as visible; +import 'package:matrix/matrix.dart'; + +class ClassNameHeader extends StatelessWidget { + final Room room; + final ChatDetailsController controller; + const ClassNameHeader({ + Key? key, + required this.room, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: + room.canSendDefaultStates ? controller.setDisplaynameAction : null, + onHover: room.canSendDefaultStates ? controller.hoverEditNameIcon : null, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 25), + ), + label: visible.Visibility( + visible: controller.showEditNameIcon, + child: Icon( + Icons.edit, + color: Theme.of(context).colorScheme.onBackground, + ), + ), + icon: room.nameAndRoomTypeIcon(TextStyle( + fontSize: 20, + color: Theme.of(context).textTheme.bodyLarge!.color, + )), + // icon: Text( + // room.getLocalizedDisplayname( + // MatrixLocals(L10n.of(context)!), + // ), + // style: TextStyle( + // fontSize: 20, + // color: Theme.of(context).textTheme.bodyText1!.color, + // ), + // ), + ); + } +} diff --git a/lib/pangea/pages/class_settings/class_settings_page.dart b/lib/pangea/pages/class_settings/class_settings_page.dart new file mode 100644 index 000000000..94e890151 --- /dev/null +++ b/lib/pangea/pages/class_settings/class_settings_page.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/layouts/empty_page.dart'; +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 '../../../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({Key? key}) : super(key: key); + + @override + State createState() => ClassSettingsController(); +} + +class ClassSettingsController extends State { + PangeaController pangeaController = MatrixState.pangeaController; + + final GlobalKey rulesEditorKey = GlobalKey(); + final GlobalKey classSettingsKey = + GlobalKey(); + + Room? room; + + String? get roomId => GoRouterState.of(context).pathParameters['roomid']; + + Future handleSave(BuildContext context) async { + if (classSettingsKey.currentState!.sameLanguages) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.noIdenticalLanguages), + ), + ); + return; + } + if (rulesEditorKey.currentState != null) { + await rulesEditorKey.currentState?.setRoomRules(roomId!); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(m: "Null rules editor state"); + } + if (classSettingsKey.currentState != null) { + await classSettingsKey.currentState?.setClassSettings( + roomId!, + ); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(m: "Null class settings state"); + } + } + + void goback(BuildContext context) { + context.push("/spaces/$roomId"); + } + + String get className => + Matrix.of(context).client.getRoomById(roomId!)?.name ?? ''; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + Future.delayed(Duration.zero, () { + room = Matrix.of(context).client.getRoomById(roomId!); + if (room == null) { + debugger(when: kDebugMode); + context.pop(); + } + setState(() {}); + }); + } + //PTODO - show loading widget + + void setDisplaynameAction() => setClassDisplayname(context, roomId); + + bool showEditNameIcon = false; + void hoverEditNameIcon(bool hovering) => + setState(() => showEditNameIcon = !showEditNameIcon); + + @override + Widget build(BuildContext context) => room != null + ? ClassSettingsPageView(controller: this) + : const EmptyPage(); +} diff --git a/lib/pangea/pages/class_settings/class_settings_view.dart b/lib/pangea/pages/class_settings/class_settings_view.dart new file mode 100644 index 000000000..9d5e575ed --- /dev/null +++ b/lib/pangea/pages/class_settings/class_settings_view.dart @@ -0,0 +1,85 @@ +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 '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 '../../../widgets/layouts/max_width_body.dart'; + +class ClassSettingsPageView extends StatelessWidget { + final ClassSettingsController controller; + const ClassSettingsPageView({Key? key, required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + debugPrint("in class settings page with roomId ${controller.roomId}"); + // PTODO-Lala - make the page scrollable anywhere, not just in the area of the elements + // so like, the user should be able scroll using the mouse wheel from anywhere within this view + // currently, your cursor needs be horizontally within the tiles in order to scroll + return Scaffold( + appBar: AppBar( + leading: GoRouterState.of(context).path?.startsWith('/spaces/') ?? false + ? null + : IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () => controller.goback(context), + ), + centerTitle: true, + title: Text(L10n.of(context)!.classSettings), + ), + body: ListView( + children: [ + MaxWidthBody( + child: ListTile( + title: Center( + child: TextButton.icon( + onPressed: controller.setDisplaynameAction, + onHover: controller.hoverEditNameIcon, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 25), + ), + label: Visibility( + visible: controller.showEditNameIcon, + child: Icon( + Icons.edit, + color: Theme.of(context).colorScheme.onBackground, + ), + ), + icon: Text( + controller.className, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + MaxWidthBody( + child: Column( + children: [ + ClassSettings( + roomId: controller.roomId, + startOpen: true, + ), + RoomRulesEditor(roomId: controller.roomId), + ], + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => showFutureLoadingDialog( + context: context, + future: () => controller.handleSave(context), + ), + label: Text(L10n.of(context)!.saveChanges), + icon: const Icon(Icons.save_outlined), + ), + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_analytics_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_analytics_button.dart new file mode 100644 index 000000000..dad1a9d91 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_analytics_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +class ClassAnalyticsButton extends StatelessWidget { + const ClassAnalyticsButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final roomId = GoRouterState.of(context).pathParameters['roomid']; + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + + return Column( + children: [ + ListTile( + title: Text( + L10n.of(context)!.classAnalytics, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(L10n.of(context)!.classAnalyticsDesc), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.analytics_outlined), + ), + onTap: () => context.go('/rooms/analytics/$roomId'), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart new file mode 100644 index 000000000..e7b786381 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_description_button.dart @@ -0,0 +1,51 @@ +import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class ClassDescriptionButton extends StatelessWidget { + final Room room; + final ChatDetailsController controller; + const ClassDescriptionButton({ + Key? key, + required this.room, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + return Column( + children: [ + ListTile( + onTap: room.canSendEvent(EventTypes.RoomTopic) + ? controller.setTopicAction + : null, + leading: room.canSendEvent(EventTypes.RoomTopic) + ? CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.topic_outlined), + ) + : null, + subtitle: Text( + room.topic.isEmpty + ? (room.isSpace + ? L10n.of(context)!.classDescriptionDesc + : L10n.of(context)!.chatTopicDesc) + : room.topic, + ), + title: Text( + room.isSpace + ? L10n.of(context)!.classDescription + : L10n.of(context)!.chatTopic, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart new file mode 100644 index 000000000..c03177494 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_details_toggle_add_students_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../../pages/chat_details/chat_details.dart'; + +class SpaceDetailsToggleAddStudentsTile extends StatelessWidget { + const SpaceDetailsToggleAddStudentsTile({ + Key? key, + required this.controller, + }) : super(key: key); + + final ChatDetailsController controller; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + L10n.of(context)!.addStudents, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon( + Icons.add, + ), + ), + trailing: Icon(controller.displayAddStudentOptions + ? Icons.keyboard_arrow_down_outlined + : Icons.keyboard_arrow_right_outlined), + onTap: controller.toggleAddStudentOptions, + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart new file mode 100644 index 000000000..5638828e8 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_invitation_buttons.dart @@ -0,0 +1,146 @@ +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; + +import '../../../../utils/fluffy_share.dart'; +import '../../../../widgets/avatar.dart'; + +class ClassInvitationButtons extends StatelessWidget { + final String roomId; + const ClassInvitationButtons({Key? key, required this.roomId}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final Room? room = Matrix.of(context).client.getRoomById(roomId); + if (room == null) return Text(L10n.of(context)!.oopsSomethingWentWrong); + + final copyClassLinkListTile = ListTile( + title: Text( + L10n.of(context)!.copyClassLink, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(L10n.of(context)!.copyClassLinkDesc), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon( + Icons.copy_outlined, + ), + ), + onTap: () { + final String initialUrl = + kIsWeb ? html.window.origin! : Environment.frontendURL; + FluffyShare.share( + "$initialUrl/#/join_with_link?${UrlQueryParameterKeys.classCode}=${room.classCode}", + context, + ); + }, + ); + + final copyCodeListTile = ListTile( + title: Text( + "${L10n.of(context)!.copyClassCode}: ${room.classCode}", + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(L10n.of(context)!.copyClassCodeDesc), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon( + Icons.copy, + ), + ), + onTap: () async { + //PTODO-Lala: Standarize toast + //PTODO - explore using Fluffyshare for this + await Clipboard.setData(ClipboardData(text: room.classCode)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)), + ); + }, + ); + + // final inviateWithEmailListTile = ListTile( + // enabled: false, + // //PTODO - add to copy + // title: const Text("Invite with email"), + // subtitle: const Text("Coming soon"), + // leading: CircleAvatar( + // backgroundColor: Theme.of(context).primaryColor, + // foregroundColor: Colors.white, + // radius: Avatar.defaultSize / 2, + // child: const Icon(Icons.email_outlined), + // ), + // //PTODO: Add invite with email functionality + // // onTap: () => VRouter.of(context).to('invite'), + // ); + + // final addFromGoogleClassooomListTile = ListTile( + // enabled: false, + // //PTODO - add to copy + // title: Text( + // L10n.of(context)!.addFromGoogleClassroom, + // style: TextStyle( + // color: Theme.of(context).colorScheme.secondary, + // fontWeight: FontWeight.bold, + // ), + // ), + // subtitle: Text(L10n.of(context)!.addFromGoogleClassroomDesc), + // leading: CircleAvatar( + // backgroundColor: Theme.of(context).primaryColor, + // foregroundColor: Colors.white, + // radius: Avatar.defaultSize / 2, + // child: SvgPicture.asset( + // "assets/pangea/google.svg", + // height: 20, + // width: 20, + // ), + // ), + // //PTODO: Add via google classroom functionality + // // onTap: () => VRouter.of(context).to('invite'), + // ); + + final inviteStudentByUserNameTile = ListTile( + title: Text( + L10n.of(context)!.inviteStudentByUserName, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(L10n.of(context)!.inviteStudentByUserNameDesc), + leading: CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + radius: Avatar.defaultSize / 2, + child: const Icon(Icons.add_outlined), + ), + onTap: () => context.go('/rooms/$roomId/invite'), + ); + + return Column( + children: [ + // inviteStudentByUserNameTile, + copyClassLinkListTile, + copyCodeListTile, + // inviateWithEmailListTile, + // addFromGoogleClassooomListTile, + ], + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_name_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_name_button.dart new file mode 100644 index 000000000..56362fd0e --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_name_button.dart @@ -0,0 +1,48 @@ +import 'package:fluffychat/pages/chat_details/chat_details.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class ClassNameButton extends StatelessWidget { + final Room room; + final ChatDetailsController controller; + const ClassNameButton({ + Key? key, + required this.room, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + return Column( + children: [ + ListTile( + onTap: controller.setDisplaynameAction, + title: Text( + room.isSpace + ? L10n.of(context)! + .changeTheNameOfTheClass + : L10n.of(context)! + .changeTheNameOfTheChat, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.people_outline_outlined), + ), + subtitle: Text( + room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_settings_button.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_settings_button.dart new file mode 100644 index 000000000..f2738c1ac --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_settings_button.dart @@ -0,0 +1,40 @@ +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({Key? key}) : super(key: key); + + // final PangeaController _pangeaController = MatrixState.pangeaController; + + @override + Widget build(BuildContext context) { + // final roomId = GoRouterState.of(context).pathParameters['roomid']; + + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + return Column( + children: [ + ListTile( + // enabled: roomId != null && + // _pangeaController.classController + // .getClassModelBySpaceIdLocal(roomId) != + // null, + title: Text( + L10n.of(context)!.classSettings, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(L10n.of(context)!.classSettingsDesc), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + child: const Icon(Icons.settings_outlined), + ), + onTap: () => context.go('/class_settings'), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_show_edit_dialog.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_show_edit_dialog.dart new file mode 100644 index 000000000..7752cae7a --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_show_edit_dialog.dart @@ -0,0 +1,34 @@ + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; + +void showEditFieldDialog(BuildContext context, String title) async { + // final room = Matrix.of(context).client.getRoomById(roomId!)!; + final input = await showTextInputDialog( + useRootNavigator: false, + context: context, + title: title, + okLabel: L10n.of(context)!.ok, + cancelLabel: L10n.of(context)!.cancel, + textFields: [ + DialogTextField( + hintText: title, + // initialText: room.topic, + minLines: 1, + maxLines: 4, + ) + ], + ); + if (input == null) return; + final success = await showFutureLoadingDialog( + context: context, + // TODO change this later + future: () async => null, + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged))); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/class_textfields_group.dart b/lib/pangea/pages/class_settings/p_class_widgets/class_textfields_group.dart new file mode 100644 index 000000000..a6a33d9c7 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/class_textfields_group.dart @@ -0,0 +1,38 @@ +// import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +// import 'package:flutter/cupertino.dart'; + +// import '../../../../pages/new_space/new_space.dart'; +// import '../../../widgets/common_widgets/p_input_field.dart'; + +// import 'package:flutter_gen/gen_l10n/l10n.dart'; + +// class ClassTextFieldGroup extends StatelessWidget { +// PangeaController controller; +// ClassTextFieldGroup({Key? key, required this.controller}) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return Column( +// children: [ +// PInputTextField( +// controller: controller.classController.cityController, +// hintText: L10n.of(context)!.enterCityName, +// labelText: L10n.of(context)!.optionalCity, +// onSubmit: (String value) {}, +// ), +// PInputTextField( +// controller: controller.classController.cityController, +// hintText: L10n.of(context)!.enterCountryName, +// labelText: L10n.of(context)!.optionalCountry, +// onSubmit: (String value) {}, +// ), +// PInputTextField( +// controller: controller.classController.cityController, +// hintText: L10n.of(context)!.enterSchoolName, +// labelText: L10n.of(context)!.optionalSchool, +// onSubmit: (String value) {}, +// ), +// ], +// ); +// } +// } diff --git a/lib/pangea/pages/class_settings/p_class_widgets/delete_class_tile.dart b/lib/pangea/pages/class_settings/p_class_widgets/delete_class_tile.dart new file mode 100644 index 000000000..44e23c1e9 --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/delete_class_tile.dart @@ -0,0 +1,147 @@ +import 'package:fluffychat/pangea/utils/delete_room.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:vrouter/vrouter.dart'; + +class DeleteSpaceTile extends StatelessWidget { + final Room room; + + const DeleteSpaceTile({ + Key? key, + required this.room, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + bool classNameMatch = true; + final textController = TextEditingController(); + Future deleteSpace() async { + final Client client = Matrix.of(context).client; + final GetSpaceHierarchyResponse spaceHierarchy = + await client.getSpaceHierarchy(room.id); + + if (spaceHierarchy.rooms.isNotEmpty) { + final List spaceChats = spaceHierarchy.rooms + .where((c) => c.roomId != room.id) + .map((e) => Matrix.of(context).client.getRoomById(e.roomId)) + .where((c) => c != null && !c.isSpace && !c.isDirectChat) + .cast() + .toList(); + + await Future.wait( + spaceChats.map((c) => deleteRoom(c.id, client)), + ); + } + deleteRoom(room.id, client); + VRouter.of(context).to('/rooms'); + return; + } + + Future deleteChat() { + VRouter.of(context).to('/rooms'); + return deleteRoom(room.id, Matrix.of(context).client); + } + + Future deleteChatAction() async { + showDialog( + context: context, + useRootNavigator: false, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + room.isSpace + ? L10n.of(context)!.areYouSureDeleteClass + : L10n.of(context)!.areYouSureDeleteGroup, + style: const TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + Text( + L10n.of(context)!.cannotBeReversed, + style: const TextStyle( + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + if (room.isSpace) + Text( + L10n.of(context)!.enterDeletedClassName, + style: const TextStyle( + fontSize: 14, + ), + textAlign: TextAlign.center, + ) + ], + ), + content: room.isSpace + ? TextField( + autofocus: true, + controller: textController, + decoration: InputDecoration( + hintText: room.name, + errorText: !classNameMatch + ? L10n.of(context)!.incorrectClassName + : null, + ), + ) + : null, + actions: [ + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () async { + if (room.isSpace) { + setState(() { + classNameMatch = textController.text == room.name; + }); + if (classNameMatch) { + Navigator.of(context).pop(); + await showFutureLoadingDialog( + context: context, + future: () => deleteSpace(), + ); + } + } else { + await showFutureLoadingDialog( + context: context, + future: () => deleteChat(), + ); + } + }, + ), + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + ); + }, + ); + }, + ); + } + + return ListTile( + trailing: const Icon(Icons.delete_outlined), + title: Text( + room.isSpace + ? L10n.of(context)!.deleteSpace + : L10n.of(context)!.deleteGroup, + style: const TextStyle(color: Colors.red), + ), + onTap: () => deleteChatAction(), + ); + } +} diff --git a/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart b/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart new file mode 100644 index 000000000..e9462a2cb --- /dev/null +++ b/lib/pangea/pages/class_settings/p_class_widgets/room_rules_editor.dart @@ -0,0 +1,243 @@ +import 'package:fluffychat/pangea/models/class_model.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 '../../../../config/app_config.dart'; +import '../../../../widgets/matrix.dart'; +import '../../../constants/pangea_event_types.dart'; +import '../../../extensions/pangea_room_extension.dart'; + +class RoomRulesEditor extends StatefulWidget { + final String? roomId; + final bool startOpen; + final bool showAdd; + + const RoomRulesEditor( + {Key? key, this.roomId, this.startOpen = true, this.showAdd = false}) + : super(key: key); + + @override + RoomRulesState createState() => RoomRulesState(); +} + +class RoomRulesState extends State { + Room? room; + late PangeaRoomRules rules; + late bool isOpen; + + RoomRulesState({ + Key? key, + }); + + @override + void initState() { + isOpen = widget.startOpen; + + room = widget.roomId != null + ? Matrix.of(context).client.getRoomById(widget.roomId!) + : null; + + rules = room?.pangeaRoomRules ?? PangeaRoomRules(); + + super.initState(); + } + + Future setRoomRules(String roomId) { + return Matrix.of(context).client.setRoomStateWithKey( + roomId, + PangeaEventTypes.rules, + '', + (rules).toJson(), + ); + } + + Future updatePermission(void Function() makeLocalRuleChange) async { + makeLocalRuleChange(); + if (room != null) { + await showFutureLoadingDialog( + context: context, + future: () => setRoomRules(room!.id), + ); + } + setState(() {}); + } + + // //function to handleShowAdd + // void handleShowAdd() { + // setState(() => isOpen = !isOpen); + // debugger(when: rules != null && kDebugMode); + + // rules = PangeaRoomRules(); + // } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // if (widget.showAdd) + // ListTile( + // title: Text( + // L10n.of(context)!.studentPermissions, + // style: TextStyle( + // color: Theme.of(context).colorScheme.secondary, + // fontWeight: FontWeight.bold, + // ), + // ), + // subtitle: Text(L10n.of(context)!.addRoomRulesDesc), + // leading: CircleAvatar( + // backgroundColor: Theme.of(context).primaryColor, + // foregroundColor: Colors.white, + // radius: Avatar.defaultSize / 2, + // child: Icon(rules == null ? Icons.add : Icons.remove), + // ), + // trailing: Icon( + // isOpen + // ? Icons.keyboard_arrow_down_outlined + // : Icons.keyboard_arrow_right_outlined, + // ), + // onTap: handleShowAdd, + // ), + + ListTile( + enableFeedback: !widget.startOpen, + title: Text( + L10n.of(context)!.studentPermissions, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(L10n.of(context)!.studentPermissionsDesc), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon( + Icons.settings_outlined, + ), + ), + trailing: !widget.startOpen + ? Icon( + isOpen + ? Icons.keyboard_arrow_down_outlined + : Icons.keyboard_arrow_right_outlined, + ) + : null, + onTap: () => + !widget.startOpen ? setState(() => isOpen = !isOpen) : null, + ), + if (isOpen) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: isOpen ? null : 0, + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 8.0), + child: Column( + children: [ + for (final setting in ToolSetting.values) + Column( + children: [ + ListTile( + title: Text( + "${setting.toolName( + context, + )}: ${rules.languageToolPermissionsText( + context, + setting, + )}", + ), + subtitle: Text(setting.toolDescription(context)), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 120.0), + child: Slider( + value: rules.getToolSettings(setting).toDouble(), + onChanged: (value) { + updatePermission(() { + rules.setLanguageToolSetting( + setting, value.toInt()); + }); + }, + divisions: 2, + max: 2, + min: 0, + label: rules.languageToolPermissionsText( + context, setting), + ), + ), + ], + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.oneToOneChatsWithinClass), + subtitle: + Text(L10n.of(context)!.oneToOneChatsWithinClassDesc), + value: rules.oneToOneChatClass, + onChanged: (value) => updatePermission( + () => rules.oneToOneChatClass = value, + ), + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.createGroupChats), + subtitle: Text(L10n.of(context)!.createGroupChatsDesc), + value: rules.isCreateRooms, + onChanged: (value) => updatePermission( + () => rules.isCreateRooms = value, + ), + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.shareVideo), + subtitle: Text(L10n.of(context)!.shareVideoDesc), + value: rules.isShareVideo, + onChanged: (value) => updatePermission( + () => rules.isShareVideo = value, + ), + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.sharePhotos), + subtitle: Text(L10n.of(context)!.sharePhotosDesc), + value: rules.isSharePhoto, + onChanged: (value) => updatePermission( + () => rules.isSharePhoto = value, + ), + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.shareFiles), + subtitle: Text(L10n.of(context)!.shareFilesDesc), + value: rules.isShareFiles, + onChanged: (value) => updatePermission( + () => rules.isShareFiles = value, + ), + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.sendVoiceNotes), + subtitle: Text(L10n.of(context)!.sendVoiceNotesDesc), + value: rules.isVoiceNotes, + onChanged: (value) => updatePermission( + () => rules.isVoiceNotes = value, + ), + ), + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.shareLocation), + subtitle: Text(L10n.of(context)!.shareLocationDesc), + value: rules.isShareLocation, + onChanged: (value) => updatePermission( + () => rules.isShareLocation = value, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/connect/p_sso_button.dart b/lib/pangea/pages/connect/p_sso_button.dart new file mode 100644 index 000000000..f97ea7f06 --- /dev/null +++ b/lib/pangea/pages/connect/p_sso_button.dart @@ -0,0 +1,84 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:matrix/matrix.dart'; + +class ButtonInfo { + String iconPath; + String text; + + ButtonInfo(this.iconPath, this.text); +} + +class PangeaSsoButton extends StatelessWidget { + final IdentityProvider identityProvider; + final void Function()? onPressed; + const PangeaSsoButton({ + Key? key, + required this.identityProvider, + this.onPressed, + }) : super(key: key); + + ButtonInfo getButtonInfo(BuildContext context) { + switch (identityProvider.id) { + case "oidc-google": + return ButtonInfo( + "assets/pangea/google.svg", + "${L10n.of(context)!.loginOrSignup} Google", + ); + case "oidc-apple": + return ButtonInfo( + "assets/pangea/apple.svg", + "${L10n.of(context)!.loginOrSignup} Apple", + ); + default: + return ButtonInfo( + "assets/pangea/pangea.svg", + "${L10n.of(context)!.loginOrSignup} Pangea Chat", + ); + } + } + + @override + Widget build(BuildContext context) { + final ButtonInfo buttonInfo = getButtonInfo(context); + return ElevatedButton( + onPressed: onPressed, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + identityProvider.icon == null + ? SvgPicture.asset( + buttonInfo.iconPath, + height: 20, + width: 20, + color: Theme.of(context).brightness == Brightness.light + ? AppConfig.primaryColor + : AppConfig.primaryColorLight, + ) + : Image.network( + Uri.parse(identityProvider.icon!) + .getDownloadLink(Matrix.of(context).getLoginClient()) + .toString(), + width: 32, + height: 32, + ), + // #Pangea + Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Text( + identityProvider.name != null + ? buttonInfo.text + : (identityProvider.brand != null + ? L10n.of(context)!.loginOrSignup + : L10n.of(context)!.loginOrSignup), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/exchange/add_exchange_to_class.dart b/lib/pangea/pages/exchange/add_exchange_to_class.dart new file mode 100644 index 000000000..711ae22ef --- /dev/null +++ b/lib/pangea/pages/exchange/add_exchange_to_class.dart @@ -0,0 +1,55 @@ +import 'package:fluffychat/pangea/widgets/class/add_class_and_invite.dart'; +import 'package:fluffychat/pangea/widgets/class/add_space_toggles.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +class AddExchangeToClass extends StatefulWidget { + const AddExchangeToClass({Key? key}) : super(key: key); + + @override + AddExchangeToClassState createState() => AddExchangeToClassState(); +} + +class AddExchangeToClassState extends State { + final GlobalKey addToSpaceKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final String spaceId = + GoRouterState.of(context).pathParameters['exchangeid']!; + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text(L10n.of(context)!.addToClassTitle), + ), + body: FutureBuilder( + future: + Matrix.of(context).client.waitForRoomInSync(spaceId, join: true), + builder: (context, snapshot) { + if (snapshot.hasData) { + return ListView( + children: [ + const SizedBox(height: 40), + AddToSpaceToggles( + roomId: + GoRouterState.of(context).pathParameters['exchangeid'], + key: addToSpaceKey, + startOpen: true, + mode: AddToClassMode.exchange, + ), + ], + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => context.go("/rooms"), + child: const Icon(Icons.arrow_forward_outlined), + ), + ); + } +} diff --git a/lib/pangea/pages/find_partner/find_partner.dart b/lib/pangea/pages/find_partner/find_partner.dart new file mode 100644 index 000000000..634e94285 --- /dev/null +++ b/lib/pangea/pages/find_partner/find_partner.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/models/user_model.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; + +import '../../../widgets/matrix.dart'; +import '../../controllers/pangea_controller.dart'; +import '../../models/user_profile_search_model.dart'; +import '../../repo/user_repo.dart'; +import 'find_partner_view.dart'; + +class FindPartner extends StatefulWidget { + const FindPartner({Key? key}) : super(key: key); + + @override + State createState() => FindPartnerController(); +} + +class FindPartnerController extends State { + PangeaController pangeaController = MatrixState.pangeaController; + + bool loading = false; + String currentSearchTerm = ""; + late LanguageModel targetLanguageSearch; + late LanguageModel sourceLanguageSearch; + String? countrySearch; + String? flagEmoji; + + //PTODO - implement pagination + String? previousPage; + + Timer? coolDown; + + final List _userProfilesCache = []; + + @override + void initState() { + targetLanguageSearch = pangeaController.languageController.userL1 ?? + pangeaController.pLanguageStore.targetOptions[1]; + sourceLanguageSearch = pangeaController.languageController.userL2 ?? + pangeaController.pLanguageStore.targetOptions[0]; + + searchUserProfiles(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return FindPartnerView(this); + } + + // List get userProfiles => currentSearchTerm.isNotEmpty + // ? _userProfilesCache + // .where((p) => + // (p.fullName != null && p.fullName!.contains(currentSearchTerm)) || + // (p.pangeaUserId != null && + // p.pangeaUserId!.contains(currentSearchTerm)) || + // (p.sourceLanguage != null && + // p.sourceLanguage!.contains(currentSearchTerm)) || + // // (p.speaks != null && + // // p.speaks!.any((e) => e.contains(currentSearchTerm))) || + // (p.country != null && p.country!.contains(currentSearchTerm)) || + // // (p.interests != null && + // // p.interests!.any((e) => e.contains(currentSearchTerm)))) + // .toList() + // : _userProfilesCache; + + List get userProfiles => _userProfilesCache.where((p) { + return (p.targetLanguage != null && + targetLanguageSearch.langCode == p.targetLanguage) && + (p.sourceLanguage != null && + sourceLanguageSearch.langCode == p.sourceLanguage) && + (countrySearch == null || + (p.country != null && countrySearch == p.country)); + }).toList(); + + void searchUserProfilesWithCoolDown(String text) { + coolDown?.cancel(); + coolDown = Timer( + const Duration(milliseconds: 0), + () => searchUserProfiles(), + ); + } + + void searchUserProfiles() async { + coolDown?.cancel(); + if (loading) return; + setState(() => loading = true); + + final UserProfileSearchResponse response = + await PUserRepo.searchUserProfiles( + accessToken: await pangeaController.userController.accessToken, + targetLanguage: targetLanguageSearch.langCode, + sourceLanguage: sourceLanguageSearch.langCode, + country: countrySearch, + limit: 30, + ); + for (final p in response.results) { + if (!_userProfilesCache + .any((element) => p.pangeaUserId == element.pangeaUserId)) { + _userProfilesCache.add(p); + } + } + + setState(() => loading = false); + } +} diff --git a/lib/pangea/pages/find_partner/find_partner_view.dart b/lib/pangea/pages/find_partner/find_partner_view.dart new file mode 100644 index 000000000..9845aa9e0 --- /dev/null +++ b/lib/pangea/pages/find_partner/find_partner_view.dart @@ -0,0 +1,298 @@ +import 'package:country_picker/country_picker.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/models/user_model.dart'; +import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_language_dropdown.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart' as matrix; + +import '../../../widgets/profile_bottom_sheet.dart'; +import 'find_partner.dart'; + +class FindPartnerView extends StatelessWidget { + final FindPartnerController controller; + const FindPartnerView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () => context.pop(), + ), + centerTitle: true, + title: const PageTitleText(), + ), + body: Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 2, + minWidth: FluffyThemes.columnWidth * 2, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + LanguageSelectionRow( + controller: controller, + isSource: true, + ), + LanguageSelectionRow( + controller: controller, + isSource: false, + ), + Padding( + padding: const EdgeInsets.all(18), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + L10n.of(context)!.iWantALanguagePartnerFrom, + style: const TextStyle(fontSize: 16), + ), + Row( + children: [ + Text( + controller.countrySearch ?? + L10n.of(context)!.worldWide, + ), + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + child: controller.flagEmoji != null + ? RichText( + text: TextSpan( + text: controller.flagEmoji, + style: const TextStyle(fontSize: 30), + ), + ) + : const PangeaLogoSvg(width: 30), + ), + IconButton( + icon: const Icon(Icons.expand_more), + onPressed: () => showCountryPicker( + showWorldWide: true, + context: context, + showPhoneCode: false, + onSelect: (Country country) { + if (country.name != "World Wide") { + controller.countrySearch = + country.displayNameNoCountryCode; + controller.flagEmoji = country.flagEmoji; + } else { + controller.countrySearch = null; + controller.flagEmoji = null; + } + controller.searchUserProfiles(); + }, + ), + ), + ], + ), + ], + ), + ), + controller.loading + ? const ExpandedContainer(body: ListPlaceholder()) + : controller.userProfiles.isNotEmpty + ? ExpandedContainer( + body: ListView.builder( + itemCount: controller.userProfiles.length, + itemBuilder: (context, i) => UserProfileEntry( + pangeaProfile: controller.userProfiles[i], + controller: controller, + ), + ), + ) + : ExpandedContainer( + body: Center( + child: Text(L10n.of(context)!.noResults), + ), + ), + ], + ), + ), + ), + ); + } +} + +class ExpandedContainer extends StatelessWidget { + const ExpandedContainer({ + Key? key, + required this.body, + }) : super(key: key); + + final Widget body; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + margin: const EdgeInsets.fromLTRB(0, 20, 0, 20), + child: body, + ), + ); + } +} + +class ProfileSearchTextField extends StatelessWidget { + const ProfileSearchTextField({ + Key? key, + required this.controller, + }) : super(key: key); + + final FindPartnerController controller; + + @override + Widget build(BuildContext context) { + return TextField( + autofocus: true, + decoration: InputDecoration( + hintText: L10n.of(context)!.searchBy, + suffixIconConstraints: const BoxConstraints( + maxWidth: 48, + maxHeight: 48, + minWidth: 48, + ), + suffixIcon: controller.loading + ? const CircularProgressIndicator.adaptive() + : const Icon(Icons.search_outlined), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + onChanged: controller.searchUserProfilesWithCoolDown, + ); + } +} + +class PageTitleText extends StatelessWidget { + const PageTitleText({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FittedBox( + child: Text( + L10n.of(context)!.iWantAConversationPartner, + style: TextStyle( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + ); + } +} + +class LanguageSelectionRow extends StatelessWidget { + const LanguageSelectionRow({ + Key? key, + required this.controller, + required this.isSource, + }) : super(key: key); + + final FindPartnerController controller; + final bool isSource; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + child: ListTile( + title: isSource + ? Text( + L10n.of(context)!.iWantALanguagePartnerWhoSpeaks, + style: const TextStyle(fontSize: 16), + ) + : Text( + L10n.of(context)!.iWantALanguagePartnerWhoIsLearning, + style: const TextStyle(fontSize: 16), + ), + ), + ), + Flexible( + child: PLanguageDropdown( + languages: isSource + ? controller.pangeaController.pLanguageStore.baseOptions + : controller.pangeaController.pLanguageStore.targetOptions, + onChange: (language) { + isSource + ? controller.sourceLanguageSearch = language + : controller.targetLanguageSearch = language; + controller.searchUserProfiles(); + }, + initialLanguage: isSource + ? controller.sourceLanguageSearch + : controller.targetLanguageSearch, + ), + ), + ], + ); + } +} + +class UserProfileEntry extends StatelessWidget { + final Profile pangeaProfile; + final FindPartnerController controller; + + const UserProfileEntry({ + Key? key, + required this.pangeaProfile, + required this.controller, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FutureBuilder( + future: Matrix.of(context) + .client + .getProfileFromUserId(pangeaProfile.pangeaUserId), + builder: ((context, snapshot) { + final matrixProfile = snapshot.data; + return ListTile( + leading: Avatar( + name: matrixProfile == null || matrixProfile.avatarUrl == null + ? pangeaProfile.pangeaUserId + : null, + mxContent: matrixProfile?.avatarUrl, + ), + title: Row( + children: [ + Text( + //PTODO - get matrix u and show displayName + matrixProfile?.displayName ?? pangeaProfile.pangeaUserId, + ), + const SizedBox(width: 20), + RichText( + text: TextSpan( + text: pangeaProfile.flagEmoji, + style: const TextStyle(fontSize: 15), + ), + ), + ], + ), + onTap: () => showModalBottomSheet( + context: context, + builder: (c) => ProfileBottomSheet( + userId: pangeaProfile.pangeaUserId, + outerContext: context, + ), + ), + ); + }), + ), + ], + ); + } +} diff --git a/lib/pangea/pages/new_class/new_class.dart b/lib/pangea/pages/new_class/new_class.dart new file mode 100644 index 000000000..18c676cb4 --- /dev/null +++ b/lib/pangea/pages/new_class/new_class.dart @@ -0,0 +1,121 @@ +import 'dart:developer'; + +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 '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 '../../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({Key? key}) : super(key: key); + + @override + NewClassController createState() => NewClassController(); +} + +class NewClassController extends State { + TextEditingController controller = TextEditingController(); + + final PangeaController pangeaController = MatrixState.pangeaController; + final GlobalKey rulesEditorKey = GlobalKey(); + final GlobalKey classSettingsKey = + GlobalKey(); + + void submitAction([_]) async { + //TODO: validate that object is complete + final matrix = Matrix.of(context); + if (controller.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.classNameRequired), + ), + ); + return; + } + if (classSettingsKey.currentState == null) { + debugger(when: kDebugMode); + } + if (classSettingsKey.currentState!.sameLanguages) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.noIdenticalLanguages), + ), + ); + return; + } + + final roomID = await showFutureLoadingDialog( + context: context, + future: () async { + final String roomID = await matrix.client.createRoom( + //PTODO - investigate effects of changing visibility from public + preset: sdk.CreateRoomPreset.publicChat, + creationContent: { + 'type': RoomCreationTypes.mSpace, + }, + visibility: sdk.Visibility.public, + // roomAliasName: controller.text.isNotEmpty + // ? "${matrix.client.userID!.localpart}-${controller.text.trim().toLowerCase().replaceAll(' ', '_')}" + // : null, + roomAliasName: ClassCodeUtil.generateClassCode(), + name: controller.text.isNotEmpty ? controller.text : null, + ); + + if (rulesEditorKey.currentState != null) { + await rulesEditorKey.currentState!.setRoomRules(roomID); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(m: "Null rules editor state"); + } + if (classSettingsKey.currentState != null) { + await classSettingsKey.currentState!.setClassSettings( + roomID, + ); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(m: "Null class settings state"); + } + return roomID; + }, + onError: (e) { + debugger(when: kDebugMode); + return e; + }, + ); + + if (roomID.error == null && roomID.result is String) { + pangeaController.classController.setActiveSpaceIdInChatListController( + roomID.result!, + ); + context.push('/spaces/${roomID.result!}'); + } else { + debugger(when: kDebugMode); + ErrorHandler.logError(e: roomID.error, s: StackTrace.current); + } + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => NewSpaceView(this); +} diff --git a/lib/pangea/pages/new_class/new_class_view.dart b/lib/pangea/pages/new_class/new_class_view.dart new file mode 100644 index 000000000..5249c191e --- /dev/null +++ b/lib/pangea/pages/new_class/new_class_view.dart @@ -0,0 +1,87 @@ +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 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.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, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + // #Pangea + centerTitle: true, + // Pangea# + title: Text(L10n.of(context)!.createNewClass), + ), + body: MaxWidthBody( + // #Pangea + child: ListView( + // child: Column( + // mainAxisSize: MainAxisSize.min, + // #Pangea + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: TextField( + // #Pangea + maxLength: ClassDefaultValues.maxClassName, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + // #Pangea + controller: controller.controller, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.go, + onSubmitted: controller.submitAction, + decoration: InputDecoration( + labelText: L10n.of(context)!.spaceName, + prefixIcon: const Icon(Icons.people_outlined), + hintText: L10n.of(context)!.enterASpacepName, + ), + ), + ), + // #Pangea + ClassSettings( + key: controller.classSettingsKey, + roomId: null, + startOpen: true, + ), + RoomRulesEditor( + key: controller.rulesEditorKey, + roomId: null, + ), + const SizedBox(height: 45), + // SwitchListTile.adaptive( + // title: Text(L10n.of(context)!.spaceIsPublic), + // value: controller.publicGroup, + // onChanged: controller.setPublicGroup, + // ), + // ListTile( + // trailing: const Padding( + // padding: EdgeInsets.symmetric(horizontal: 16.0), + // child: Icon(Icons.info_outlined), + // ), + // subtitle: Text(L10n.of(context)!.newSpaceDescription), + // ), + // #Pangea + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: controller.submitAction, + child: const Icon(Icons.arrow_forward_outlined), + ), + ); + } +} diff --git a/lib/pangea/pages/p_user_age/p_user_age.dart b/lib/pangea/pages/p_user_age/p_user_age.dart new file mode 100644 index 000000000..250528e79 --- /dev/null +++ b/lib/pangea/pages/p_user_age/p_user_age.dart @@ -0,0 +1,132 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/age_limits.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/pages/p_user_age/p_user_age_view.dart'; +import 'package:fluffychat/pangea/utils/p_extension.dart'; +import 'package:fluffychat/widgets/fluffy_chat_app.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:intl/intl.dart'; + +import '../../utils/bot_name.dart'; +import '../../utils/error_handler.dart'; + +class PUserAge extends StatefulWidget { + const PUserAge({Key? key}) : super(key: key); + + @override + PUserAgeController createState() => PUserAgeController(); +} + +class PUserAgeController extends State { + bool loading = false; + final GlobalKey formKey = GlobalKey(); + TextEditingController dobController = TextEditingController(); + // #Pangea + + String? error; + bool unknownErrorState = false; + // DateTime? dateOfBirth; + + final PangeaController pangeaController = MatrixState.pangeaController; + + @override + void initState() { + super.initState(); + Future.delayed( + Duration.zero, + () => Matrix.of(context) + .client + .startDirectChat( + BotName.byEnvironment, + enableEncryption: false, + ) + .onError( + (error, stackTrace) => + ErrorHandler.logError(e: error, s: stackTrace), + ), + ); + } + + String? dobFieldValidator(String? value) { + try { + if (value?.isEmpty ?? true) { + return L10n.of(context)!.yourBirthdayPleaseShort; + } + final DateTime dob = _textToDate!; + if (!dob.isAtLeastYearsOld(AgeLimits.toUseTheApp)) { + return L10n.of(context)!.mustBe13; + } + return null; + } catch (err, stack) { + ErrorHandler.logError(e: err, s: stack); + return L10n.of(context)!.invalidDob; + } + } + + DateTime? get _textToDate { + try { + final DateTime initial = DateFormat.yMd().parse(dobController.text); + return initial; + } catch (err) { + return null; + } + } + + DateTime get initialDate => + _textToDate ?? DateTime.now().subtract(const Duration(days: 13 * 365)); + + //Note: used linear progress bar (also used in fluffychat signup button) for consistency + createUserInPangea() async { + try { + setState(() { + error = null; + }); + if (!formKey.currentState!.validate()) return; + setState(() { + loading = true; + }); + + final String date = DateFormat('MM-dd-yyyy').format(_textToDate!); + + if (pangeaController.userController.userModel?.access == null) { + await pangeaController.userController.createPangeaUser(dob: date); + } else { + await pangeaController.userController.updateUserProfile( + dateOfBirth: date, + ); + } + // Matrix.of(context).widget.router!.currentState!.to( + // '/rooms', + // queryParameters: + // Matrix.of(context).widget.router!.currentState!.queryParameters, + // ); + FluffyChatApp.router.go('/rooms'); + } catch (err, s) { + setState(() { + unknownErrorState = true; + }); + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + } finally { + loading = false; + } + } + + @override + Widget build(BuildContext context) { + return !unknownErrorState + ? PUserAgeView(this) + : Center( + child: Padding( + padding: const EdgeInsets.all(50), + child: Text( + "${L10n.of(context)!.oopsSomethingWentWrong} \n ${L10n.of(context)!.errorPleaseRefresh}", + ), + ), + ); + } +} diff --git a/lib/pangea/pages/p_user_age/p_user_age_view.dart b/lib/pangea/pages/p_user_age/p_user_age_view.dart new file mode 100644 index 000000000..6ad1bbfb9 --- /dev/null +++ b/lib/pangea/pages/p_user_age/p_user_age_view.dart @@ -0,0 +1,107 @@ +import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import '../../../widgets/layouts/login_scaffold.dart'; + +class PUserAgeView extends StatelessWidget { + final PUserAgeController controller; + const PUserAgeView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + controller.dobController.text = ""; + return LoginScaffold( + appBar: AppBar( + automaticallyImplyLeading: !controller.loading, + ), + body: Form( + key: controller.formKey, + child: ListView( + children: [ + // #Pangea + Container( + margin: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.all(12), + child: Text( + L10n.of(context)!.yourBirthdayPlease, + textAlign: TextAlign.justify, + style: const TextStyle(color: Colors.white), + ), + ), + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + controller: controller.dobController, + keyboardType: TextInputType.datetime, + autofillHints: + controller.loading ? null : [AutofillHints.birthday], + validator: controller.dobFieldValidator, + onTap: () async { + final DateTime? pickedDate = await showDatePicker( + initialDatePickerMode: DatePickerMode.year, + context: context, + initialDate: controller.initialDate, + firstDate: DateTime(1940), + lastDate: DateTime.now(), + ); + + if (pickedDate != null) { + controller.dobController.text = + DateFormat.yMd().format(pickedDate); + controller.error = null; + } else { + controller.error = L10n.of(context)!.invalidDob; + } + }, + // onChanged: (String newValue) { + // try { + // controller.dateOfBirth = + // DateTime.parse(controller.dobController.text); + // controller.error = null; + // } catch (err) { + // controller.error = L10n.of(context)!.invalidDob; + // } + // }, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.calendar_today), + hintText: L10n.of(context)!.enterYourDob, + fillColor: Theme.of(context) + .colorScheme + .background + .withOpacity(0.75), + errorText: controller.error, + errorStyle: TextStyle( + color: controller.dobController.text.isEmpty + ? Colors.orangeAccent + : Colors.orange, + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + Hero( + tag: 'loginButton', + child: Padding( + padding: const EdgeInsets.all(12), + child: ElevatedButton( + onPressed: controller.createUserInPangea, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.getStarted), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/pages/settings_learning/settings_learning.dart b/lib/pangea/pages/settings_learning/settings_learning.dart new file mode 100644 index 000000000..965967095 --- /dev/null +++ b/lib/pangea/pages/settings_learning/settings_learning.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning_view.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +class SettingsLearning extends StatefulWidget { + const SettingsLearning({Key? key}) : super(key: key); + + @override + SettingsLearningController createState() => SettingsLearningController(); +} + +class SettingsLearningController extends State { + late StreamSubscription _userSubscription; + PangeaController pangeaController = MatrixState.pangeaController; + + setPublicProfile(bool b) async { + await pangeaController.userController.updateUserProfile(publicProfile: b); + setState(() {}); + } + + @override + void initState() { + super.initState(); + + _userSubscription = + pangeaController.userController.stateStream.listen((event) { + setState(() {}); + }); + } + + @override + void dispose() { + super.dispose(); + _userSubscription.cancel(); + } + + @override + Widget build(BuildContext context) { + return SettingsLearningView(this); + } +} diff --git a/lib/pangea/pages/settings_learning/settings_learning_view.dart b/lib/pangea/pages/settings_learning/settings_learning_view.dart new file mode 100644 index 000000000..59941237f --- /dev/null +++ b/lib/pangea/pages/settings_learning/settings_learning_view.dart @@ -0,0 +1,68 @@ +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/country_picker_tile.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/language_tile.dart'; +import 'package:fluffychat/pangea/widgets/user_settings/p_settings_switch_list_tile.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; + +import '../../../config/app_config.dart'; + +class SettingsLearningView extends StatelessWidget { + final SettingsLearningController controller; + const SettingsLearningView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + L10n.of(context)!.learningSettings, + ), + ), + body: ListTileTheme( + iconColor: Theme.of(context).textTheme.bodyLarge!.color, + child: MaxWidthBody( + withScrolling: true, + child: Column( + children: [ + LanguageTile(), + CountryPickerTile(), + const SizedBox(height: 8), + const Divider(height: 1), + const SizedBox(height: 8), + if (controller.pangeaController.permissionsController.isUser18()) + SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.publicProfileTitle), + subtitle: Text(L10n.of(context)!.publicProfileDesc), + value: controller.pangeaController.userController.isPublic, + onChanged: (bool isPublicProfile) => showFutureLoadingDialog( + context: context, + future: () => controller.setPublicProfile(isPublicProfile), + onError: (err) => + ErrorHandler.logError(e: err, s: StackTrace.current), + ), + ), + ListTile( + subtitle: Text(L10n.of(context)!.toggleToolSettingsDescription), + ), + for (final setting in ToolSetting.values) + PSettingsSwitchListTile.adaptive( + defaultValue: controller.pangeaController.localSettings + .userLanguageToolSetting(setting), + title: setting.toolName(context), + subtitle: setting.toolDescription(context), + pStoreKey: setting.toString(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/pages/settings_subscription/change_subscription.dart b/lib/pangea/pages/settings_subscription/change_subscription.dart new file mode 100644 index 000000000..d86d55a5e --- /dev/null +++ b/lib/pangea/pages/settings_subscription/change_subscription.dart @@ -0,0 +1,101 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart'; +import 'package:fluffychat/pangea/widgets/subscription/subscription_buttons.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class ChangeSubscription extends StatelessWidget { + final SubscriptionManagementController controller; + ChangeSubscription({ + required this.controller, + super.key, + }); + + final PangeaController pangeaController = MatrixState.pangeaController; + + @override + Widget build(BuildContext context) { + void submitChange({bool isPromo = false}) { + try { + pangeaController.subscriptionController.submitSubscriptionChange( + controller.selectedSubscription, + context, + isPromo: isPromo, + ); + } catch (err) { + showOkAlertDialog( + context: context, + title: L10n.of(context)!.oopsSomethingWentWrong, + message: L10n.of(context)!.errorPleaseRefresh, + okLabel: L10n.of(context)!.close, + ); + } + } + + return pangeaController.subscriptionController.subscription != null && + pangeaController.subscriptionController.subscription! + .availableSubscriptions.isNotEmpty + ? Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + L10n.of(context)!.selectYourPlan, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 16.0), + const Divider(height: 1), + SubscriptionButtons(controller: controller), + const SizedBox(height: 32), + if (controller.selectedSubscription != null) + IntrinsicWidth( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton( + onPressed: () => submitChange(), + child: Text(L10n.of(context)!.pay), + ), + const SizedBox(height: 10), + if (kIsWeb) + OutlinedButton( + onPressed: () => submitChange(isPromo: true), + child: Text(L10n.of(context)!.redeemPromoCode), + ), + ], + ), + ), + // if (controller.selectedSubscription != null && Platform.isIOS) + // TextButton( + // onPressed: () { + // try { + // pangeaController.subscriptionController + // .redeemPromoCode(context); + // } catch (err) { + // showOkAlertDialog( + // context: context, + // title: L10n.of(context)!.oopsSomethingWentWrong, + // message: L10n.of(context)!.errorPleaseRefresh, + // okLabel: L10n.of(context)!.close, + // ); + // } + // }, + // child: Text(L10n.of(context)!.redeemPromoCode), + // ) + ], + ) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context)!.oopsSomethingWentWrong), + Text(L10n.of(context)!.errorPleaseRefresh), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/settings_subscription/settings_subscription.dart b/lib/pangea/pages/settings_subscription/settings_subscription.dart new file mode 100644 index 000000000..037e9fb27 --- /dev/null +++ b/lib/pangea/pages/settings_subscription/settings_subscription.dart @@ -0,0 +1,125 @@ +import 'dart:async'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription_view.dart'; +import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class SubscriptionManagement extends StatefulWidget { + const SubscriptionManagement({Key? key}) : super(key: key); + + @override + SubscriptionManagementController createState() => + SubscriptionManagementController(); +} + +class SubscriptionManagementController extends State { + final PangeaController pangeaController = MatrixState.pangeaController; + SubscriptionDetails? selectedSubscription; + late StreamSubscription _settingsSubscription; + + @override + void initState() { + _settingsSubscription = + pangeaController.subscriptionController.stateStream.listen((event) { + debugPrint("stateStream event in subscription settings"); + setState(() {}); + }); + pangeaController.subscriptionController.updateCustomerInfo(); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _settingsSubscription.cancel(); + } + + bool get currentSubscriptionAvailable => + pangeaController.subscriptionController.currentSubscriptionAvailable; + + String? get purchasePlatformDisplayName => pangeaController + .subscriptionController.subscription?.purchasePlatformDisplayName; + + bool get currentSubscriptionIsPromotional => + pangeaController.subscriptionController.subscription + ?.currentSubscriptionIsPromotional ?? + false; + + bool get showManagementOptions { + if (!currentSubscriptionAvailable) { + return false; + } + if (pangeaController.subscriptionController.subscription!.purchasedOnWeb) { + return true; + } + return pangeaController.subscriptionController.subscription! + .currentPlatformMatchesPurchasePlatform; + } + + Future launchMangementUrl(ManagementOption option) async { + String managementUrl = Environment.stripeManagementUrl; + final String? email = await pangeaController.userController.userEmail; + if (email != null) { + managementUrl += "?prefilled_email=${Uri.encodeComponent(email)}"; + } + final String? purchaseAppId = pangeaController + .subscriptionController.subscription?.currentSubscription?.appId!; + if (purchaseAppId == null) return; + + final SubscriptionAppIds? appIds = + pangeaController.subscriptionController.subscription!.appIds; + + if (purchaseAppId == appIds?.stripeId) { + launchUrlString(managementUrl); + return; + } + if (purchaseAppId == appIds?.appleId) { + launchUrlString( + AppConfig.appleMangementUrl, + mode: LaunchMode.externalApplication, + ); + return; + } + switch (option) { + case ManagementOption.history: + launchUrlString( + AppConfig.googlePlayHistoryUrl, + mode: LaunchMode.externalApplication, + ); + break; + case ManagementOption.paymentMethod: + launchUrlString( + AppConfig.googlePlayPaymentMethodUrl, + mode: LaunchMode.externalApplication, + ); + break; + default: + launchUrlString( + AppConfig.googlePlayMangementUrl, + mode: LaunchMode.externalApplication, + ); + break; + } + } + + void selectSubscription(SubscriptionDetails? subscription) { + setState(() => selectedSubscription = subscription); + } + + @override + Widget build(BuildContext context) { + return SettingsSubscriptionView(this); + } +} + +enum ManagementOption { + cancel, + paymentMethod, + history, +} diff --git a/lib/pangea/pages/settings_subscription/settings_subscription_view.dart b/lib/pangea/pages/settings_subscription/settings_subscription_view.dart new file mode 100644 index 000000000..bcc9fb2a6 --- /dev/null +++ b/lib/pangea/pages/settings_subscription/settings_subscription_view.dart @@ -0,0 +1,148 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/pages/settings_subscription/change_subscription.dart'; +import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:intl/intl.dart'; + +class SettingsSubscriptionView extends StatelessWidget { + final SubscriptionManagementController controller; + final PangeaController pangeaController = MatrixState.pangeaController; + SettingsSubscriptionView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final String currentSubscriptionTitle = pangeaController + .subscriptionController.subscription?.currentSubscription + ?.displayName(context) ?? + ""; + final String currentSubscriptionPrice = pangeaController + .subscriptionController.subscription?.currentSubscription + ?.displayPrice(context) ?? + ""; + + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + L10n.of(context)!.subscriptionManagement, + ), + ), + body: ListTileTheme( + iconColor: Theme.of(context).textTheme.bodyLarge!.color, + child: MaxWidthBody( + child: !(pangeaController.subscriptionController.isSubscribed) + ? ChangeSubscription(controller: controller) + : Column( + children: [ + if (pangeaController.subscriptionController.subscription! + .currentSubscription != + null) + ListTile( + title: Text(L10n.of(context)!.currentSubscription), + subtitle: Text(currentSubscriptionTitle), + trailing: Text(currentSubscriptionPrice), + ), + Column( + children: [ + ListTile( + title: Text(L10n.of(context)!.cancelSubscription), + enabled: controller.showManagementOptions, + onTap: () => controller.launchMangementUrl( + ManagementOption.cancel, + ), + trailing: const Icon(Icons.cancel_outlined), + ), + const Divider(height: 1), + ListTile( + title: Text(L10n.of(context)!.paymentMethod), + trailing: const Icon(Icons.credit_card), + onTap: () => controller.launchMangementUrl( + ManagementOption.paymentMethod, + ), + enabled: controller.showManagementOptions, + ), + ListTile( + title: Text(L10n.of(context)!.paymentHistory), + trailing: + const Icon(Icons.keyboard_arrow_right_outlined), + onTap: () => controller.launchMangementUrl( + ManagementOption.history, + ), + enabled: controller.showManagementOptions, + ), + ], + ), + const SizedBox(height: 50), + if (!(controller.showManagementOptions)) + ManagementNotAvailableWarning( + controller: controller, + subscriptionController: + pangeaController.subscriptionController, + ) + ], + ), + ), + ), + ); + } +} + +class ManagementNotAvailableWarning extends StatelessWidget { + final SubscriptionManagementController controller; + final SubscriptionController subscriptionController; + + const ManagementNotAvailableWarning({ + required this.controller, + required this.subscriptionController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final bool currentSubscriptionAvailable = + controller.currentSubscriptionAvailable; + final bool currentSubscriptionIsPromotional = + controller.currentSubscriptionIsPromotional; + final String? purchasePlatformDisplayName = + controller.purchasePlatformDisplayName; + final bool isLifetimeSubscription = + subscriptionController.subscription?.isLifetimeSubscription ?? false; + final DateTime? expirationDate = + subscriptionController.subscription?.expirationDate; + + String warningText = L10n.of(context)!.subscriptionManagementUnavailable; + final DateFormat formatter = DateFormat('yyyy-MM-dd'); + + if (currentSubscriptionAvailable) { + warningText = L10n.of(context)!.subsciptionPlatformTooltip; + } else if (currentSubscriptionIsPromotional) { + if (isLifetimeSubscription) { + warningText = L10n.of(context)!.promotionalSubscriptionDesc; + } else { + warningText = L10n.of(context)!.promoSubscriptionExpirationDesc( + formatter.format(expirationDate!), + ); + } + } + + return Center( + child: Column( + children: [ + Text( + warningText, + textAlign: TextAlign.center, + ), + if (purchasePlatformDisplayName != null) + Text( + "${L10n.of(context)!.originalSubscriptionPlatform} $purchasePlatformDisplayName", + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/lib/pangea/pages/sign_up/signup.dart b/lib/pangea/pages/sign_up/signup.dart new file mode 100644 index 000000000..af6a515ba --- /dev/null +++ b/lib/pangea/pages/sign_up/signup.dart @@ -0,0 +1,154 @@ +import 'package:fluffychat/pangea/pages/sign_up/signup_view.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SignupPage extends StatefulWidget { + const SignupPage({Key? key}) : super(key: key); + + @override + SignupPageController createState() => SignupPageController(); +} + +class SignupPageController extends State { + final TextEditingController passwordController = TextEditingController(); + final TextEditingController password2Controller = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + String? error; + bool loading = false; + bool showPassword = false; + bool noEmailWarningConfirmed = false; + bool displaySecondPasswordField = false; + + static const int minPassLength = 8; + + void toggleShowPassword() => setState(() => showPassword = !showPassword); + + // String? get domain => VRouter.of(context).queryParameters['domain']; + + final GlobalKey formKey = GlobalKey(); + + void onPasswordType(String text) { + if (text.length >= minPassLength && !displaySecondPasswordField) { + setState(() { + displaySecondPasswordField = true; + }); + } + } + + String? password1TextFieldValidator(String? value) { + if (value!.isEmpty) { + return L10n.of(context)!.chooseAStrongPassword; + } + if (value.length < minPassLength) { + return L10n.of(context)! + .pleaseChooseAtLeastChars(minPassLength.toString()); + } + return null; + } + + String? password2TextFieldValidator(String? value) { + if (value!.isEmpty) { + return L10n.of(context)!.repeatPassword; + } + if (value != passwordController.text) { + return L10n.of(context)!.passwordsDoNotMatch; + } + return null; + } + + String? emailTextFieldValidator(String? value) { + if (value!.isEmpty && !noEmailWarningConfirmed) { + noEmailWarningConfirmed = true; + return L10n.of(context)!.noEmailWarning; + } + if (value.isNotEmpty && !value.contains('@')) { + return L10n.of(context)!.pleaseEnterValidEmail; + } + return null; + } + + // #Pangea + bool isTnCChecked = false; + String? signupError; + void onTncChange(bool? value) { + isTnCChecked = value ?? false; + signupError = null; + setState(() {}); + } + // #Pangea + + void signup([_]) async { + setState(() { + error = null; + }); + if (!formKey.currentState!.validate()) return; + // #Pangea + if (!isTnCChecked) { + setState(() { + signupError = 'Please agree to the Terms and Conditions'; + }); + return; + } + // #Pangea + setState(() { + loading = true; + }); + + try { + final client = Matrix.of(context).getLoginClient(); + final email = emailController.text; + if (email.isNotEmpty) { + Matrix.of(context).currentClientSecret = + DateTime.now().millisecondsSinceEpoch.toString(); + Matrix.of(context).currentThreepidCreds = + await client.requestTokenToRegisterEmail( + Matrix.of(context).currentClientSecret, + email, + 0, + ); + } + + final displayname = Matrix.of(context).loginUsername!; + final localPart = displayname.toLowerCase().replaceAll(' ', '_'); + + final registerRes = await client.uiaRequestBackground( + (auth) => client.register( + username: localPart, + password: passwordController.text, + initialDeviceDisplayName: PlatformInfos.clientName, + auth: auth, + ), + ); + + //@brord is this right?? + //#Pangea + GoogleAnalytics.login("pangea", registerRes.userId); + //Pangea# + + // Set displayname + if (displayname != localPart) { + await client.setDisplayName( + client.userID!, + displayname, + ); + } + } catch (e) { + //#Pangea + ErrorHandler.logError(e: e); + //Pangea# + error = (e).toLocalizedString(context); + } finally { + if (mounted) { + setState(() => loading = false); + } + } + } + + @override + Widget build(BuildContext context) => SignupPageView(this); +} diff --git a/lib/pangea/pages/sign_up/signup_view.dart b/lib/pangea/pages/sign_up/signup_view.dart new file mode 100644 index 000000000..65a6f1eaf --- /dev/null +++ b/lib/pangea/pages/sign_up/signup_view.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; +import 'package:fluffychat/pangea/widgets/signup/tos_checkbox.dart'; +import 'signup.dart'; + +class SignupPageView extends StatelessWidget { + final SignupPageController controller; + const SignupPageView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return LoginScaffold( + appBar: AppBar( + leading: controller.loading ? null : const BackButton(), + automaticallyImplyLeading: !controller.loading, + title: Text( + L10n.of(context)!.signUp, + // #Pangea + style: const TextStyle(color: Colors.white), + // #Pangea + ), + ), + body: Form( + key: controller.formKey, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + onChanged: controller.onPasswordType, + autofillHints: + controller.loading ? null : [AutofillHints.newPassword], + controller: controller.passwordController, + obscureText: !controller.showPassword, + validator: controller.password1TextFieldValidator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.vpn_key_outlined), + suffixIcon: IconButton( + tooltip: L10n.of(context)!.showPassword, + icon: Icon( + controller.showPassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: Colors.black, + ), + onPressed: controller.toggleShowPassword, + ), + errorStyle: const TextStyle(color: Colors.orange), + hintText: L10n.of(context)!.chooseAStrongPassword, + // #Pangea + fillColor: Theme.of(context) + .colorScheme + .background + .withOpacity(0.75), + // #Pangea + ), + ), + ), + if (controller.displaySecondPasswordField) + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + autofillHints: + controller.loading ? null : [AutofillHints.newPassword], + controller: controller.password2Controller, + obscureText: !controller.showPassword, + validator: controller.password2TextFieldValidator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.repeat_outlined), + hintText: L10n.of(context)!.repeatPassword, + errorStyle: const TextStyle(color: Colors.orange), + // #Pangea + fillColor: Theme.of(context) + .colorScheme + .background + .withOpacity(0.75), + // #Pangea + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + controller: controller.emailController, + keyboardType: TextInputType.emailAddress, + autofillHints: + controller.loading ? null : [AutofillHints.username], + validator: controller.emailTextFieldValidator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.mail_outlined), + hintText: L10n.of(context)!.enterAnEmailAddress, + errorText: controller.error, + errorMaxLines: 4, + // #Pangea + fillColor: Theme.of(context) + .colorScheme + .background + .withOpacity(0.75), + // #Pangea + errorStyle: TextStyle( + color: controller.emailController.text.isEmpty + ? Colors.orangeAccent + : Colors.orange, + ), + ), + ), + ), + // #Pangea + TosCheckbox(controller), + // #Pangea + Hero( + tag: 'loginButton', + child: Padding( + padding: const EdgeInsets.all(12), + // #Pangea + child: ElevatedButton( + onPressed: controller.loading ? () {} : controller.signup, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.signUp), + ), + // child: ElevatedButton.icon( + // icon: const Icon(Icons.person_add_outlined), + // style: ElevatedButton.styleFrom( + // foregroundColor: Theme.of(context).colorScheme.onPrimary, + // backgroundColor: Theme.of(context).colorScheme.primary, + // ), + // onPressed: controller.loading ? () {} : controller.signup, + // label: controller.loading + // ? const LinearProgressIndicator() + // : Text(L10n.of(context)!.signUp), + // ), + // #Pangea + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/repo/class_analytics_repo.dart b/lib/pangea/repo/class_analytics_repo.dart new file mode 100644 index 000000000..20152df7f --- /dev/null +++ b/lib/pangea/repo/class_analytics_repo.dart @@ -0,0 +1,74 @@ +// import 'dart:convert'; +// import 'dart:developer'; + +// import 'package:fluffychat/pangea/network/requests.dart'; +// import 'package:fluffychat/pangea/utils/analytics_util.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:http/http.dart'; + +// import '../../config/environment.dart'; +// import '../models/analytics_model_oldest.dart'; +// import '../network/urls.dart'; + +class PClassAnalyticsRepo { + /// deprecated in favor of new analytics + static Future repoGetAnalyticsByIds( + String accessToken, String timeSpan, + {List? classIds, + List? userIds, + List? chatIds}) async { + // if (!AnalyticsUtil.isValidSpan(timeSpan)) throw "Invalid span"; + + // final Requests req = Requests( + // accessToken: accessToken, choreoApiKey: Environment.choreoApiKey); + + // final body = {}; + // body["timespan"] = timeSpan; + // if (classIds != null) body["class_ids"] = classIds; + // if (chatIds != null) body["chat_ids"] = chatIds; + // if (userIds != null) body["user_ids"] = userIds; + + // final Response res = + // await req.post(url: PApiUrls.classAnalytics, body: body); + // final json = jsonDecode(res.body); + + // final Iterable? classJson = json["class_analytics"]; + // final Iterable? chatJson = json["chat_analytics"]; + // final Iterable? userJson = json["user_analytics"]; + + // final classInfo = classJson != null + // ? (classJson) + // .map((e) { + // e["timespan"] = timeSpan; + // return chartAnalytics(e); + // }) + // .toList() + // .cast() + // : []; + // final chatInfo = chatJson != null + // ? (chatJson) + // .map((e) { + // e["timespan"] = timeSpan; + // return chartAnalytics(e); + // }) + // .toList() + // .cast() + // : []; + // final userInfo = userJson != null + // ? (userJson) + // .map((e) { + // e["timespan"] = timeSpan; + // return chartAnalytics(e); + // }) + // .toList() + // .cast() + // : []; + + // final List allAnalytics = [ + // ...classInfo, + // ...chatInfo, + // ...userInfo + // ]; + // return allAnalytics; + } +} diff --git a/lib/pangea/repo/class_repo.dart b/lib/pangea/repo/class_repo.dart new file mode 100644 index 000000000..f6f576602 --- /dev/null +++ b/lib/pangea/repo/class_repo.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/models/class_email_invite_model.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import '../network/requests.dart'; +import '../network/urls.dart'; + +class PClassRepo { + static classesBySpaceIds(String accessToken, List spaceIds) async { + final Requests req = + Requests(baseUrl: PApiUrls.baseAPI, accessToken: accessToken); + final Response res = await req + .post(url: PApiUrls.classListBySpaceIds, body: {"class_ids": spaceIds}); + final json = jsonDecode(res.body); + final List pangeaClasses = json["results"] + .map((e) { + final ClassSettingsModel model = ClassSettingsModel.fromJson(e); + return model; + }) + .toList() + .cast(); + return pangeaClasses; + } + + //Question for Jordan - what happens if code is incorrect? statuscode 400? + // what about if user is already in the class? + //Question for Lala: In this widget, controller, repo framework, where are + // errors handled? How are they passed? + static Future getClassByCode( + String classCode, String accessToken) async { + final Requests req = + Requests(baseUrl: PApiUrls.baseAPI, accessToken: accessToken); + final Response res = + await req.get(url: PApiUrls.getClassByClassCode + classCode); + + if (res.statusCode == 400) { + return null; + } + final json = jsonDecode(res.body); + + final classSettings = ClassSettingsModel.fromJson(json); + + return classSettings; + } + + static searchClass(String text) async {} + + static sendEmailToJoinClass(List data, String roomId, + String teacherName) async {} + + static inviteAction(BuildContext context, String id, String roomId) async {} + + static reportUser({ + String? classRoomNamedata, + String? classTeacherNamedata, + String? reportedUserdata, + String? classTeacherEmaildata, + String? offensivedata, + String? reasondata, + }) async {} +} diff --git a/lib/pangea/repo/contextualized_translation_repo.dart b/lib/pangea/repo/contextualized_translation_repo.dart new file mode 100644 index 000000000..9d2f608dd --- /dev/null +++ b/lib/pangea/repo/contextualized_translation_repo.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:http/http.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../config/environment.dart'; +import '../models/pangea_token_model.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class ContextualizationTranslationRepo { + //Question for Jordan - is this for an individual token or could it be a span? + static Future translate({ + required String accessToken, + required ContextualTranslationRequestModel request, + }) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.contextualizedTranslation, + body: request.toJson(), + ); + + final ContextTranslationResponseModel response = + ContextTranslationResponseModel.fromJson( + jsonDecode( + utf8.decode(res.bodyBytes).toString(), + ), + ); + + if (response.translations.isEmpty) { + ErrorHandler.logError( + e: Exception( + "empty translations in contextual translation response return", + ), + ); + } + + return response; + } +} + +class ContextualTranslationRequestModel { + String fullText; + String srcLangCode; + String tgtLangCode; + String userL1; + String userL2; + PangeaTokenText span; + + ContextualTranslationRequestModel({ + required this.fullText, + required this.srcLangCode, + required this.tgtLangCode, + required this.span, + required this.userL1, + required this.userL2, + }); + + static const String _spanKey = "span"; + + Map toJson() => { + ModelKey.fullText: fullText, + ModelKey.srcLang: srcLangCode, + ModelKey.tgtLang: tgtLangCode, + _spanKey: span.toJson(), + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, + }; +} + +class ContextTranslationResponseModel { + List translations; + + ContextTranslationResponseModel({required this.translations}); + + static const _translationsKey = "translation"; + + factory ContextTranslationResponseModel.fromJson( + Map json, + ) { + final List trans = json[_translationsKey] is List + ? (json[_translationsKey] as List).map((e) => e.toString()).toList() + : json[_translationsKey] != null + ? [ + json[_translationsKey], + ] + : []; + + if (trans.isEmpty) { + Sentry.addBreadcrumb( + Breadcrumb( + message: "ContextTranslationResponseModel with empty translations", + data: {"response": json}), + ); + } + + return ContextTranslationResponseModel( + translations: trans, + ); + } +} diff --git a/lib/pangea/repo/exchange_repo.dart b/lib/pangea/repo/exchange_repo.dart new file mode 100644 index 000000000..97bd9ed74 --- /dev/null +++ b/lib/pangea/repo/exchange_repo.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class PExchangeRepo { + static fetchExchangeClassInfo(String exchangePangeaId) async {} + + static saveExchangeRecord( + String requestFromClass, + String requestToClass, + String requestTeacher, + String requestToClassAuthor, + String exchangePangeaId) async {} + + static exchangeRejectRequest(String roomId, String teacherName) async {} + + static validateExchange({ + required String requestFromClass, + required String requestToClass, + required BuildContext context, + }) async {} + + static createExchangeRequest({ + required String roomId, + required String teacherID, + required String toClass, + required BuildContext context, + }) async {} + + static isExchange( + BuildContext context, String accessToken, String exchangeId) async {} +} diff --git a/lib/pangea/repo/full_text_translation_repo.dart b/lib/pangea/repo/full_text_translation_repo.dart new file mode 100644 index 000000000..a9a13bc87 --- /dev/null +++ b/lib/pangea/repo/full_text_translation_repo.dart @@ -0,0 +1,90 @@ +//Question for Jordan - is this for an individual token or could it be a span? + +import 'dart:convert'; + +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../constants/model_keys.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class FullTextTranslationRepo { + static Future translate({ + required String accessToken, + required FullTextTranslationRequestModel request, + }) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.simpleTranslation, + body: request.toJson(), + ); + + return FullTextTranslationResponseModel.fromJson( + jsonDecode(utf8.decode(res.bodyBytes)), + ); + } +} + +class FullTextTranslationRequestModel { + String text; + String? srcLang; + String tgtLang; + String userL1; + String userL2; + bool? deepL; + + FullTextTranslationRequestModel({ + required this.text, + this.srcLang, + required this.tgtLang, + required this.userL2, + required this.userL1, + this.deepL = false, + }); + + //PTODO throw error for null + + Map toJson() => { + "text": text, + ModelKey.srcLang: srcLang, + ModelKey.tgtLang: tgtLang, + ModelKey.userL2: userL2, + ModelKey.userL1: userL1, + ModelKey.deepL: deepL + }; +} + +class FullTextTranslationResponseModel { + List translations; + + /// detected source + /// PTODO - + String source; + String? deepL; + + FullTextTranslationResponseModel({ + required this.translations, + required this.source, + required this.deepL, + }); + + factory FullTextTranslationResponseModel.fromJson(Map json) { + return FullTextTranslationResponseModel( + translations: (json["translations"] as Iterable) + .map( + (e) => e, + ) + .toList() + .cast(), + source: json[ModelKey.srcLang], + deepL: json['deepl_res'], + ); + } + + String get bestTranslation => deepL ?? translations.first; +} diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart new file mode 100644 index 000000000..712510899 --- /dev/null +++ b/lib/pangea/repo/igc_repo.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/models/language_detection_model.dart'; +import 'package:fluffychat/pangea/models/lemma.dart'; +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/repo/span_data_repo.dart'; +import 'package:http/http.dart'; + +import '../constants/model_keys.dart'; +import '../models/igc_text_data_model.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class IgcRepo { + static Future getIGC(String? accessToken, + {required IGCRequestBody igcRequest}) async { + final Requests req = Requests( + accessToken: accessToken, + choreoApiKey: Environment.choreoApiKey, + ); + final Response res = await req.post( + url: PApiUrls.igcLite, + body: igcRequest.toJson(), + ); + + final Map json = + jsonDecode(utf8.decode(res.bodyBytes).toString()); + + final IGCTextData response = IGCTextData.fromJson(json); + + return response; + } + + static Future getMockData() async { + await Future.delayed(const Duration(seconds: 2)); + + final IGCTextData igcTextData = IGCTextData( + detections: [LanguageDetection(langCode: "en")], + tokens: [ + PangeaToken( + text: PangeaTokenText(content: "This", offset: 0, length: 4), + hasInfo: true, + lemmas: [Lemma(form: "This", text: "this", saveVocab: true)], + ), + PangeaToken( + text: PangeaTokenText(content: "be", offset: 5, length: 2), + hasInfo: true, + lemmas: [Lemma(form: "be", text: "be", saveVocab: true)], + ), + PangeaToken( + text: PangeaTokenText(content: "a", offset: 8, length: 1), + hasInfo: false, + lemmas: []), + PangeaToken( + text: PangeaTokenText(content: "sample", offset: 10, length: 6), + hasInfo: false, + lemmas: []), + PangeaToken( + text: PangeaTokenText(content: "text", offset: 17, length: 4), + hasInfo: false, + lemmas: []), + ], + matches: [ + PangeaMatch( + match: spanDataRepomockSpan, status: PangeaMatchStatus.open), + ], + originalInput: "This be a sample text", + fullTextCorrection: "This is a sample text", + userL1: "es", + userL2: "en", + enableIT: true, + enableIGC: true, + ); + + return igcTextData; + } +} + +class IGCRequestBody { + String fullText; + String userL1; + String userL2; + bool tokensOnly; + bool enableIT; + bool enableIGC; + + IGCRequestBody({ + required this.fullText, + required this.userL1, + required this.userL2, + required this.enableIGC, + required this.enableIT, + this.tokensOnly = false, + }); + + Map toJson() => { + ModelKey.fullText: fullText, + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, + "enable_it": enableIT, + "enable_igc": enableIGC, + "tokens_only": tokensOnly, + }; +} diff --git a/lib/pangea/repo/interactive_translation_repo.dart b/lib/pangea/repo/interactive_translation_repo.dart new file mode 100644 index 000000000..debb5c776 --- /dev/null +++ b/lib/pangea/repo/interactive_translation_repo.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:http/http.dart'; + +import '../models/custom_input_translation_model.dart'; +import '../models/it_response_model.dart'; +import '../models/system_choice_translation_model.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class ITRepo { + static Future customInputTranslate( + CustomInputRequestModel initalText) async { + final Requests req = Requests( + baseUrl: PApiUrls.choreoBaseApi, + choreoApiKey: Environment.choreoApiKey); + final Response res = + await req.post(url: PApiUrls.firstStep, body: initalText.toJson()); + + final json = jsonDecode(utf8.decode(res.bodyBytes).toString()); + + return ITResponseModel.fromJson(json); + } + + static Future systemChoiceTranslate( + SystemChoiceRequestModel subseqText) async { + final Requests req = Requests( + baseUrl: PApiUrls.choreoBaseApi, + choreoApiKey: Environment.choreoApiKey); + + final Response res = + await req.post(url: PApiUrls.subseqStep, body: subseqText.toJson()); + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes).toString()); + + return ITResponseModel.fromJson(decodedBody); + } +} diff --git a/lib/pangea/repo/language_repo.dart b/lib/pangea/repo/language_repo.dart new file mode 100644 index 000000000..fb564ac48 --- /dev/null +++ b/lib/pangea/repo/language_repo.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../network/requests.dart'; + +class LanguageRepo { + static Future> fetchLanguages() async { + final Requests req = Requests(baseUrl: Environment.baseAPI); + final Response res = await req.get(url: PApiUrls.getLanguages); + + final decodedBody = + jsonDecode(utf8.decode(res.bodyBytes).toString()) as List; + final List langFlag = decodedBody.map((e) { + try { + return LanguageModel.fromJson(e); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack, data: e); + return LanguageModel.unknown; + } + }).toList(); + return langFlag; + } +} diff --git a/lib/pangea/repo/message_service.repo.dart b/lib/pangea/repo/message_service.repo.dart new file mode 100644 index 000000000..49288de5e --- /dev/null +++ b/lib/pangea/repo/message_service.repo.dart @@ -0,0 +1,52 @@ +import '../config/environment.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class MessageServiceRepo { + static Future sendPayloads( + MessageServiceModel serviceModel, String messageId) async { + final Requests req = Requests( + baseUrl: PApiUrls.choreoBaseApi, + choreoApiKey: Environment.choreoApiKey); + + final json = serviceModel.toJson(); + json["msg_id"] = messageId; + + await req.post(url: PApiUrls.messageService, body: json); + } +} + +class MessageServiceModel { + List payloadIds; + String? messageId; + String message; + String userId; + String roomId; + String? classId; + String? l1Lang; + String l2Lang; + + MessageServiceModel({ + required this.payloadIds, + required this.messageId, + required this.message, + required this.userId, + required this.roomId, + required this.classId, + required this.l1Lang, + required this.l2Lang, + }); + + toJson() { + return { + 'payload_ids': payloadIds, + 'msg_id': messageId, + 'message': message, + 'user_id': userId, + 'room_id': roomId, + 'class_id': classId, + 'l1_lang': l1Lang, + 'l2_lang': l2Lang, + }; + } +} diff --git a/lib/pangea/repo/similarity_repo.dart b/lib/pangea/repo/similarity_repo.dart new file mode 100644 index 000000000..1dd21d5bd --- /dev/null +++ b/lib/pangea/repo/similarity_repo.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:http/http.dart'; + +import '../network/requests.dart'; +import '../network/urls.dart'; + +class SimilarityRepo { + static Future get({ + required String accessToken, + required SimilarityRequestModel request, + }) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.similarity, + body: request.toJson(), + ); + + final SimilartyResponseModel response = SimilartyResponseModel.fromJson( + jsonDecode( + utf8.decode(res.bodyBytes).toString(), + ), + ); + + return response; + } +} + +class SimilarityRequestModel { + String benchmark; + List toCompare; + + SimilarityRequestModel({required this.benchmark, required this.toCompare}); + + Map toJson() => { + "original": benchmark, + "to_compare": toCompare, + }; +} + +class SimilartyResponseModel { + String benchmark; + List scores; + + SimilartyResponseModel({required this.benchmark, required this.scores}); + + factory SimilartyResponseModel.fromJson( + Map json, + ) => + SimilartyResponseModel( + benchmark: json["original"], + scores: List.from( + json["scores"].map( + (x) => SimilarityScore.fromJson(x), + ), + ), + ); + + SimilarityScore get highestScore { + SimilarityScore highest = scores.first; + for (final SimilarityScore score in scores) { + if (score.score > highest.score) { + highest = score; + } + } + return highest; + } + + bool userTranslationIsDifferentButBetter(String userTranslation) { + return highestScore.text == userTranslation; + } + + bool userTranslationIsSameAsBotTranslation(String userTranslation) { + return highestScore.text == userTranslation && + scores.where((e) => e.text == userTranslation).length == 2; + } + + num userScore(String userTranslation) { + return scores.firstWhere((e) => e.text == userTranslation).score; + } +} + +class SimilarityScore { + String text; + double score; + int index; + + SimilarityScore({ + required this.text, + required this.score, + required this.index, + }); + + factory SimilarityScore.fromJson(Map json) { + return SimilarityScore( + text: json["text"], + score: json["score"], + index: json["index"], + ); + } +} diff --git a/lib/pangea/repo/span_data_repo.dart b/lib/pangea/repo/span_data_repo.dart new file mode 100644 index 000000000..df20b0580 --- /dev/null +++ b/lib/pangea/repo/span_data_repo.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/enum/span_choice_type.dart'; +import 'package:fluffychat/pangea/enum/span_data_type.dart'; +import 'package:fluffychat/pangea/models/span_data.dart'; +import 'package:http/http.dart'; + +import '../constants/model_keys.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class SpanDataRepo { + static Future getSpanDetails(String? accessToken, + {required SpanDetailsRepoReqAndRes request}) async { + final Requests req = Requests( + accessToken: accessToken, + choreoApiKey: Environment.choreoApiKey, + ); + final Response res = await req.post( + url: PApiUrls.spanDetails, + body: request.toJson(), + ); + + final Map json = + jsonDecode(utf8.decode(res.bodyBytes).toString()); + + return SpanDetailsRepoReqAndRes.fromJson(json); + } +} + +Future getMock(SpanDetailsRepoReqAndRes req) async { + await Future.delayed(const Duration(seconds: 2)); + if (req.span.choices != null && + req.span.choices!.any((element) => element.selected)) { + return req..span = mockReponseWithHintOne.span; + } else { + return req..span = mockReponseWithChoices.span; + } +} + +class SpanDetailsRepoReqAndRes { + String userL1; + String userL2; + bool enableIT; + bool enableIGC; + SpanData span; + + SpanDetailsRepoReqAndRes({ + required this.userL1, + required this.userL2, + required this.enableIGC, + required this.enableIT, + required this.span, + }); + + Map toJson() => { + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, + "enable_it": enableIT, + "enable_igc": enableIGC, + 'span': span.toJson(), + }; + + factory SpanDetailsRepoReqAndRes.fromJson(Map json) => + SpanDetailsRepoReqAndRes( + userL1: json['user_l1'] as String, + userL2: json['user_l2'] as String, + enableIT: json['enable_it'] as bool, + enableIGC: json['enable_igc'] as bool, + span: SpanData.fromJson(json['span']), + ); +} + +final spanDataRepomockSpan = SpanData( + offset: 5, + length: 2, + fullText: "This be a sample text", + type: SpanDataType(typeName: SpanDataTypeEnum.correction), + context: null, + choices: [SpanChoice(value: "is", type: SpanChoiceType.bestCorrection)], + message: null, + rule: null, + shortMessage: null, +); + +//json mock request +final mockRequest = SpanDetailsRepoReqAndRes( + userL1: "es", + userL2: "en", + enableIGC: true, + enableIT: true, + span: spanDataRepomockSpan, +); + +SpanDetailsRepoReqAndRes get mockReponseWithChoices { + final SpanDetailsRepoReqAndRes res = mockRequest; + res.span.choices = [ + SpanChoice(value: "is", type: SpanChoiceType.bestCorrection), + SpanChoice(value: "are", type: SpanChoiceType.distractor), + SpanChoice(value: "was", type: SpanChoiceType.distractor), + ]; + return res; +} + +SpanDetailsRepoReqAndRes get mockReponseWithHintOne { + final SpanDetailsRepoReqAndRes res = mockReponseWithChoices; + res.span.choices![1].selected = true; + res.span.message = "Conjugation error"; + return res; +} diff --git a/lib/pangea/repo/subscription_repo.dart b/lib/pangea/repo/subscription_repo.dart new file mode 100644 index 000000000..92c7ee532 --- /dev/null +++ b/lib/pangea/repo/subscription_repo.dart @@ -0,0 +1,220 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/subscription_app_id.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +import '../network/urls.dart'; + +class SubscriptionRepo { + static final Map requestHeaders = { + 'Content-type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer ${Environment.rcKey}' + }; + + static Future getAppIds() async { + try { + final http.Response res = await http.get( + Uri.parse(PApiUrls.rcApps), + headers: SubscriptionRepo.requestHeaders, + ); + final json = jsonDecode(res.body); + return SubscriptionAppIds.fromJson(json); + } catch (err) { + ErrorHandler.logError( + m: "Failed to fetch app information for revenuecat API", + s: StackTrace.current, + ); + return null; + } + } + + static Future?> getAllProducts() async { + try { + final http.Response res = await http.get( + Uri.parse(PApiUrls.rcProducts), + headers: SubscriptionRepo.requestHeaders, + ); + final Map json = jsonDecode(res.body); + final RCProductsResponseModel resp = + RCProductsResponseModel.fromJson(json); + return resp.allProducts; + } catch (err) { + ErrorHandler.logError( + m: "Failed to fetch entitlement information for revenuecat API", + s: StackTrace.current, + ); + return null; + } + } + + static Future getCurrentSubscriptionInfo( + String? userId, + List? allProducts, + ) async { + final Map stripeHeaders = { + 'Content-type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer ${Environment.rcStripeKey}' + }; + final String url = "${PApiUrls.rcSubscribers}/$userId"; + final http.Response res = await http.get( + Uri.parse(url), + headers: stripeHeaders, + ); + final Map json = jsonDecode(res.body); + final RCSubscriptionResponseModel resp = + RCSubscriptionResponseModel.fromJson( + json, + allProducts, + ); + return resp; + } +} + +class RCProductsResponseModel { + List allProducts; + + RCProductsResponseModel({ + required this.allProducts, + }); + + factory RCProductsResponseModel.fromJson( + Map json, + ) { + final List offerings = json["items"] as List; + final offering = offerings.firstWhereOrNull( + Environment.isStaging + ? (offering) => !(offering['is_current'] as bool) + : (offering) => offering['is_current'] as bool, + ); + final Map metadata = offering['metadata']; + + final List allProducts = []; + for (final packageDetails in offering['packages']['items']) { + final String packageId = packageDetails['id']; + final List products = + RCProductsResponseModel.productsFromPackageDetails( + packageDetails, + packageId, + metadata, + ); + allProducts.addAll(products); + } + + return RCProductsResponseModel(allProducts: allProducts); + } + + static List productsFromPackageDetails( + Map packageDetails, + String packageId, + Map metadata, + ) { + return packageDetails['products']['items'] + .map((productDetails) => SubscriptionDetails( + price: double.parse(metadata['$packageId.price']), + duration: metadata['$packageId.duration'], + id: productDetails['product']['store_identifier'], + appId: productDetails['product']['app_id'], + )) + .toList() + .cast(); + } +} + +class RCSubscriptionResponseModel { + String? currentSubscriptionId; + SubscriptionDetails? currentSubscription; + DateTime? expirationDate; + List? allEntitlements; + + RCSubscriptionResponseModel({ + this.currentSubscriptionId, + this.currentSubscription, + this.allEntitlements, + this.expirationDate, + }); + + factory RCSubscriptionResponseModel.fromJson( + Map json, + List? allProducts, + ) { + final List activeEntitlements = + RCSubscriptionResponseModel.getActiveEntitlements(json); + + final List allEntitlements = + RCSubscriptionResponseModel.getAllEntitlements(json); + + if (activeEntitlements.length > 1) { + debugPrint( + "User has more than one active entitlement. This shouldn't happen", + ); + } + if (activeEntitlements.isEmpty) { + debugPrint("User has no active entitlements"); + return RCSubscriptionResponseModel(); + } + + final String currentSubscriptionId = activeEntitlements[0]; + + final Map currentSubscriptionMetadata = + json['subscriber']['subscriptions'][currentSubscriptionId]; + + final DateTime expirationDate = DateTime.parse( + currentSubscriptionMetadata['expires_date'], + ); + + final String currentSubscriptionPeriodType = + currentSubscriptionMetadata['period_type'] ?? ""; + + final SubscriptionDetails? currentSubscription = + allProducts?.firstWhereOrNull( + (SubscriptionDetails sub) => + sub.id.contains(currentSubscriptionId) || + currentSubscriptionId.contains(sub.id), + ); + + if (currentSubscriptionPeriodType == "trial") { + currentSubscription?.makeTrial(); + } + + return RCSubscriptionResponseModel( + currentSubscription: currentSubscription, + currentSubscriptionId: currentSubscriptionId, + expirationDate: expirationDate, + allEntitlements: activeEntitlements, + ); + } + + static List getActiveEntitlements(Map json) { + return json['subscriber']['entitlements'] + .entries + .where( + (MapEntry entitlement) => DateTime.parse( + entitlement.value['expires_date'], + ).isAfter(DateTime.now()), + ) + .map( + (MapEntry entitlement) => + entitlement.value['product_identifier'], + ) + .cast() + .toList(); + } + + static List getAllEntitlements(Map json) { + return json['subscriber']['entitlements'] + .entries + .map( + (MapEntry entitlement) => + entitlement.value['product_identifier'], + ) + .cast() + .toList(); + } +} diff --git a/lib/pangea/repo/tokens_repo.dart b/lib/pangea/repo/tokens_repo.dart new file mode 100644 index 000000000..7b6c2ba60 --- /dev/null +++ b/lib/pangea/repo/tokens_repo.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../models/pangea_token_model.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class TokensRepo { + static Future tokenize( + String accessToken, + TokensRequestModel request, + ) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.tokenize, + body: request.toJson(), + ); + + final TokensResponseModel response = TokensResponseModel.fromJson( + jsonDecode( + utf8.decode(res.bodyBytes).toString(), + ), + ); + + if (response.tokens.isEmpty) { + ErrorHandler.logError( + e: Exception( + "empty tokens in tokenize response return", + ), + ); + } + + return response; + } +} + +class TokensRequestModel { + String fullText; + String userL1; + String userL2; + + TokensRequestModel({ + required this.fullText, + required this.userL1, + required this.userL2, + }); + + Map toJson() => { + ModelKey.fullText: fullText, + ModelKey.userL1: userL1, + ModelKey.userL2: userL2 + }; +} + +class TokensResponseModel { + List tokens; + String lang; + + TokensResponseModel({required this.tokens, required this.lang}); + + factory TokensResponseModel.fromJson( + Map json, + ) => + TokensResponseModel( + tokens: (json[ModelKey.tokens] as Iterable) + .map( + (e) => PangeaToken.fromJson(e as Map), + ) + .toList() + .cast(), + lang: json[ModelKey.lang]); +} diff --git a/lib/pangea/repo/topic_data_repo.dart b/lib/pangea/repo/topic_data_repo.dart new file mode 100644 index 000000000..e9f7b4d89 --- /dev/null +++ b/lib/pangea/repo/topic_data_repo.dart @@ -0,0 +1,70 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../models/chat_topic_model.dart'; +import '../models/lemma.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +/// accepts ChatTopic and calls an API for a list of Lemma +class TopicDataRepo { + static Future generate(String? accessToken, + {required TopicDataRequest request}) async { + final Requests req = Requests( + accessToken: accessToken, + choreoApiKey: Environment.choreoApiKey, + ); + final Response res = await req.post( + url: PApiUrls.topicInfo, + body: request.toJson(), + ); + + return TopicDataResponse.fromJson(jsonDecode(res.body)).topicInfo; + } + + /// gets list of ChatTopic from assets/chat_data.json + static Future> getTopics(String langCode) async { + final String data = await rootBundle.loadString("assets/chat_data.json"); + final jsonResult = json.decode(data); + final List topics = []; + for (final topic in jsonResult['chats']) { + topics.add(ChatTopic.fromJson(topic)); + } + return topics; + } +} + +class TopicDataResponse { + final ChatTopic topicInfo; + + TopicDataResponse({required this.topicInfo}); + + factory TopicDataResponse.fromJson(Map json) { + return TopicDataResponse( + topicInfo: ChatTopic.fromJson(json['topic_info']), + ); + } +} + +class TopicDataRequest { + final ChatTopic topicInfo; + final int numWords; + final int numPrompts; + + TopicDataRequest({ + required this.topicInfo, + required this.numWords, + required this.numPrompts, + }); + + Map toJson() { + return { + 'topic_info': topicInfo.toJson(), + 'num_words': numWords, + 'num_prompts': numPrompts, + }; + } +} diff --git a/lib/pangea/repo/user_repo.dart b/lib/pangea/repo/user_repo.dart new file mode 100644 index 000000000..3f0ad0d9b --- /dev/null +++ b/lib/pangea/repo/user_repo.dart @@ -0,0 +1,124 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:http/http.dart'; + +import '../../widgets/matrix.dart'; +import '../models/user_model.dart'; +import '../models/user_profile_search_model.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class PUserRepo { + static Future repoCreatePangeaUser({ + required String userID, + required String dateOfBirth, + required fullName, + required String matrixAccessToken, + }) async { + final Requests req = Requests( + baseUrl: PApiUrls.baseAPI, + matrixAccessToken: matrixAccessToken, + ); + + final Map body = { + ModelKey.userFullName: fullName, + ModelKey.userPangeaUserId: userID, + ModelKey.userDateOfBirth: dateOfBirth + }; + final Response res = await req.post( + url: PApiUrls.createUser, + body: body, + ); + return PUserModel.fromJson(jsonDecode(res.body)); + } + + static Future fetchPangeaUserInfo({ + required String userID, + required String matrixAccessToken, + }) async { + Response res; + try { + final Requests req = Requests( + baseUrl: PApiUrls.baseAPI, + matrixAccessToken: matrixAccessToken, + ); + res = await req.get( + url: PApiUrls.userDetails, + objectId: userID, + ); + + return PUserModel.fromJson(jsonDecode(res.body)); + } catch (err) { + //status code should be 400 - PTODO - check ffor this. + log("Most likely a first signup and needs to make an account"); + return null; + } + } + + //notes for jordan - only replace non-null fields, return whole profile + //Jordan - should return pangeaUserId as well + static Future updateUserProfile( + Profile userProfile, + String accessToken, + ) async { + final Requests req = Requests( + baseUrl: PApiUrls.baseAPI, + accessToken: accessToken, + ); + final Response res = await req.put( + url: PApiUrls.updateUserProfile, + body: userProfile.toJson(), + ); + + //temp fix + final content = jsonDecode(res.body); + //PTODO - try taking this out and see where bug occurs + if (content[ModelKey.userPangeaUserId] == null) { + content[ModelKey.userPangeaUserId] = + MatrixState.pangeaController.matrixState.client.userID; + } + + return Profile.fromJson(content); + } + + static Future searchUserProfiles({ + // List? interests, + String? targetLanguage, + String? sourceLanguage, + String? country, + // String? speaks, + String? pageNumber, + required String accessToken, + required int limit, + }) async { + final Requests req = Requests( + baseUrl: PApiUrls.baseAPI, + accessToken: accessToken, + ); + final Map body = {}; + // if (interests != null) body[ModelKey.userInterests] = interests.toString(); + if (targetLanguage != null) { + body[ModelKey.userTargetLanguage] = targetLanguage; + } + if (sourceLanguage != null) { + body[ModelKey.userSourceLanguage] = sourceLanguage; + } + if (country != null) body[ModelKey.userCountry] = country; + // if (speaks != null) body[ModelKey.userSpeaks] = speaks; + if (pageNumber != null) { + body["page_number"] = pageNumber; + } + body["limit"] = limit; + + final Response res = await req.post( + url: PApiUrls.searchUserProfiles, + body: body, + ); + + //PTODO - implement paginiation - make another call with next url + + return UserProfileSearchResponse.fromJson(jsonDecode(res.body)); + } +} diff --git a/lib/pangea/repo/word_repo.dart b/lib/pangea/repo/word_repo.dart new file mode 100644 index 000000000..362c3b643 --- /dev/null +++ b/lib/pangea/repo/word_repo.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:http/http.dart'; + +import '../constants/model_keys.dart'; +import '../models/word_data_model.dart'; +import '../network/requests.dart'; +import '../network/urls.dart'; + +class WordRepo { + static Future getWordNetData({ + required String accessToken, + required String fullText, + required String word, + required String userL1, + required String userL2, + }) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, accessToken: accessToken); + final Response res = await req.post(url: PApiUrls.wordNet, body: { + ModelKey.word: word, + ModelKey.fullText: fullText, + ModelKey.userL1: userL1, + ModelKey.userL2: userL2, + }); + + final json = jsonDecode(utf8.decode(res.bodyBytes)); + + final WordData wordData = WordData.fromJson( + json, + fullText: fullText, + word: word, + userL1: userL1, + userL2: userL2, + ); + + return wordData; + } +} diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart new file mode 100644 index 000000000..a804ef96a --- /dev/null +++ b/lib/pangea/utils/any_state_holder.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../models/widget_measurement.dart'; + +class PangeaAnyState { + final Map?> _streams = {}; + final Map> _pastValues = {}; + final Map _layerLinkAndKeys = {}; + OverlayEntry? overlay; + + dispose() { + closeOverlay(); + _layerLinkAndKeys.clear(); + } + + LayerLinkAndKey layerLinkAndKey(String transformTargetId, + [throwErrorIfNotThere = false]) { + if (_layerLinkAndKeys[transformTargetId] == null) { + if (throwErrorIfNotThere) { + Sentry.addBreadcrumb(Breadcrumb.fromJson(_layerLinkAndKeys)); + throw Exception("layerLinkAndKey with null for $transformTargetId"); + } else { + _layerLinkAndKeys[transformTargetId] = + LayerLinkAndKey(transformTargetId); + } + } + + return _layerLinkAndKeys[transformTargetId]!; + } + + void disposeByWidgetKey(String transformTargetId) { + _layerLinkAndKeys.remove(transformTargetId); + } + + void closeOverlay() { + if (overlay != null) { + overlay!.remove(); + overlay = null; + } + } + + LayerLinkAndKey messageLinkAndKey(String eventId) => layerLinkAndKey(eventId); + + // String chatViewTargetKey(String? roomId) => "chatViewKey$roomId"; + // LayerLinkAndKey chatViewLinkAndKey(String? roomId) => + // layerLinkAndKey(chatViewTargetKey(roomId)); +} + +class LayerLinkAndKey { + late LabeledGlobalKey key; + late LayerLink link; + String transformTargetId; + + LayerLinkAndKey(this.transformTargetId) { + key = LabeledGlobalKey(transformTargetId); + link = LayerLink(); + } + + Map toJson() => { + "key": key.toString(), + "link": link.toString(), + "transformTargetId": transformTargetId, + }; +} diff --git a/lib/pangea/utils/archive_space.dart b/lib/pangea/utils/archive_space.dart new file mode 100644 index 000000000..3cefe54a5 --- /dev/null +++ b/lib/pangea/utils/archive_space.dart @@ -0,0 +1,21 @@ +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:matrix/matrix.dart'; + +Future archiveSpace(Room? space, Client client) async { + if (space == null) { + ErrorHandler.logError( + e: 'Tried to archive a space that is null. This should not happen.', + s: StackTrace.current, + ); + return; + } + + final List children = + space.spaceChildren.map((e) => e.roomId).where((e) => e != null).toList(); + for (final String? child in children) { + final Room? room = client.getRoomById(child!); + if (room == null) continue; + await room.leave(); + } + await space.leave(); +} diff --git a/lib/pangea/utils/bot_name.dart b/lib/pangea/utils/bot_name.dart new file mode 100644 index 000000000..dc7c0da8f --- /dev/null +++ b/lib/pangea/utils/bot_name.dart @@ -0,0 +1,7 @@ +import 'package:fluffychat/pangea/config/environment.dart'; + +class BotName { + static String get byEnvironment => + Environment.isStaging ? "@bot:staging.pangea.chat" : "@bot:pangea.chat"; + static String get localBot => "@matrix-bot-test:staging.pangea.chat"; +} diff --git a/lib/pangea/utils/bot_style.dart b/lib/pangea/utils/bot_style.dart new file mode 100644 index 000000000..86afcd6e2 --- /dev/null +++ b/lib/pangea/utils/bot_style.dart @@ -0,0 +1,35 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/material.dart'; + +class BotStyle { + static TextStyle text( + BuildContext context, { + TextStyle? existingStyle, + bool setColor = true, + bool big = false, + bool italics = false, + bool bold = true, + }) { + try { + final TextStyle botStyle = TextStyle( + fontFamily: 'Inconsolata', + fontWeight: bold ? FontWeight.w700 : null, + fontSize: AppConfig.messageFontSize * + AppConfig.fontSizeFactor * + (big == true ? 1.2 : 1), + fontStyle: italics ? FontStyle.italic : null, + color: setColor + ? Theme.of(context).brightness == Brightness.dark + ? AppConfig.primaryColorLight + : AppConfig.primaryColor + : null, + ); + + return existingStyle?.merge(botStyle) ?? botStyle; + } catch (err, stack) { + ErrorHandler.logError(m: "error getting styles", s: stack); + return existingStyle ?? const TextStyle(); + } + } +} diff --git a/lib/pangea/utils/chat_list_handle_space_tap.dart b/lib/pangea/utils/chat_list_handle_space_tap.dart new file mode 100644 index 000000000..6979aac12 --- /dev/null +++ b/lib/pangea/utils/chat_list_handle_space_tap.dart @@ -0,0 +1,141 @@ +import 'package:fluffychat/pages/chat_list/chat_list.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:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'error_handler.dart'; + +// ignore: curly_braces_in_flow_control_structures +void chatListHandleSpaceTap( + BuildContext context, + ChatListController controller, + Room space, +) { + void setActiveSpaceAndCloseChat() { + controller.setActiveSpace(space.id); + if (controller.activeChat != null && + !space.isFirstOrSecondChild(controller.activeChat!)) { + context.go("/rooms"); + } + } + + void autoJoin(Room space) { + showFutureLoadingDialog( + context: context, + future: () async { + await space.join(); + await space.postLoad(); + setActiveSpaceAndCloseChat(); + }, + onError: (exception) { + ErrorHandler.logError(e: exception); + return exception.toString(); + }, + ); + } + + //show alert dialog prompting user to accept invite or reject + //if accepted, setActiveSpaceAndCloseChat() + //if rejected, leave space + // use standard alert diolog, not cupertino + void showAlertDialog(BuildContext context) { + // set up the AlertDialog + final AlertDialog alert = AlertDialog( + title: Text(L10n.of(context)!.youreInvited), + content: Text( + space.isSpace + ? L10n.of(context)! + .invitedToClassOrExchange(space.name, space.creatorId ?? "???") + : L10n.of(context)! + .invitedToChat(space.name, space.creatorId ?? "???"), + ), + actions: [ + TextButton( + onPressed: () => showFutureLoadingDialog( + context: context, + future: () async { + await space.leave(); + //show snackbar message that you've left + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.declinedInvitation), + duration: const Duration(seconds: 3), + ), + ); + Navigator.of(context).pop(); + }, + onError: (exception) { + ErrorHandler.logError(e: exception); + Navigator.of(context).pop(); + return exception.toString(); + }, + ), + child: Text(L10n.of(context)!.decline), + ), + TextButton( + onPressed: () => showFutureLoadingDialog( + context: context, + future: () async { + await space.join(); + setActiveSpaceAndCloseChat(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.acceptedInvitation), + duration: const Duration(seconds: 3), + ), + ); + context.go( + '/rooms/join_exchange/${controller.activeSpaceId}', + ); + }, + onError: (exception) { + ErrorHandler.logError(e: exception); + Navigator.of(context).pop(); + return exception.toString(); + }, + ), + child: Text(L10n.of(context)!.accept), + ), + ], + ); + + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); + } + + switch (space.membership) { + case Membership.join: + setActiveSpaceAndCloseChat(); + break; + case Membership.invite: + //if space is a child of a space you're in, automatically join + //else confirm you want to join + //can we tell whether space or chat? + final rooms = Matrix.of(context).client.rooms.where( + (element) => + element.isSpace && element.membership == Membership.join, + ); + if (rooms.any((s) => s.spaceChildren.any((c) => c.roomId == space.id))) { + autoJoin(space); + } else { + showAlertDialog(context); + } + break; + default: + setActiveSpaceAndCloseChat(); + ErrorHandler.logError( + m: 'should not show space with membership ${space.membership}', + data: space.toJson(), + ); + break; + } +} diff --git a/lib/pangea/utils/class_chat_power_levels.dart b/lib/pangea/utils/class_chat_power_levels.dart new file mode 100644 index 000000000..73dfecf65 --- /dev/null +++ b/lib/pangea/utils/class_chat_power_levels.dart @@ -0,0 +1,36 @@ +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import '../constants/class_default_values.dart'; +import '../extensions/pangea_room_extension.dart'; + +class ClassChatPowerLevels { + static Future> powerLevelOverrideForClassChat( + BuildContext context, List spaceParents) async { + final Client client = Matrix.of(context).client; + final Map powerLevelOverride = {}; + powerLevelOverride['events'] = { + EventTypes.spaceChild: 0, + PangeaEventTypes.studentAnalyticsSummary: 0, + }; + powerLevelOverride['users'] = {}; + + final List spaceAdmin = []; + for (final classRoom in spaceParents) { + final List classTeachers = await classRoom.teachers; + spaceAdmin.addAll(classTeachers); + } + + for (final admin in spaceAdmin) { + powerLevelOverride['users'][admin.id] = + ClassDefaultValues.powerLevelOfAdmin; + } + + powerLevelOverride['users'][client.userID] = + ClassDefaultValues.powerLevelOfAdmin; + + return powerLevelOverride; + } +} diff --git a/lib/pangea/utils/class_code.dart b/lib/pangea/utils/class_code.dart new file mode 100644 index 000000000..c41bc9cd8 --- /dev/null +++ b/lib/pangea/utils/class_code.dart @@ -0,0 +1,107 @@ +import 'dart:math'; + +import 'package:fluffychat/pangea/utils/error_handler.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 '../controllers/pangea_controller.dart'; + +class ClassCodeUtil { + static const codeLength = 6; + + static bool isValidCode(String? classcode) { + return classcode == null || classcode.length > 4; + } + + static String generateClassCode() { + final r = Random(); + const chars = 'AaBbCcDdEeFfGgHhiJjKkLMmNnoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + return List.generate(codeLength, (index) => chars[r.nextInt(chars.length)]) + .join(); + } + + static void joinWithClassCodeDialog( + BuildContext outerContext, + PangeaController pangeaController, + String? classCode, + ) { + final TextEditingController textFieldController = TextEditingController( + text: classCode, + ); + + showDialog( + context: outerContext, + useRootNavigator: false, + builder: (BuildContext context) => Scaffold( + backgroundColor: Colors.transparent, + body: AlertDialog( + title: Text(L10n.of(context)!.joinWithClassCode), + content: TextField( + controller: textFieldController, + decoration: InputDecoration( + hintText: L10n.of(context)!.joinWithClassCodeHint, + ), + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () => showFutureLoadingDialog( + context: context, + future: () async { + try { + await pangeaController.classController.joinClasswithCode( + outerContext, + textFieldController.text, + ); + } catch (err) { + messageSnack( + outerContext, + ErrorCopy(outerContext, err).body, + ); + } finally { + context.go("/rooms"); + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ), + ), + ); + } + + static messageDialog( + BuildContext context, + String title, + void Function()? action, + ) => + showDialog( + context: context, + useRootNavigator: false, + builder: (context) => AlertDialog( + content: Text(title), + actions: [ + TextButton( + onPressed: action, + child: Text(L10n.of(context)!.ok), + ), + ], + ), + ); + + static void messageSnack(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 10), + content: Text(message), + ), + ); + } +} diff --git a/lib/pangea/utils/custom_exception.dart b/lib/pangea/utils/custom_exception.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/lib/pangea/utils/custom_exception.dart @@ -0,0 +1 @@ + diff --git a/lib/pangea/utils/delete_room.dart b/lib/pangea/utils/delete_room.dart new file mode 100644 index 000000000..2558e4c48 --- /dev/null +++ b/lib/pangea/utils/delete_room.dart @@ -0,0 +1,89 @@ +import 'package:fluffychat/pangea/constants/class_default_values.dart'; +import 'package:matrix/matrix.dart'; + +import 'error_handler.dart'; + +Future deleteRoom(String? roomID, Client client) async { + if (roomID == null) { + ErrorHandler.logError( + m: "in deleteRoomAction with null pangeaClassRoomID", + s: StackTrace.current, + ); + return; + } + + final Room? room = client.getRoomById(roomID); + if (room == null) { + ErrorHandler.logError( + m: "failed to fetch room with roomID $roomID", + s: StackTrace.current, + ); + return; + } + + try { + await room.join(); + } catch (err) { + ErrorHandler.logError( + m: "failed to join room with roomID $roomID", + s: StackTrace.current, + ); + return; + } + + List members; + try { + members = await room.requestParticipants(); + } catch (err) { + ErrorHandler.logError( + m: "failed to fetch members for room with roomID $roomID", + s: StackTrace.current, + ); + return; + } + + final List otherAdmins = []; + for (final User member in members) { + final String memberID = member.id; + final int memberPowerLevel = room.getPowerLevelByUserId(memberID); + if (memberID == client.userID) continue; + if (memberPowerLevel >= ClassDefaultValues.powerLevelOfAdmin) { + otherAdmins.add(member); + continue; + } + try { + await room.kick(memberID); + } catch (err) { + ErrorHandler.logError( + m: "Failed to kick user $memberID from room with id $roomID. Error: $err", + s: StackTrace.current, + ); + continue; + } + } + + if (otherAdmins.isNotEmpty && room.canSendEvent(EventTypes.RoomJoinRules)) { + try { + await client.setRoomStateWithKey( + roomID, + EventTypes.RoomJoinRules, + "", + {"join_rules": "invite"}, + ); + } catch (err) { + ErrorHandler.logError( + m: "Failed to update student create room permissions. error: $err, roomId: $roomID", + s: StackTrace.current, + ); + } + } + + try { + await room.leave(); + } catch (err) { + ErrorHandler.logError( + m: "Failed to leave room with id $roomID. Error: $err", + s: StackTrace.current, + ); + } +} diff --git a/lib/pangea/utils/download_chat.dart b/lib/pangea/utils/download_chat.dart new file mode 100644 index 000000000..38a34b139 --- /dev/null +++ b/lib/pangea/utils/download_chat.dart @@ -0,0 +1,355 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:csv/csv.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:intl/intl.dart'; +import 'package:matrix/matrix.dart'; +import 'package:matrix/src/models/timeline_chunk.dart'; +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:syncfusion_flutter_xlsio/xlsio.dart'; +import 'package:universal_html/html.dart' as webFile; + +import '../models/choreo_record.dart'; + +enum DownloadType { txt, csv, xlsx } + +Future downloadChat(Room room, ClassSettingsModel classSettings, + DownloadType type, Client client, BuildContext context) async { + List allPangeaMessages; + + try { + final List allEvents = await getAllEvents(room, client); + final TimelineChunk chunk = TimelineChunk(events: allEvents); + final Timeline timeline = Timeline( + room: room, + chunk: chunk, + ); + + allPangeaMessages = getPangeaMessageEvents( + allEvents, + timeline, + room, + classSettings.targetLanguage, + ); + } catch (err) { + ErrorHandler.logError( + e: Exception( + "Failed to fetch messages for chat ${room.id} in while downloading chat"), + s: StackTrace.current, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "${L10n.of(context)!.oopsSomethingWentWrong} ${L10n.of(context)!.errorPleaseRefresh}", + ), + ), + ); + return; + } + + if (allPangeaMessages.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.emptyChatDownloadWarning, + ), + ), + ); + return; + } + + final String filename = getFilename(room, type); + + switch (type) { + case DownloadType.txt: + final String content = + getTxtContent(allPangeaMessages, context, filename, room); + downloadFile(content, filename, DownloadType.txt); + break; + case DownloadType.csv: + final String content = + getCSVContent(allPangeaMessages, context, filename); + downloadFile(content, filename, DownloadType.csv); + return; + case DownloadType.xlsx: + final List content = + getExcelContent(allPangeaMessages, context, filename); + downloadFile(content, filename, DownloadType.xlsx); + return; + } +} + +Future> getAllEvents(Room room, Client client) async { + final GetRoomEventsResponse initalResp = + await client.getRoomEvents(room.id, Direction.b); + if (initalResp.end == null) return []; + String? nextStartToken = initalResp.end; + List allMatrixEvents = initalResp.chunk; + while (nextStartToken != null) { + final GetRoomEventsResponse resp = await client.getRoomEvents( + room.id, + Direction.b, + from: nextStartToken, + ); + final chunkMessages = resp.chunk; + allMatrixEvents.addAll(chunkMessages); + resp.end != nextStartToken + ? nextStartToken = resp.end + : nextStartToken = null; + } + allMatrixEvents = allMatrixEvents.reversed.toList(); + final List allEvents = allMatrixEvents + .map((MatrixEvent message) => Event.fromMatrixEvent(message, room)) + .toList(); + return allEvents; +} + +List getPangeaMessageEvents( + List events, Timeline timeline, Room room, String? targetLang) { + final List allPangeaMessages = events + .where( + (Event event) => + event.type == EventTypes.Message && + event.content['msgtype'] == MessageTypes.Text, + ) + .map( + (Event message) => PangeaMessageEvent( + event: message, + timeline: timeline, + ownMessage: false, + selected: false, + ), + ) + .cast() + .toList(); + return allPangeaMessages; +} + +String getOriginalText(PangeaMessageEvent message) { + try { + final List? steps = + message.originalSent?.choreo?.choreoSteps; + if (steps != null && steps.isNotEmpty) return steps.first.text; + if (message.originalWritten != null) return message.originalWritten!.text; + if (message.originalSent != null) return message.originalSent!.text; + return message.body; + } catch (err) { + return message.body; + } +} + +String getSentText(PangeaMessageEvent message) => + message.originalSent?.text ?? message.body; + +bool usageIsAvailable(PangeaMessageEvent message) { + try { + return message.originalSent?.choreo != null; + } catch (err) { + return false; + } +} + +String getFilename(Room room, DownloadType type) { + final String roomName = room + .getLocalizedDisplayname() + .trim() + .replaceAll(RegExp(r'[^A-Za-z0-9\s]'), "") + .replaceAll(RegExp(r'\s+'), "-"); + final String timestamp = + DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now()); + final String extension = type == DownloadType.txt + ? 'txt' + : type == DownloadType.csv + ? 'csv' + : 'xlsx'; + return "$roomName-$timestamp.$extension"; +} + +String mimetype(DownloadType fileType) { + switch (fileType) { + case DownloadType.txt: + return 'text/plain'; + case DownloadType.csv: + return 'text/csv'; + case DownloadType.xlsx: + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } +} + +Future downloadFile( + contents, String filename, DownloadType fileType) async { + if (kIsWeb) { + final blob = webFile.Blob([contents], mimetype(fileType), 'native'); + webFile.AnchorElement( + href: webFile.Url.createObjectUrlFromBlob(blob).toString(), + ) + ..setAttribute("download", filename) + ..click(); + return; + } + if (await Permission.storage.request().isGranted) { + Directory? directory; + try { + if (Platform.isIOS) { + directory = await getApplicationDocumentsDirectory(); + } else { + directory = Directory('/storage/emulated/0/Download'); + if (!await directory.exists()) { + directory = await getExternalStorageDirectory(); + } + } + } catch (err) { + debugPrint("Failed to get download folder path"); + ErrorHandler.logError( + e: Exception("Failed to get download folder path"), + s: StackTrace.current, + ); + } + if (directory != null) { + final File f = File("${directory.path}/$filename"); + File resp; + if (fileType == DownloadType.txt || fileType == DownloadType.csv) { + resp = await f.writeAsString(contents); + } else { + resp = await f.writeAsBytes(contents); + } + OpenFile.open(resp.path); + } + } +} + +String getTxtContent(List messages, BuildContext context, + String filename, Room room) { + String formattedInfo = ""; + for (final PangeaMessageEvent message in messages) { + final String timestamp = + DateFormat('yyyy-MM-dd hh:mm:ss').format(message.originServerTs); + final String sender = message.senderId; + final String originalMsg = getOriginalText(message); + final String sentMsg = getSentText(message); + final bool usageAvailable = usageIsAvailable(message); + + if (!usageAvailable) { + formattedInfo += + "${L10n.of(context)!.sender}: $sender\n${L10n.of(context)!.time}: $timestamp\n${L10n.of(context)!.originalMessage}: $originalMsg\n${L10n.of(context)!.sentMessage}: $sentMsg\n${L10n.of(context)!.useType}: ${L10n.of(context)!.notAvailable}\n\n"; + continue; + } + + final bool includedIT = message.originalSent!.choreo!.includedIT; + final bool includedIGC = message.originalSent!.choreo!.includedIGC; + + formattedInfo += + "${L10n.of(context)!.sender}: $sender\n${L10n.of(context)!.time}: $timestamp\n${L10n.of(context)!.originalMessage}: $originalMsg\n${L10n.of(context)!.sentMessage}: $sentMsg\n${L10n.of(context)!.useType}: "; + if (includedIT && includedIGC) { + formattedInfo += L10n.of(context)!.taAndGaTooltip; + } else if (includedIT) { + formattedInfo += L10n.of(context)!.taTooltip; + } else if (includedIGC) { + formattedInfo += L10n.of(context)!.gaTooltip; + } else { + formattedInfo += L10n.of(context)!.waTooltip; + } + formattedInfo += "\n\n"; + } + formattedInfo = "${room.getLocalizedDisplayname()}\n\n$formattedInfo"; + return formattedInfo; +} + +String getCSVContent( + List messages, BuildContext context, String fileName) { + final List> csvData = [ + [ + L10n.of(context)!.sender, + L10n.of(context)!.time, + L10n.of(context)!.originalMessage, + L10n.of(context)!.sentMessage, + L10n.of(context)!.taTooltip, + L10n.of(context)!.gaTooltip, + ] + ]; + for (final PangeaMessageEvent message in messages) { + final String timestamp = + DateFormat('yyyy-MM-dd hh:mm:ss').format(message.originServerTs); + final String sender = message.senderId; + final String originalMsg = getOriginalText(message); + final String sentMsg = getSentText(message); + final bool usageAvailable = usageIsAvailable(message); + + if (!usageAvailable) { + csvData.add([ + sender, + timestamp, + originalMsg, + sentMsg, + L10n.of(context)!.notAvailable, + L10n.of(context)!.notAvailable + ]); + continue; + } + + final bool includedIT = message.originalSent!.choreo!.includedIT; + final bool includedIGC = message.originalSent!.choreo!.includedIGC; + + csvData.add([ + sender, + timestamp, + originalMsg, + sentMsg, + includedIT.toString(), + includedIGC.toString() + ]); + } + final String fileString = const ListToCsvConverter().convert(csvData); + return fileString; +} + +List getExcelContent( + List messages, BuildContext context, String filename) { + final Workbook workbook = Workbook(); + final Worksheet sheet = workbook.worksheets[0]; + + sheet.getRangeByIndex(1, 1).setValue(L10n.of(context)!.sender); + sheet.getRangeByIndex(1, 2).setValue(L10n.of(context)!.time); + sheet.getRangeByIndex(1, 3).setValue(L10n.of(context)!.originalMessage); + sheet.getRangeByIndex(1, 4).setValue(L10n.of(context)!.sentMessage); + sheet.getRangeByIndex(1, 5).setValue(L10n.of(context)!.taTooltip); + sheet.getRangeByIndex(1, 6).setValue(L10n.of(context)!.gaTooltip); + + for (int i = 0; i < messages.length; i++) { + final PangeaMessageEvent message = messages[i]; + final String sender = message.senderId; + final String originalMsg = getOriginalText(message); + final String sentMsg = getSentText(message); + final bool usageAvailable = usageIsAvailable(message); + + bool includedIT = false; + bool includedIGC = false; + + if (usageAvailable) { + includedIT = message.originalSent!.choreo!.includedIT; + includedIGC = message.originalSent!.choreo!.includedIGC; + } + + sheet.getRangeByIndex(i + 2, 1).setValue(sender); + sheet.getRangeByIndex(i + 2, 2).setDateTime(message.originServerTs); + sheet.getRangeByIndex(i + 2, 3).setValue(originalMsg); + sheet.getRangeByIndex(i + 2, 4).setValue(sentMsg); + sheet.getRangeByIndex(i + 2, 5).setValue(L10n.of(context)!.notAvailable); + sheet.getRangeByIndex(i + 2, 6).setValue(L10n.of(context)!.notAvailable); + if (usageAvailable) { + sheet.getRangeByIndex(i + 2, 5).setValue(includedIT); + sheet.getRangeByIndex(i + 2, 6).setValue(includedIGC); + } + } + + final List bytes = workbook.saveAsStream(); + return bytes; +} diff --git a/lib/pangea/utils/error_handler.dart b/lib/pangea/utils/error_handler.dart new file mode 100644 index 000000000..b961bad28 --- /dev/null +++ b/lib/pangea/utils/error_handler.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; + +class ErrorHandler { + ErrorHandler(); + + static Future initialize() async { + FutureOr Function(Scope)? withScope( + Scope scope, FlutterErrorDetails details) { + // if (details.exception is http.Response) { + // final res = details.exception as http.Response; + // scope.addBreadcrumb( + // Breadcrumb.http( + // url: res.request?.url ?? Uri(path: "not available"), + // method: "where does method go?", + // statusCode: res.statusCode, + // ), + // ); + // } else { + // debugPrint("not an http exception ${details.exception.toString()}"); + // } + return null; + } + + await SentryFlutter.init( + (options) { + options.dsn = Environment.sentryDsn; + options.tracesSampleRate = 0.1; + options.debug = kDebugMode; + options.environment = Environment.isStaging ? "staging" : "productionC"; + // options.beforeSend = (event, {hint}) { + // debugger(when: kDebugMode); + // return null; + // }; + }, + ); + + // Error handling + FlutterError.onError = (FlutterErrorDetails details) async { + if (!kDebugMode) { + Sentry.captureException( + details.exception, + stackTrace: details.stack ?? StackTrace.current, + withScope: (scope) => withScope(scope, details), + ); + } + }; + + PlatformDispatcher.instance.onError = (exception, stack) { + logError(e: exception, s: stack); + return true; + }; + } + + static logError( + {Object? e, StackTrace? s, String? m, Map? data}) async { + if ((e ?? m) != null) debugPrint("error: ${e?.toString() ?? m}"); + if (data != null) { + Sentry.addBreadcrumb(Breadcrumb.fromJson(data)); + } + FlutterError.reportError(FlutterErrorDetails( + exception: e ?? Exception(m ?? "no message supplied"), + stack: s, + library: 'Pangea', + context: ErrorSummary(e?.toString() ?? "error not defined"), + stackFilter: (input) => input.where( + (e) => !(e.contains("org-dartlang-sdk") || + e.contains("future_impl") || + e.contains("microtask") || + e.contains("async_patch")), + ), + )); + } +} + +class ErrorCopy { + BuildContext context; + Object? error; + + late String title; + late String body; + int? errorCode; + + ErrorCopy(this.context, this.error) { + setCopy(); + } + + void _setDefaults() { + title = "Unexpected error."; + body = "Please reload and try again."; + errorCode = 400; + } + + void setCopy() { + try { + if (error is http.Response) { + errorCode = (error as http.Response).statusCode; + } else { + ErrorHandler.logError(e: error, s: StackTrace.current); + errorCode = null; + } + if (L10n.of(context) == null) { + _setDefaults(); + Sentry.addBreadcrumb(Breadcrumb.fromJson({"error": error?.toString()})); + ErrorHandler.logError( + m: "null L10n in ErrorCopy.setCopy", + s: StackTrace.current, + ); + return; + } + final L10n l10n = L10n.of(context)!; + + switch (errorCode) { + case 502: + case 504: + case 500: + title = l10n.error502504Title; + body = l10n.error502504Desc; + break; + case 404: + title = l10n.error404Title; + body = l10n.error404Desc; + break; + case 405: + title = l10n.error405Title; + body = l10n.error405Desc; + break; + case 601: + title = l10n.errorDisableIT; + body = l10n.errorDisableITUserDesc; + break; + case 602: + title = l10n.errorDisableIGC; + body = l10n.errorDisableIGCUserDesc; + break; + case 603: + title = l10n.errorDisableIT; + body = l10n.errorDisableITClassDesc; + break; + case 604: + title = l10n.errorDisableIGC; + body = l10n.errorDisableIGCClassDesc; + break; + default: + title = l10n.oopsSomethingWentWrong; + body = l10n.errorPleaseRefresh; + } + } catch (e, s) { + ErrorHandler.logError(e: s, s: s); + _setDefaults(); + } + } +} diff --git a/lib/pangea/utils/find_conversation_partner_dialog.dart b/lib/pangea/utils/find_conversation_partner_dialog.dart new file mode 100644 index 000000000..640524331 --- /dev/null +++ b/lib/pangea/utils/find_conversation_partner_dialog.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +import '../controllers/pangea_controller.dart'; + +void findConversationPartnerDialog( + BuildContext context, + PangeaController pangeaController, +) { + debugPrint(pangeaController.userController.isPublic.toString()); + if (pangeaController.userController.isPublic) { + context.go('/rooms/partner'); + } else { + showDialog( + context: context, + useRootNavigator: false, + builder: (context) => AlertDialog( + title: Text(L10n.of(context)!.setToPublicSettingsTitle), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 250), + child: Text(L10n.of(context)!.setToPublicSettingsDesc), + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(L10n.of(context)!.cancel), + ), + TextButton( + onPressed: () { + context.go('/rooms/settings/learning'); + Navigator.of(context).pop(); + }, + child: Text(L10n.of(context)!.accountSettings), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/utils/firebase_analytics.dart b/lib/pangea/utils/firebase_analytics.dart new file mode 100644 index 000000000..40da756c2 --- /dev/null +++ b/lib/pangea/utils/firebase_analytics.dart @@ -0,0 +1,157 @@ +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +// PageRoute import +import 'package:flutter/widgets.dart'; + +import '../../config/firebase_options.dart'; +import '../enum/use_type.dart'; + +// Add import: +// import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +// Call method: GoogleAnalytics.logout() + +class GoogleAnalytics { + static FirebaseAnalytics? analytics; + + GoogleAnalytics(); + + static Future initialize() async { + FirebaseApp app; + try { + app = await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } on Exception { + app = Firebase.app(); + } + + analytics = FirebaseAnalytics.instanceFor(app: app); + } + + static analyticsUserUpdate(String? userID) { + print("user update $userID"); + analytics?.setUserId(id: userID); + } + + static updateUserSubscriptionStatus(bool subscribed) { + analytics?.setUserProperty( + name: 'subscribed', + value: "$subscribed", + ); + } + + static logEvent(String name, {parameters}) { + debugPrint("event: $name - parameters: $parameters"); + analytics?.logEvent(name: name, parameters: parameters); + } + + static login(String type, String? userID) { + logEvent('login', parameters: {'method': type}); + analyticsUserUpdate(userID); + } + + static signUp(String type) { + logEvent('sign_up', parameters: {'method': type}); + } + + static logout() { + logEvent('logout'); + analyticsUserUpdate(null); + } + + static createClass(String className, String classCode) { + logEvent('create_class', + parameters: {'name': className, 'group_id': classCode}); + } + + static createExchange(String exchangeName, String classCode) { + logEvent('create_exchange', + parameters: {'name': exchangeName, 'group_id': classCode}); + } + + static createChat(String newChatRoomId) { + logEvent('create_chat', parameters: {"chat_id": newChatRoomId}); + } + + static addParent(String chatRoomId, String classCode) { + logEvent('add_room_to_class', + parameters: {"chat_id": chatRoomId, 'group_id': classCode}); + } + + static removeChatFromClass(String chatRoomId, String classCode) { + logEvent('remove_room_from_class', + parameters: {"chat_id": chatRoomId, 'group_id': classCode}); + } + + static addChatToExchange(String chatRoomId, String classCode) { + logEvent('add_chat_to_exchange', + parameters: {"chat_id": chatRoomId, 'group_id': classCode}); + } + + static inviteClassToExchange(String classId, String exchangeId) { + logEvent('invite_class_to_exchange', + parameters: {'group_id': classId, 'exchange_id': exchangeId}); + } + + static kickClassFromExchange(String classId, String exchangeId) { + logEvent('kick_class_from_exchange', + parameters: {'group_id': classId, 'exchange_id': exchangeId}); + } + + static joinClass(String classCode) { + logEvent('join_group', parameters: {'group_id': classCode}); + } + + static sendMessage(String chatRoomId, String classCode, UseType useType) { + logEvent('sent_message', parameters: { + "chat_id": chatRoomId, + 'group_id': classCode, + "message_type": useType.toString() + }); + } + + static contextualRequest() { + logEvent('context_request'); + } + + static messageTranslate() { + logEvent('message_translate'); + } + + static beginPurchaseSubscription( + SubscriptionDetails details, BuildContext context) { + logEvent('begin_checkout', parameters: { + "currency": "USD", + 'value': details.price, + 'transaction_id': details.id, + 'items': [ + { + 'item_id': details.package!.identifier, + 'item_name': details.displayName(context), + 'price': details.price, + 'item_category': "subscription", + 'quantity': 1 + } + ] + }); + } + + static FirebaseAnalyticsObserver getAnalyticsObserver() => + FirebaseAnalyticsObserver( + analytics: analytics!, + routeFilter: (route) { + // By default firebase only tracks page routes + if (route is! PageRoute || + // No user logged in, so we dont track + route.settings.name == "login" || + route.settings.name == "/home" || + route.settings.name == "connect" || + route.settings.name == "signup") { + return false; + } + final String? name = route.settings.name; + debugPrint("navigating to route: $name"); + return true; + }); +} diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart new file mode 100644 index 000000000..4626bad82 --- /dev/null +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -0,0 +1,77 @@ +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import '../../utils/matrix_sdk_extensions/matrix_locals.dart'; + +class GetChatListItemSubtitle { + Future getSubtitle( + BuildContext context, + Event? event, + PangeaController pangeaController, + ) async { + if (event == null) return L10n.of(context)!.emptyChat; + // try { + if (event.type != EventTypes.Message || + !pangeaController.permissionsController + .isToolEnabled(ToolSetting.immersionMode, event.room)) { + return event.calcLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: !event.room.isDirectChat || + event.room.directChatMatrixID != event.room.lastEvent?.senderId, + ); + } + + final Timeline timeline = + await event.room.getTimeline(eventContextId: event.eventId); + final PangeaMessageEvent pangeaMessageEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: false, + selected: false, + ); + final l2Code = + pangeaController.languageController.activeL2Code(roomID: event.roomId); + + if (l2Code == null || l2Code == LanguageKeys.unknownLanguage) { + return event.body; + } + + final String? text = + (await pangeaMessageEvent.representationByLanguageGlobal( + context: context, + langCode: l2Code, + )) + ?.text; + + final i18n = MatrixLocals(L10n.of(context)!); + + if (text == null) return L10n.of(context)!.emptyChat; + + if (!event.room.isDirectChat || + event.room.directChatMatrixID != event.room.lastEvent?.senderId) { + final senderNameOrYou = event.senderId == event.room.client.userID + ? i18n.you + : event.room + .unsafeGetUserFromMemoryOrFallback(event.senderId) + .calcDisplayname(i18n: i18n); + + return "$senderNameOrYou: $text"; + } + + return text; + // } catch (e, s) { + // debugger(when: kDebugMode); + // ErrorHandler.logError(e: e, s: s); + // return event.body; + // } + } +} diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart new file mode 100644 index 000000000..f99705304 --- /dev/null +++ b/lib/pangea/utils/instructions.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../config/app_config.dart'; +import '../../widgets/matrix.dart'; +import '../controllers/pangea_controller.dart'; +import '../widgets/common/bot_face_svg.dart'; +import '../widgets/igc/card_header.dart'; +import 'bot_style.dart'; +import 'error_handler.dart'; +import 'overlay.dart'; + +class InstructionsController { + late PangeaController _pangeaController; + + final Map _instructionsClosed = {}; + final Map _instructionsShown = {}; + + InstructionsController(PangeaController pangeaController) { + _pangeaController = pangeaController; + } + + bool wereInstructionsTurnedOff(InstructionsEnum key) => + _pangeaController.pStoreService.read(key.toString()) ?? + _instructionsClosed[key] ?? + false; + + void updateEnableInstructions(InstructionsEnum key, bool value) => + _pangeaController.pStoreService.save(key.toString(), value); + + void show( + BuildContext context, InstructionsEnum key, String transformTargetKey, + [bool showToggle = true]) { + if (wereInstructionsTurnedOff(key)) { + return; + } + if (L10n.of(context) == null) { + ErrorHandler.logError( + m: "null context in ITBotButton.showCard", + s: StackTrace.current, + ); + return; + } + if (_instructionsShown[key] ?? false) { + return; + } + _instructionsShown[key] = true; + + final botStyle = BotStyle.text(context); + Future.delayed( + const Duration(seconds: 1), + () => OverlayUtil.showPositionedCard( + context: context, + backDropToDismiss: false, + cardToShow: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CardHeader( + text: key.title(context), + botExpression: BotExpression.surprised, + onClose: () => {_instructionsClosed[key] = true}, + ), + const SizedBox(height: 10.0), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + key.body(context), + style: botStyle, + ), + ), + ), + ), + if (showToggle) InstructionsToggle(instructionsKey: key) + ], + ), + cardSize: const Size(300.0, 300.0), + transformTargetId: transformTargetKey, + ), + ); + } +} + +enum InstructionsEnum { + itInstructions, + clickMessage, + understandingMessages, + blurMeansTranslate, +} + +extension Copy on InstructionsEnum { + String title(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return L10n.of(context)!.itInstructionsTitle; + case InstructionsEnum.clickMessage: + return L10n.of(context)!.clickMessageTitle; + case InstructionsEnum.understandingMessages: + return L10n.of(context)!.understandingMessagesTitle; + case InstructionsEnum.blurMeansTranslate: + return L10n.of(context)!.blurMeansTranslateTitle; + } + } + + String body(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return L10n.of(context)!.itInstructionsBody; + case InstructionsEnum.clickMessage: + return L10n.of(context)!.clickMessageBody; + case InstructionsEnum.understandingMessages: + return L10n.of(context)!.understandingMessagesBody; + case InstructionsEnum.blurMeansTranslate: + return L10n.of(context)!.blurMeansTranslateBody; + } + } +} + +class InstructionsToggle extends StatefulWidget { + const InstructionsToggle({ + Key? key, + required this.instructionsKey, + }) : super(key: key); + + final InstructionsEnum instructionsKey; + + @override + InstructionsToggleState createState() => InstructionsToggleState(); +} + +class InstructionsToggleState extends State { + PangeaController pangeaController = MatrixState.pangeaController; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SwitchListTile.adaptive( + activeColor: AppConfig.activeToggleColor, + title: Text(L10n.of(context)!.doNotShowAgain), + value: pangeaController.instructions + .wereInstructionsTurnedOff(widget.instructionsKey), + onChanged: ((value) { + pangeaController.instructions + .updateEnableInstructions(widget.instructionsKey, value); + setState(() {}); + })); + } +} diff --git a/lib/pangea/utils/join_all_space_chats.dart b/lib/pangea/utils/join_all_space_chats.dart new file mode 100644 index 000000000..054bf7c62 --- /dev/null +++ b/lib/pangea/utils/join_all_space_chats.dart @@ -0,0 +1,47 @@ +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +// Used in lock space. Handles case when child rooms return null from client.getRoomById +// Because the user hasn't joined them +Future> joinAllSpaceChats(Room space, Client client) async { + final List childrenIds = + space.spaceChildren.map((x) => x.roomId!).toList(); + + final List children = []; + for (final String childId in childrenIds) { + final Room? child = client.getRoomById(childId); + if (child != null) { + children.add(child); + } + // child may be null if the user is not in the room + else { + final Room? child = await tryGetRoomById(childId, client); + if (child != null) { + children.add(child); + } + } + } + return children; +} + +Future tryGetRoomById(String roomId, Client client) async { + try { + await client.joinRoomById(roomId); + } catch (err) { + // caused when chat has been archived + debugPrint("Failed to join $roomId"); + return null; + } + await client.waitForRoomInSync(roomId); + final Room? room = client.getRoomById(roomId); + if (room != null) { + return room; + } else { + ErrorHandler.logError( + e: "Failed to fetch child room with id $roomId after joining", + s: StackTrace.current, + ); + } + return null; +} diff --git a/lib/pangea/utils/language_level_copy.dart b/lib/pangea/utils/language_level_copy.dart new file mode 100644 index 000000000..7f68f544a --- /dev/null +++ b/lib/pangea/utils/language_level_copy.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class LanguageLevelTextPicker { + static String languageLevelText(BuildContext context, int languageLevel) { + final L10n copy = L10n.of(context)!; + switch (languageLevel) { + case 0: + return copy.languageLevelPreA1; + case 1: + return copy.languageLevelA1; + case 2: + return copy.languageLevelA2; + case 3: + return copy.languageLevelB1; + case 4: + return copy.languageLevelB2; + case 5: + return copy.languageLevelC1; + case 6: + return copy.languageLevelC2; + default: + return "undefined level"; + } + } +} diff --git a/lib/pangea/utils/lock_room.dart b/lib/pangea/utils/lock_room.dart new file mode 100644 index 000000000..92d1a6f5d --- /dev/null +++ b/lib/pangea/utils/lock_room.dart @@ -0,0 +1,63 @@ +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/join_all_space_chats.dart'; +import 'package:matrix/matrix.dart'; + +Future unlockChat(Room room, Client client) async { + final Map powerLevelsContent = Map.from( + room.getState(EventTypes.RoomPowerLevels)!.content, + ); + + powerLevelsContent['events_default'] = 0; + powerLevelsContent['events'][EventTypes.spaceChild] = 0; + + await room.client.setRoomStateWithKey( + room.id, + EventTypes.RoomPowerLevels, + '', + powerLevelsContent, + ); +} + +Future lockChat(Room room, Client client) async { + final Map powerLevelsContent = Map.from( + room.getState(EventTypes.RoomPowerLevels)!.content, + ); + powerLevelsContent['events_default'] = 100; + powerLevelsContent['events'][EventTypes.spaceChild] = 100; + + await room.client.setRoomStateWithKey( + room.id, + EventTypes.RoomPowerLevels, + '', + powerLevelsContent, + ); +} + +Future lockSpace(Room space, Client client) async { + final List children = await joinAllSpaceChats(space, client); + for (final Room child in children) { + await lockChat(child, client); + } + await lockChat(space, client); +} + +Future unlockSpace(Room space, Client client) async { + final List children = space.spaceChildren + .map((child) => client.getRoomById(child.roomId!)) + .toList(); + for (final Room? child in children) { + if (child != null) { + await unlockChat(child, client); + } + } + await unlockChat(space, client); +} + +Future toggleLockRoom(Room? room, Client client) async { + if (room == null || !room.isRoomAdmin) return; + if (!room.isSpace) { + room.locked ? await unlockChat(room, client) : await lockChat(room, client); + return; + } + room.locked ? await unlockSpace(room, client) : await lockSpace(room, client); +} diff --git a/lib/pangea/utils/logout.dart b/lib/pangea/utils/logout.dart new file mode 100644 index 000000000..cffc2608a --- /dev/null +++ b/lib/pangea/utils/logout.dart @@ -0,0 +1,25 @@ +import 'package:adaptive_dialog/adaptive_dialog.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'; + +void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async { + if (await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSureYouWantToLogout, + message: L10n.of(context)!.noBackupWarning, + isDestructiveAction: isDestructiveAction ?? false, + okLabel: L10n.of(context)!.logout, + cancelLabel: L10n.of(context)!.cancel, + ) == + OkCancelResult.cancel) { + return; + } + final matrix = Matrix.of(context); + await showFutureLoadingDialog( + context: context, + future: () => matrix.client.logout(), + ); +} diff --git a/lib/pangea/utils/martix.utils.dart b/lib/pangea/utils/martix.utils.dart new file mode 100644 index 000000000..99e535a1b --- /dev/null +++ b/lib/pangea/utils/martix.utils.dart @@ -0,0 +1,7 @@ +import 'package:matrix/matrix.dart'; + +class MatrixUtils { + static String generateUniqueTransactionId(Client client) { + return client.generateUniqueTransactionId(); + } +} diff --git a/lib/pangea/utils/match_copy.dart b/lib/pangea/utils/match_copy.dart new file mode 100644 index 000000000..94abd4335 --- /dev/null +++ b/lib/pangea/utils/match_copy.dart @@ -0,0 +1,227 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/enum/span_data_type.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../constants/match_rule_ids.dart'; +import '../models/pangea_match_model.dart'; + +class MatchCopy { + PangeaMatch match; + late String title; + String? description; + + MatchCopy(BuildContext context, this.match) { + if (match.match.rule?.id != null) { + _byMatchRuleId(context); + return; + } + if (match.match.shortMessage != null) { + title = match.match.shortMessage!; + } + if (match.match.message != null) { + description = match.match.message!; + } + if (match.match.shortMessage == null) { + _bySpanDataType(context); + } + } + + _setDefaults() { + try { + title = match.match.shortMessage ?? "unknown"; + description = match.match.message ?? "unknown"; + } catch (err) { + title = "Error"; + description = "Could not find the check info"; + } + } + + void _bySpanDataType(BuildContext context) { + try { + final L10n l10n = L10n.of(context)!; + switch (match.match.type.typeName) { + case SpanDataTypeEnum.correction: + title = l10n.someErrorTitle; + description = l10n.someErrorBody; + break; + case SpanDataTypeEnum.definition: + title = match.matchContent; + description = null; + break; + case SpanDataTypeEnum.itStart: + title = l10n.needsItShortMessage; + // description = l10n.needsItMessage; + break; + case SpanDataTypeEnum.practice: + title = match.match.shortMessage ?? "Activity"; + description = match.match.message; + break; + } + } catch (err, stack) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: "match", + data: { + "match": match.toJson(), + }, + ), + ); + ErrorHandler.logError(e: err, s: stack); + _setDefaults(); + } + } + + void _byMatchRuleId(BuildContext context) { + try { + if (match.match.rule?.id == null) { + throw Exception("match.match.rule.id is null"); + } + final L10n l10n = L10n.of(context)!; + + final List splits = match.match.rule!.id.split(":"); + if (splits.length >= 2) { + splits.removeAt(0); + } + final String afterColon = splits.join(); + + print("grammar rule ${match.match.rule!.id}"); + + switch (afterColon) { + case MatchRuleIds.interactiveTranslation: + title = l10n.needsItShortMessage; + description = l10n.needsItMessage; + break; + case MatchRuleIds.tokenNeedsTranslation: + title = l10n.tokenTranslationTitle; + description = l10n.spanTranslationDesc; + break; + case MatchRuleIds.tokenSpanNeedsTranslation: + title = l10n.spanTranslationTitle; + description = l10n.spanTranslationDesc; + break; + case MatchRuleIds.l1SpanAndGrammar: + title = l10n.l1SpanAndGrammarTitle; + description = l10n.l1SpanAndGrammarDesc; + break; + // case "PART": + // title = l10n.partTitle; + // description = l10n.partDesc; + // break; + // case "PUNCT": + // title = l10n.punctTitle; + // description = l10n.punctDesc; + // break; + // case "ORTH": + // title = l10n.orthTitle; + // description = l10n.orthDesc; + // break; + // case "SPELL": + // title = l10n.spellTitle; + // description = l10n.spellDesc; + // break; + // case "WO": + // title = l10n.woTitle; + // description = l10n.woDesc; + // break; + // case "MORPH": + // title = l10n.morphTitle; + // description = l10n.morphDesc; + // break; + // case "ADV": + // title = l10n.advTitle; + // description = l10n.advDesc; + // break; + // case "CONTR": + // title = l10n.contrTitle; + // description = l10n.contrDesc; + // break; + // case "CONJ": + // title = l10n.conjTitle; + // description = l10n.conjDesc; + // break; + // case "DET": + // title = l10n.detTitle; + // description = l10n.detDesc; + // break; + // case "DETART": + // title = l10n.detArtTitle; + // description = l10n.detArtDesc; + // break; + // case "PREP": + // title = l10n.prepTitle; + // description = l10n.prepDesc; + // break; + // case "PRON": + // title = l10n.pronTitle; + // description = l10n.pronDesc; + // break; + // case "VERB": + // title = l10n.verbTitle; + // description = l10n.verbDesc; + // break; + // case "VERBFORM": + // title = l10n.verbFormTitle; + // description = l10n.verbFormDesc; + // break; + // case "VERBTENSE": + // title = l10n.verbTenseTitle; + // description = l10n.verbTenseDesc; + // break; + // case "VERBSVA": + // title = l10n.verbSvaTitle; + // description = l10n.verbSvaDesc; + // break; + // case "VERBINFL": + // title = l10n.verbInflTitle; + // description = l10n.verbInflDesc; + // break; + // case "ADJ": + // title = l10n.adjTitle; + // description = l10n.adjDesc; + // break; + // case "ADJFORM": + // title = l10n.adjFormTitle; + // description = l10n.adjFormDesc; + // break; + // case "NOUN": + // title = l10n.nounTitle; + // description = l10n.nounDesc; + // break; + // case "NOUNPOSS": + // title = l10n.nounPossTitle; + // description = l10n.nounPossDesc; + // break; + // case "NOUNINFL": + // title = l10n.nounInflTitle; + // description = l10n.nounInflDesc; + // break; + // case "NOUNNUM": + // title = l10n.nounNumTitle; + // description = l10n.nounNumDesc; + // break; + case "OTHER": + default: + _setDefaults(); + break; + } + } catch (err, stack) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: "match", + data: { + "match": match.toJson(), + }, + ), + ); + ErrorHandler.logError(e: err, s: stack); + _setDefaults(); + } + } +} diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart new file mode 100644 index 000000000..397549f65 --- /dev/null +++ b/lib/pangea/utils/overlay.dart @@ -0,0 +1,206 @@ +import 'dart:developer'; +import 'dart:math'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/widgets/common_widgets/overlay_container.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../config/themes.dart'; +import '../../widgets/matrix.dart'; +import 'error_handler.dart'; + +class OverlayUtil { + static showPositionedCard({ + required BuildContext context, + required Widget cardToShow, + required Size cardSize, + required String transformTargetId, + backDropToDismiss = true, + Color? borderColor, + }) { + try { + MatrixState.pAnyState.closeOverlay(); + + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(transformTargetId); + + final Offset cardOffset = _calculateCardOffset( + cardSize: cardSize, + transformTargetKey: layerLinkAndKey.key, + ); + + MatrixState.pAnyState.overlay = OverlayEntry( + builder: (context) => Stack( + children: [ + if (backDropToDismiss) const TransparentBackdrop(), + Positioned( + width: cardSize.width, + height: cardSize.height, + child: CompositedTransformFollower( + link: layerLinkAndKey.link, + showWhenUnlinked: false, + offset: cardOffset, + child: Material( + borderOnForeground: false, + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: OverlayContainer( + cardToShow: cardToShow, borderColor: borderColor), + ), + ), + ), + ], + ), + ); + + Overlay.of(layerLinkAndKey.key.currentContext!) + .insert(MatrixState.pAnyState.overlay!); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + /// calculates the card offset relative to the target + /// identified by [transformTargetKey] + static Offset _calculateCardOffset({ + required Size cardSize, + required LabeledGlobalKey transformTargetKey, + final double minPadding = 10.0, + }) { + // debugger(when: kDebugMode); + //Note: assumes overlay in chatview + final OverlayConstraints constraints = + ChatViewConstraints(transformTargetKey.currentContext!); + + final RenderObject? targetRenderBox = + transformTargetKey.currentContext!.findRenderObject(); + if (targetRenderBox == null) return Offset.zero; + final Offset transformTargetOffset = + (targetRenderBox as RenderBox).localToGlobal(Offset.zero); + final Size transformTargetSize = targetRenderBox.size; + + // ideally horizontally centered on target + double dx = transformTargetSize.width / 2 - cardSize.width / 2; + // make sure it's not off the left edge of the screen + // if transformTargetOffset.dx + dc < constraints.x0 + minPadding + + if (transformTargetOffset.dx + dx < minPadding + constraints.x0) { + debugPrint("setting dx"); + dx = minPadding + constraints.x0 - transformTargetOffset.dx; + } + // make sure it's not off the right edge of the screen + if (transformTargetOffset.dx + dx + cardSize.width + minPadding > + constraints.x1) { + dx = constraints.x1 - + transformTargetOffset.dx - + cardSize.width - + minPadding; + } + + // if there's more room above target, + // put the card there + // else, + // put it below + // debugPrint( + // "transformTargetOffset.dx ${transformTargetOffset.dx} transformTargetOffset.dy ${transformTargetOffset.dy}"); + // debugPrint( + // "transformTargetSize.width ${transformTargetSize.width} transformTargetSize.height ${transformTargetSize.height}"); + double dy = transformTargetOffset.dy > + constraints.y1 - + transformTargetOffset.dy - + transformTargetSize.height + ? -cardSize.height - minPadding + : transformTargetSize.height + minPadding; + // make sure it's not off the top edge of the screen + if (dy < minPadding + constraints.y0 - transformTargetOffset.dy) { + dy = minPadding + constraints.y0 - transformTargetOffset.dy; + } + // make sure it's not off the bottom edge of the screen + if (transformTargetOffset.dy + dy + cardSize.height + minPadding > + constraints.y1) { + dy = constraints.y1 - + transformTargetOffset.dy - + cardSize.height - + minPadding; + } + // debugPrint("dx $dx dy $dy"); + + return Offset(dx, dy); + } +} + +class TransparentBackdrop extends StatelessWidget { + const TransparentBackdrop({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + borderOnForeground: false, + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: InkWell( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + focusColor: Colors.transparent, + highlightColor: Colors.transparent, + onTap: () { + MatrixState.pAnyState.closeOverlay(); + }, + child: Container( + height: double.infinity, + width: double.infinity, + color: Colors.transparent, + ), + ), + ); + } +} + +/// global coordinates that the overlay should stay inside +abstract class OverlayConstraints { + late double x0; + late double y0; + late double x1; + late double y1; +} + +class ChatViewConstraints implements OverlayConstraints { + @override + late double x0; + @override + late double y0; + @override + late double x1; + @override + late double y1; + + ChatViewConstraints(BuildContext context) { + final MediaQueryData mediaQueryData = + MediaQuery.of(Scaffold.of(context).context); + final bool isColumnMode = FluffyThemes.isColumnMode(context); + + x0 = isColumnMode + ? AppConfig.columnWidth + 70.0 + : max(mediaQueryData.viewPadding.left, mediaQueryData.viewInsets.left); + y0 = max(mediaQueryData.viewPadding.top, mediaQueryData.viewInsets.top); + x1 = mediaQueryData.size.width - + max(mediaQueryData.viewPadding.right, mediaQueryData.viewInsets.right); + y1 = mediaQueryData.size.height - + max(mediaQueryData.viewPadding.bottom, + mediaQueryData.viewInsets.bottom); + + // https://medium.com/flutter-community/a-flutter-guide-to-visual-overlap-padding-viewpadding-and-viewinsets-a63e214be6e8 + // debugPrint( + // "viewInsets ${mediaQueryData.viewInsets.left} ${mediaQueryData.viewInsets.top} ${mediaQueryData.viewInsets.right} ${mediaQueryData.viewInsets.bottom}"); + // debugPrint( + // "padding ${mediaQueryData.padding.left} ${mediaQueryData.padding.top} ${mediaQueryData.padding.right} ${mediaQueryData.padding.bottom}"); + // debugPrint( + // "viewPadding ${mediaQueryData.viewPadding.left} ${mediaQueryData.viewPadding.top} ${mediaQueryData.viewPadding.right} ${mediaQueryData.viewPadding.bottom}"); + // debugPrint("chatViewConstraints x0: $x0 y0: $y0 x1: $x1 y1: $y1"); + } +} diff --git a/lib/pangea/utils/p_extension.dart b/lib/pangea/utils/p_extension.dart new file mode 100644 index 000000000..a80a302a5 --- /dev/null +++ b/lib/pangea/utils/p_extension.dart @@ -0,0 +1,12 @@ +extension IsAtLeastYearsOld on DateTime { + bool isAtLeastYearsOld(int years) { + final now = DateTime.now(); + final boundaryDate = DateTime(now.year - years, now.month, now.day); + + // Discard the time from [this]. + final thisDate = DateTime(year, month, day); + + // Did [thisDate] occur on or before [boundaryDate]? + return thisDate.compareTo(boundaryDate) <= 0; + } +} diff --git a/lib/pangea/utils/p_store.dart b/lib/pangea/utils/p_store.dart new file mode 100644 index 000000000..a071722cd --- /dev/null +++ b/lib/pangea/utils/p_store.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:get_storage/get_storage.dart'; + +class PLocalStore { + final GetStorage _box = GetStorage(); + final PangeaController pangeaController; + + PLocalStore({required this.pangeaController}); + + /// save data in local + Future save(String key, dynamic data, + {bool addClientIdToKey = true}) async { + await _box.write(_key(key, addClientIdToKey: addClientIdToKey), data); + } + + /// fetch data from local + dynamic read(String key, {bool addClientIdToKey = true}) { + return pangeaController.matrixState.client.userID != null + ? _box.read(_key(key, addClientIdToKey: addClientIdToKey)) + : null; + } + + /// delete data from local + Future delete(String key, {bool addClientIdToKey = true}) async { + return pangeaController.matrixState.client.userID != null + ? _box.remove(_key(key, addClientIdToKey: addClientIdToKey)) + : null; + } + + _key(String key, {bool addClientIdToKey = true}) { + return addClientIdToKey + ? pangeaController.matrixState.client.userID! + key + : key; + } + + /// clear all local storage + clearStorage() { + _box.erase(); + } +} diff --git a/lib/pangea/utils/p_toast.dart b/lib/pangea/utils/p_toast.dart new file mode 100644 index 000000000..7ccbd2c14 --- /dev/null +++ b/lib/pangea/utils/p_toast.dart @@ -0,0 +1,31 @@ +// import 'package:flutter/material.dart'; + +class PToastController { + //TODO: Figure out best toast to show + //Ideal is reuse FluffyChat UI for consistency + ///show toast message + static toastMsg({bool success = false, String msg = ""}) { + // success + // ? Fluttertoast.showToast( + // msg: msg, + // fontSize: 16.0, + // backgroundColor: Color(0xFF228C22), + // webBgColor: "#228C22", + // textColor: Colors.white, + // timeInSecForIosWeb: 2) + // : Fluttertoast.showToast( + // msg: msg, + // fontSize: 16.0, + // timeInSecForIosWeb: 2, + // webBgColor: "#ff0000", + // backgroundColor: Colors.red, + // textColor: Colors.white, + // ); + } + + // @override + // void dispose() { + // // TODO: implement dispose + // super.dispose(); + //} +} diff --git a/lib/pangea/utils/password_forgotten.dart b/lib/pangea/utils/password_forgotten.dart new file mode 100644 index 000000000..7c7344cb6 --- /dev/null +++ b/lib/pangea/utils/password_forgotten.dart @@ -0,0 +1,152 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pages/login/login.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; + +extension PangeaPasswordForgotten on LoginController { + void pangeaPasswordForgotten() async { + final TextEditingController emailController = TextEditingController(); + final TextEditingController newPasswordController = TextEditingController(); + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => Scaffold( + backgroundColor: Colors.transparent, + body: AlertDialog( + title: Text(L10n.of(context)!.passwordForgotten), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(L10n.of(context)!.enterAnEmailAddress), + const SizedBox(height: 12), + TextField( + controller: emailController, + decoration: InputDecoration( + hintText: L10n.of(context)!.enterAnEmailAddress, + ), + ) + ], + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + return; + }, + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () async { + if (emailController.text == "") return; + final clientSecret = + DateTime.now().millisecondsSinceEpoch.toString(); + final response = await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context) + .getLoginClient() + .requestTokenToResetPasswordEmail( + clientSecret, + emailController.text, + LoginController.sendAttempt++, + ), + ); + if (response.error != null) { + return; + } + Navigator.of(context).pop(); + final TextEditingController textFieldController = + TextEditingController(); + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => Scaffold( + backgroundColor: Colors.transparent, + body: AlertDialog( + title: Text(L10n.of(context)!.passwordForgotten), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(L10n.of(context)!.chooseAStrongPassword), + const SizedBox(height: 12), + TextField( + obscureText: true, + controller: newPasswordController, + decoration: const InputDecoration( + hintText: "******", + ), + ), + ], + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + return; + }, + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () async { + if (newPasswordController.text == "") return; + final ok = await showOkAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.weSentYouAnEmail, + message: L10n.of(context)!.pleaseClickOnLink, + okLabel: L10n.of(context)!.iHaveClickedOnLink, + fullyCapitalizedForMaterial: false, + ); + if (ok != OkCancelResult.ok) return; + final data = { + 'new_password': newPasswordController.text, + 'logout_devices': false, + "auth": AuthenticationThreePidCreds( + type: AuthenticationTypes.emailIdentity, + threepidCreds: ThreepidCreds( + sid: response.result!.sid, + clientSecret: clientSecret, + ), + ).toJson(), + }; + final success = await showFutureLoadingDialog( + context: context, + future: () => + Matrix.of(context).getLoginClient().request( + RequestType.POST, + '/client/r0/account/password', + data: data, + ), + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)! + .passwordHasBeenChanged, + ), + ), + ); + usernameController.text = emailController.text; + passwordController.text = newPasswordController.text; + login(); + } + }, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pangea/utils/platform_name.dart b/lib/pangea/utils/platform_name.dart new file mode 100644 index 000000000..f5b6c1dd4 --- /dev/null +++ b/lib/pangea/utils/platform_name.dart @@ -0,0 +1,41 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode; + +class MyPlatformName { + static String get platformName { + if (kIsWeb) { + return 'web'; + } + if (Platform.isAndroid) { + return 'android'; + } + + if (Platform.isIOS) { + return 'ios'; + } + + if (Platform.isFuchsia) { + return 'fuchsia'; + } + + if (Platform.isLinux) { + return 'linux'; + } + if (Platform.isMacOS) { + return 'macos'; + } + if (Platform.isWindows) { + return 'windows'; + } + return 'unknownplatform'; + } + + static String get getPlatformWithModeName { + String mode = 'Release'; + if (kDebugMode) { + mode = 'Debug'; + } + return MyPlatformName.platformName + mode; + } +} diff --git a/lib/pangea/utils/report_message.dart b/lib/pangea/utils/report_message.dart new file mode 100644 index 000000000..333d0f563 --- /dev/null +++ b/lib/pangea/utils/report_message.dart @@ -0,0 +1,178 @@ +import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; +import 'package:fluffychat/pangea/extensions/client_extension.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:matrix/matrix.dart'; + +Future reportMessage( + BuildContext context, + String roomId, + String reason, + String reportedUserId, + String reportedMessage, +) async { + final Room? reportedInRoom = Matrix.of(context).client.getRoomById(roomId); + if (reportedInRoom == null) { + throw ("Null room with id $roomId in reportMessage"); + } + + final List teachers = + await getReportTeachers(context, reportedInRoom); + if (teachers.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.noTeachersFound, + ), + ), + ); + return; + } + + final List? selectedTeachers = await showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => TeacherSelectDialog(teachers: teachers), + ); + + if (selectedTeachers == null || selectedTeachers.isEmpty) { + return; + } + + final List reportDMs = []; + for (final SpaceTeacher teacher in selectedTeachers) { + final Room reportDM = await Matrix.of(context).client.getReportsDM( + teacher.teacher, + teacher.space, + ); + reportDMs.add(reportDM); + } + + final String reportingUserId = Matrix.of(context).client.userID ?? ""; + final String roomName = reportedInRoom.getLocalizedDisplayname(); + final String messageTitle = L10n.of(context)!.reportMessageTitle( + reportingUserId, + reportedUserId, + roomName, + ); + final String messageBody = L10n.of(context)!.reportMessageBody( + reportedMessage, + reason, + ); + final String message = "$messageTitle\n\n$messageBody"; + for (final Room reportDM in reportDMs) { + final event = { + 'msgtype': PangeaMessageTypes.report, + 'body': message, + }; + await reportDM.sendEvent(event); + } +} + +Future> getReportTeachers( + BuildContext context, + Room room, +) async { + // create a list of teachers and their assosiated spaces + // prioritize the spaces that are parents of the report room + final List teachers = []; + + final List reportRoomParentSpaces = room.spaceParents + .where((parentSpace) => parentSpace.roomId != null) + .map((parentSpace) => + Matrix.of(context).client.getRoomById(parentSpace.roomId!)) + .where((parentSpace) => parentSpace != null) + .cast() + .toList(); + + for (final Room space in reportRoomParentSpaces) { + final List spaceTeachers = await space.teachers; + for (final User spaceTeacher in spaceTeachers) { + if (!teachers.any((teacher) => teacher.teacher.id == spaceTeacher.id) && + spaceTeacher.id != Matrix.of(context).client.userID) { + teachers.add(SpaceTeacher(spaceTeacher, space)); + } + } + } + + final List otherSpaces = Matrix.of(context) + .client + .classesAndExchangesImIn + .where((space) => !reportRoomParentSpaces.contains(space)) + .toList(); + + for (final space in otherSpaces) { + for (final spaceTeacher in await space.teachers) { + if (!teachers.any((teacher) => teacher.teacher.id == spaceTeacher.id) && + spaceTeacher.id != Matrix.of(context).client.userID) { + teachers.add(SpaceTeacher(spaceTeacher, space)); + } + } + } + + return teachers; +} + +class TeacherSelectDialog extends StatefulWidget { + final List teachers; + const TeacherSelectDialog({Key? key, required this.teachers}) + : super(key: key); + + @override + State createState() => _TeacherSelectDialogState(); +} + +class _TeacherSelectDialogState extends State { + final List _selectedItems = []; + + void _itemChange(SpaceTeacher itemValue, bool isSelected) { + setState(() { + isSelected + ? _selectedItems.add(itemValue) + : _selectedItems.remove(itemValue); + }); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text( + L10n.of(context)!.reportToTeacher, + style: const TextStyle(fontSize: 16), + ), + content: SingleChildScrollView( + child: ListBody( + children: widget.teachers + .map( + (teacher) => CheckboxListTile( + value: _selectedItems.contains(teacher), + title: Text(teacher.teacher.id), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (isChecked) => _itemChange(teacher, isChecked!), + ), + ) + .toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(L10n.of(context)!.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(_selectedItems), + child: Text(L10n.of(context)!.submit), + ), + ], + ); + } +} + +class SpaceTeacher { + final User teacher; + final Room space; + + SpaceTeacher(this.teacher, this.space); +} diff --git a/lib/pangea/utils/set_class_name.dart b/lib/pangea/utils/set_class_name.dart new file mode 100644 index 000000000..ca2aba448 --- /dev/null +++ b/lib/pangea/utils/set_class_name.dart @@ -0,0 +1,54 @@ +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 '../../utils/matrix_sdk_extensions/matrix_locals.dart'; + +void setClassDisplayname(BuildContext context, String? roomId) async { + final room = Matrix.of(context).client.getRoomById(roomId!)!; + final TextEditingController textFieldController = TextEditingController( + text: room.getLocalizedDisplayname( + MatrixLocals( + L10n.of(context)!, + ), + )); + + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => AlertDialog( + title: Text( + room.isSpace + ? L10n.of(context)!.changeTheNameOfTheClass + : L10n.of(context)!.changeTheNameOfTheChat, + ), + content: TextField( + controller: textFieldController, + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () async { + if (textFieldController.text == "") return; + final success = await showFutureLoadingDialog( + context: context, + future: () => room.setName(textFieldController.text), + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(L10n.of(context)!.displaynameHasBeenChanged))); + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ); +} diff --git a/lib/pangea/utils/set_class_topic.dart b/lib/pangea/utils/set_class_topic.dart new file mode 100644 index 000000000..3a3dae77a --- /dev/null +++ b/lib/pangea/utils/set_class_topic.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +void setClassTopic(Room room, BuildContext context) { + final TextEditingController textFieldController = + TextEditingController(text: room.topic); + showDialog( + context: context, + useRootNavigator: false, + builder: (BuildContext context) => AlertDialog( + title: Text( + room.isSpace + ? L10n.of(context)!.classDescription + : L10n.of(context)!.chatTopic, + ), + content: TextField( + controller: textFieldController, + ), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(L10n.of(context)!.ok), + onPressed: () async { + if (textFieldController.text == "") return; + final success = await showFutureLoadingDialog( + context: context, + future: () => room.setDescription(textFieldController.text), + ); + if (success.error == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(L10n.of(context)!.groupDescriptionHasBeenChanged), + ), + ); + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ); +} \ No newline at end of file diff --git a/lib/pangea/utils/shared_prefs.dart b/lib/pangea/utils/shared_prefs.dart new file mode 100644 index 000000000..5b3fc0025 --- /dev/null +++ b/lib/pangea/utils/shared_prefs.dart @@ -0,0 +1,36 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; + +class MyShared { + static saveString(String key, String value) async { + final SharedPreferences _prefs = await SharedPreferences.getInstance(); + _prefs.setString(key, value); + } + + static Future? readString(String key) async { + final SharedPreferences _prefs = await SharedPreferences.getInstance(); + String? source = _prefs.getString(key); + return source; + } + + static saveJson(String key, Map value) async { + final SharedPreferences _prefs = await SharedPreferences.getInstance(); + _prefs.setString(key, json.encode(value)); + } + + static Future? readJson(String key) async { + try { + final SharedPreferences _prefs = await SharedPreferences.getInstance(); + String? source = _prefs.getString(key); + + if (source == null) { + return null; + } + var decodedJson = json.decoder.convert(source); + //var decodedJson = json.decode(source); + return decodedJson; + } catch (err) { + return null; + } + } +} diff --git a/lib/pangea/utils/space_navigator.dart b/lib/pangea/utils/space_navigator.dart new file mode 100644 index 000000000..af72a0b25 --- /dev/null +++ b/lib/pangea/utils/space_navigator.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// this is a workaround to allow navigation of spaces out from any widget. +/// Reason is that we have no reliable way to listen on *query* changes of +/// VRouter. +/// +/// Time wasted: 3h +abstract class SpaceNavigator { + const SpaceNavigator._(); + + // TODO(TheOneWithTheBraid): adjust routing table in order to represent spaces + // ... in any present path + static final routeObserver = RouteObserver(); + + static final StreamController _controller = + StreamController.broadcast(); + + static Stream get stream => _controller.stream; + + static void navigateToSpace(String? spaceId) => _controller.add(spaceId); +} diff --git a/lib/pangea/utils/subscription_app_id.dart b/lib/pangea/utils/subscription_app_id.dart new file mode 100644 index 000000000..20a69d4a2 --- /dev/null +++ b/lib/pangea/utils/subscription_app_id.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +class SubscriptionAppIds { + String? stripeId; + String? androidId; + String? appleId; + + SubscriptionAppIds(); + + String? get currentAppId => kIsWeb + ? stripeId + : Platform.isAndroid + ? androidId + : appleId; + + String? appDisplayName(String appId) { + if (appId == stripeId) return "web"; + if (appId == androidId) return "Google Play Store"; + if (appId == appleId) return "Apple App Store"; + return null; + } + + factory SubscriptionAppIds.fromJson(json) { + final SubscriptionAppIds appIds = SubscriptionAppIds(); + for (final appInfo in (json['items'] as List)) { + final String platform = appInfo['type']; + final String appId = appInfo['id']; + switch (platform) { + case 'stripe': + appIds.stripeId = appId; + continue; + case 'app_store': + appIds.appleId = appId; + continue; + case 'play_store': + appIds.androidId = appId; + continue; + } + } + return appIds; + } +} + +enum RCPlatform { + stripe, + android, + apple, +} + +class SubscriptionPlatform { + RCPlatform currentPlatform = kIsWeb + ? RCPlatform.stripe + : Platform.isAndroid + ? RCPlatform.android + : RCPlatform.apple; + + @override + String toString() { + return currentPlatform == RCPlatform.stripe + ? 'stripe' + : currentPlatform == RCPlatform.android + ? 'play_store' + : 'app_store'; + } +} diff --git a/lib/pangea/utils/sync_status_util.dart b/lib/pangea/utils/sync_status_util.dart new file mode 100644 index 000000000..dacff01d4 --- /dev/null +++ b/lib/pangea/utils/sync_status_util.dart @@ -0,0 +1,123 @@ +import 'dart:async'; + +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; + +class PLoadingStatus extends StatefulWidget { + final void Function()? onFinish; + + final Widget child; + final Widget? shimmerChild; + const PLoadingStatus( + {Key? key, required this.child, this.onFinish, this.shimmerChild}) + : super(key: key); + + @override + PLoadingStatusState createState() => PLoadingStatusState(); +} + +class PLoadingStatusState extends State { + late final StreamSubscription _onSyncSub; + bool isFinished = false; + @override + void initState() { + _onSyncSub = Matrix.of(context).client.onSyncStatus.stream.listen( + (status) => setState(() { + if (status.status == SyncStatus.finished) { + //PTODO - test this + if (!isFinished) { + isFinished = true; + widget.onFinish?.call(); + } + } + }), + ); + super.initState(); + } + + @override + void dispose() { + _onSyncSub.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Client client = Matrix.of(context).client; + final SyncStatusUpdate status = client.onSyncStatus.value ?? + const SyncStatusUpdate(SyncStatus.waitingForResponse); + final bool hide = client.onSync.value != null && + status.status != SyncStatus.error && + client.prevBatch != null; + + final shimmerComponent = widget.shimmerChild == null + ? PangeaDefaultShimmer(hide: hide, status: status) + : widget.shimmerChild!; + + return hide ? widget.child : shimmerComponent; + } +} + +class PangeaDefaultShimmer extends StatelessWidget { + const PangeaDefaultShimmer({ + Key? key, + required this.hide, + required this.status, + }) : super(key: key); + + final bool hide; + final SyncStatusUpdate status; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.bounceInOut, + height: hide ? 0 : 36, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + value: hide ? 1.0 : status.progress, + ), + ), + const SizedBox(width: 12), + Text( + status.toLocalizedString(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ); + } +} + +extension on SyncStatusUpdate { + String toLocalizedString(BuildContext context) { + switch (status) { + case SyncStatus.waitingForResponse: + return L10n.of(context)!.loadingPleaseWait; + case SyncStatus.error: + return ((error?.exception ?? Object()) as Object) + .toLocalizedString(context); + case SyncStatus.processing: + case SyncStatus.cleaningUp: + case SyncStatus.finished: + default: + return L10n.of(context)!.synchronizingPleaseWait; + } + } +} diff --git a/lib/pangea/utils/sync_status_util_v2.dart b/lib/pangea/utils/sync_status_util_v2.dart new file mode 100644 index 000000000..5d2ac85e6 --- /dev/null +++ b/lib/pangea/utils/sync_status_util_v2.dart @@ -0,0 +1,123 @@ +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import 'error_handler.dart'; + +class PLoadingStatusV2 extends StatefulWidget { + final void Function()? onFinish; + + final Widget child; + final Widget? shimmerChild; + const PLoadingStatusV2( + {Key? key, required this.child, this.onFinish, this.shimmerChild}) + : super(key: key); + + @override + PLoadingStatusStateV2 createState() => PLoadingStatusStateV2(); +} + +class PLoadingStatusStateV2 extends State { + bool isFinished = false; + @override + void initState() { + _waitForSync(); + super.initState(); + } + + _waitForSync() async { + try { + final Client client = Matrix.of(context).client; + if (client.rooms.isEmpty) await client.roomsLoading; + + // await client.accountDataLoading; + // client.prevBatch == null ? await client.onSync.stream.first : null; + + if (!isFinished) { + isFinished = true; + widget.onFinish?.call(); + } + } catch (err, s) { + ErrorHandler.logError(e: err, s: s); + isFinished = true; + } + } + + @override + Widget build(BuildContext context) { + final Client client = Matrix.of(context).client; + final SyncStatusUpdate status = client.onSyncStatus.value ?? + const SyncStatusUpdate(SyncStatus.waitingForResponse); + final bool hide = client.onSync.value != null && + status.status != SyncStatus.error && + client.prevBatch != null; + + final shimmerComponent = widget.shimmerChild == null + ? PangeaDefaultShimmer(hide: hide, status: status) + : widget.shimmerChild!; + + return isFinished ? widget.child : shimmerComponent; + } +} + +class PangeaDefaultShimmer extends StatelessWidget { + const PangeaDefaultShimmer({ + Key? key, + required this.hide, + required this.status, + }) : super(key: key); + + final bool hide; + final SyncStatusUpdate status; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.bounceInOut, + height: hide ? 0 : 36, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + value: hide ? 1.0 : status.progress, + ), + ), + const SizedBox(width: 12), + Text( + status.toLocalizedString(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), + ], + ), + ); + } +} + +extension on SyncStatusUpdate { + String toLocalizedString(BuildContext context) { + switch (status) { + case SyncStatus.waitingForResponse: + return L10n.of(context)!.loadingPleaseWait; + case SyncStatus.error: + return ((error?.exception ?? Object()) as Object) + .toLocalizedString(context); + case SyncStatus.processing: + case SyncStatus.cleaningUp: + case SyncStatus.finished: + default: + return L10n.of(context)!.synchronizingPleaseWait; + } + } +} diff --git a/lib/pangea/widgets/chat/locked_chat_message.dart b/lib/pangea/widgets/chat/locked_chat_message.dart new file mode 100644 index 000000000..3e5ec93a5 --- /dev/null +++ b/lib/pangea/widgets/chat/locked_chat_message.dart @@ -0,0 +1,34 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class LockedChatMessage extends StatelessWidget { + const LockedChatMessage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + ), + child: Text( + L10n.of(context)!.lockedChatWarning, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14 * AppConfig.fontSizeFactor, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat_list/chat_list_body_text.dart b/lib/pangea/widgets/chat_list/chat_list_body_text.dart new file mode 100644 index 000000000..bd30b99fa --- /dev/null +++ b/lib/pangea/widgets/chat_list/chat_list_body_text.dart @@ -0,0 +1,52 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../pages/chat_list/chat_list.dart'; +import '../../../widgets/matrix.dart'; +import '../../extensions/pangea_room_extension.dart'; + +class ChatListBodyStartText extends StatelessWidget { + const ChatListBodyStartText({ + super.key, + required this.controller, + }); + + final ChatListController controller; + + chatListBodyStartText(BuildContext context) { + // note: only shows if user is in no chats in that space + + final PangeaController pangeaController = MatrixState.pangeaController; + + if (controller.activeSpaceId != null) { + if (Matrix.of(context) + .client + .getRoomById(controller.activeSpaceId!) + ?.isSpaceAdmin ?? + false) { + return L10n.of(context)!.welcomeToYourNewClass; + } + + return L10n.of(context)!.welcomeToClass; + } + + if (pangeaController.permissionsController.isUser18()) { + return L10n.of(context)!.welcomeToPangea18Plus; + } + + return L10n.of(context)!.welcomeToPangeaMinor; + } + + @override + Widget build(BuildContext context) { + return Text( + chatListBodyStartText(context), + textAlign: TextAlign.start, + style: const TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ); + } +} diff --git a/lib/pangea/widgets/chat_list/find_a_partner_tile.dart b/lib/pangea/widgets/chat_list/find_a_partner_tile.dart new file mode 100644 index 000000000..78fe7c7b4 --- /dev/null +++ b/lib/pangea/widgets/chat_list/find_a_partner_tile.dart @@ -0,0 +1,56 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_gen/gen_l10n/l10n.dart'; +// import 'package:vrouter/vrouter.dart'; + +// import '../../../pages/chat_list/chat_list.dart'; + +// class FindALanguagePartnerTile extends StatelessWidget { +// const FindALanguagePartnerTile({ +// Key? key, +// required this.controller, +// }) : super(key: key); + +// final ChatListController controller; + +// @override +// Widget build(BuildContext context) { +// return ListTile( +// leading: Icon( +// Icons.add_circle_outline, +// color: Theme.of(context).colorScheme.onBackground, +// ), +// title: Text(L10n.of(context)!.findALanguagePartner), +// onTap: () { +// if (controller +// .pangeaController.permissionsController.isPublic) { +// Scaffold.of(context).closeDrawer(); +// VRouter.of(context).to('/partner'); +// } else { +// showDialog( +// context: context, +// useRootNavigator: false, +// builder: (context) => AlertDialog( +// title: Text(L10n.of(context)!.setToPublicSettingsTitle), +// content: ConstrainedBox( +// constraints: const BoxConstraints(maxWidth: 250), +// child: Text(L10n.of(context)!.setToPublicSettingsDesc), +// ), +// actions: [ +// TextButton( +// onPressed: Navigator.of(context).pop, +// child: Text(L10n.of(context)!.cancel), +// ), +// TextButton( +// onPressed: () { +// VRouter.of(context).to('/settings/account'); +// }, +// child: Text(L10n.of(context)!.accountSettings), +// ), +// ], +// ), +// ); +// } +// }, +// ); +// } +// } diff --git a/lib/pangea/widgets/class/add_class_and_invite.dart b/lib/pangea/widgets/class/add_class_and_invite.dart new file mode 100644 index 000000000..58442568c --- /dev/null +++ b/lib/pangea/widgets/class/add_class_and_invite.dart @@ -0,0 +1,296 @@ +import 'dart:developer'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../widgets/matrix.dart'; +import '../../utils/error_handler.dart'; +import '../../utils/firebase_analytics.dart'; + +enum AddToClassMode { exchange, chat } + +class AddToClassAndInviteToggles extends StatefulWidget { + final String? roomId; + final bool startOpen; + final Function? setParentState; + final AddToClassMode mode; + + const AddToClassAndInviteToggles({ + Key? key, + this.roomId, + this.startOpen = false, + this.setParentState, + required this.mode, + }) : super(key: key); + + @override + AddToClassAndInviteState createState() => AddToClassAndInviteState(); +} + +class AddToClassAndInviteState extends State { + late Room? room; + late List parents; + late List possibleParents; + late bool isOpen; + + AddToClassAndInviteState({Key? key}); + + @override + void initState() { + room = widget.roomId != null + ? Matrix.of(context).client.getRoomById(widget.roomId!) + : null; + + if (room != null && room!.isPangeaClass) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "should not be able to add class to space, not yet at least", + ); + context.go('/rooms'); + } + + possibleParents = Matrix.of(context) + .client + .rooms + .where( + widget.mode == AddToClassMode.exchange + ? (Room r) => r.isPangeaClass && widget.roomId != r.id + : (Room r) => + (r.isPangeaClass || r.isExchange) && widget.roomId != r.id, + ) + .toList(); + + parents = widget.roomId != null + ? possibleParents + .where( + (r) => + r.spaceChildren.any((room) => room.roomId == widget.roomId), + ) + .toList() + : []; + + isOpen = widget.startOpen; + + super.initState(); + } + + Future addParents(String roomToAddId) async { + final List> addFutures = []; + for (final Room newParent in parents) { + addFutures.add(_addSingleParent(roomToAddId, newParent)); + } + await addFutures.wait; + } + + Future _addSingleParent(String roomToAddId, Room newParent) async { + GoogleAnalytics.addParent(roomToAddId, newParent.classCode); + final List> existingMembers = await Future.wait([ + room!.requestParticipants(), + newParent.requestParticipants(), + ]); + final List roomMembers = existingMembers[0]; + final List spaceMembers = existingMembers[1]; + + final List> inviteFutures = [ + newParent.setSpaceChild(roomToAddId, suggested: true), + ]; + for (final spaceMember + in spaceMembers.where((element) => element.id != room!.client.userID)) { + if (!roomMembers.any( + (m) => m.id == spaceMember.id && m.membership == Membership.join, + )) { + inviteFutures.add(_inviteSpaceMember(spaceMember)); + } else { + debugPrint('User ${spaceMember.id} is already in the room'); + } + } + await Future.wait(inviteFutures); + return; + } + + //function for kicking single student and haandling error + Future _kickSpaceMember(User spaceMember) async { + try { + await room!.kick(spaceMember.id); + debugPrint('Kicked ${spaceMember.id}'); + } catch (e) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e); + } + } + + //function for adding single student and haandling error + Future _inviteSpaceMember(User spaceMember) async { + try { + await room!.invite(spaceMember.id); + debugPrint('added ${spaceMember.id}'); + } catch (e) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e); + } + } + + //remove single class + Future _removeSingleSpaceFromParents( + String roomToRemoveId, + Room spaceToRemove, + ) async { + GoogleAnalytics.removeChatFromClass( + roomToRemoveId, + spaceToRemove.classCode, + ); + + if (room == null) { + ErrorHandler.logError(m: 'Room is null in kickSpaceMembers'); + debugger(when: kDebugMode); + return; + } + final List> roomsMembers = await Future.wait([ + room!.requestParticipants(), + spaceToRemove.requestParticipants(), + ]); + + final List toKick = roomsMembers[1] + .where( + (element) => + element.id != room!.client.userID && + roomsMembers[0].any((m) => m.id == element.id), + ) + .toList(); + + final List> kickFutures = [ + spaceToRemove.removeSpaceChild(roomToRemoveId), + ]; + for (final spaceMember in toKick) { + kickFutures.add(_kickSpaceMember(spaceMember)); + } + await Future.wait(kickFutures); + + // if (widget.setParentState != null) { + // widget.setParentState!(); + // } + await room!.requestParticipants(); + + GoogleAnalytics.kickClassFromExchange(room!.id, spaceToRemove.id); + return; + } + + // ignore: curly_braces_in_flow_control_structures + Future _handleAdd(bool add, Room possibleParent) async { + //in this case, the room has already been made so we handle adding as it happens + if (room != null) { + await showFutureLoadingDialog( + context: context, + future: () async { + await (add + ? _addSingleParent(room!.id, possibleParent) + : _removeSingleSpaceFromParents(room!.id, possibleParent)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + add + ? L10n.of(context)!.youAddedToSpace( + room!.name, + possibleParent.name, + ) + : L10n.of(context)!.youRemovedFromSpace( + room!.name, + possibleParent.name, + ), + ), + ), + ); + }, + ); + } + setState( + () => add + ? parents.add(possibleParent) + : parents.removeWhere((r) => r.id == possibleParent.id), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (!widget.startOpen) + ListTile( + enableFeedback: !widget.startOpen, + title: Text( + widget.mode == AddToClassMode.exchange + ? L10n.of(context)!.addToClass + : L10n.of(context)!.addToClassOrExchange, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon( + Icons.workspaces_outline, + ), + ), + trailing: !widget.startOpen + ? Icon( + isOpen + ? Icons.keyboard_arrow_down_outlined + : Icons.keyboard_arrow_right_outlined, + ) + : null, + onTap: () => setState(() => isOpen = !isOpen), + ), + if (isOpen) + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), + child: Column( + children: [ + if (possibleParents.isEmpty) + ListTile( + title: Text(L10n.of(context)!.noEligibleSpaces), + ), + ListView.builder( + shrinkWrap: true, + itemCount: possibleParents.length, + itemBuilder: (BuildContext context, int i) { + final bool canIAddSpaceChildren = + possibleParents[i].canIAddSpaceChild(room) && + (room?.canIAddSpaceParents ?? true); + return Column( + children: [ + Opacity( + opacity: canIAddSpaceChildren ? 1 : 0.5, + child: SwitchListTile.adaptive( + title: possibleParents[i].nameAndRoomTypeIcon(), + activeColor: AppConfig.activeToggleColor, + value: parents + .any((r) => r.id == possibleParents[i].id), + onChanged: (bool add) => canIAddSpaceChildren + ? _handleAdd(add, possibleParents[i]) + : ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(L10n.of(context)!.noPermission), + ), + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/class/add_space_toggles.dart b/lib/pangea/widgets/class/add_space_toggles.dart new file mode 100644 index 000000000..2b72c7701 --- /dev/null +++ b/lib/pangea/widgets/class/add_space_toggles.dart @@ -0,0 +1,277 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../widgets/matrix.dart'; +import '../../utils/firebase_analytics.dart'; +import 'add_class_and_invite.dart'; + +//PTODO - auto invite students when you add a space and delete the add_class_and_invite.dart file +class AddToSpaceToggles extends StatefulWidget { + final String? roomId; + final bool startOpen; + final String? activeSpaceId; + final AddToClassMode mode; + + const AddToSpaceToggles( + {Key? key, + this.roomId, + this.startOpen = false, + this.activeSpaceId, + required this.mode}) + : super(key: key); + + @override + AddToSpaceState createState() => AddToSpaceState(); +} + +class AddToSpaceState extends State { + late Room? room; + late List parents; + late List possibleParents; + late bool isOpen; + + AddToSpaceState({Key? key}); + + @override + void initState() { + //if roomId is null, it means this widget is being used in the creation flow + room = widget.roomId != null + ? Matrix.of(context).client.getRoomById(widget.roomId!) + : null; + + possibleParents = Matrix.of(context) + .client + .rooms + .where(widget.mode == AddToClassMode.exchange + ? (Room r) => r.isPangeaClass && widget.roomId != r.id + : (Room r) => + (r.isPangeaClass || r.isExchange) && widget.roomId != r.id) + .toList(); + + parents = widget.roomId != null + ? possibleParents + .where((r) => + r.spaceChildren.any((room) => room.roomId == widget.roomId)) + .map((r) => SuggestionStatus(false, r)) + .cast() + .toList() + : []; + + if (widget.activeSpaceId != null) { + final activeSpace = + Matrix.of(context).client.getRoomById(widget.activeSpaceId!); + if (activeSpace != null) { + parents.add(SuggestionStatus(false, activeSpace)); + } else { + ErrorHandler.logError( + e: Exception('activeSpaceId ${widget.activeSpaceId} not found'), + ); + } + } + + //sort possibleParents + //if possibleParent in parents, put first + //use sort but use any instead of contains because contains uses == and we want to compare by id + possibleParents.sort((a, b) { + if (parents.any((suggestionStatus) => suggestionStatus.room.id == a.id)) { + return -1; + } else if (parents + .any((suggestionStatus) => suggestionStatus.room.id == b.id)) { + return 1; + } else { + return a.name.compareTo(b.name); + } + }); + + isOpen = widget.startOpen; + initSuggestedParents(); + super.initState(); + } + + Future initSuggestedParents() async { + if (room != null) { + for (var i = 0; i < parents.length; i++) { + final parent = parents[i]; + final bool suggested = + await room?.suggestedInSpace(parent.room) ?? false; + parents[i].suggested = suggested; + } + setState(() {}); + } + } + + Future _addSingleSpace(String roomToAddId, Room newParent) { + GoogleAnalytics.addParent(roomToAddId, newParent.classCode); + return newParent.setSpaceChild(roomToAddId, + suggested: isSuggestedInSpace(newParent)); + } + + Future addSpaces(String roomToAddId) async { + final List> addFutures = []; + for (final SuggestionStatus newParent in parents) { + addFutures.add(_addSingleSpace(roomToAddId, newParent.room)); + } + await addFutures.wait; + } + + Future handleAdd(bool add, Room possibleParent) async { + //in this case, the room has already been made so we handle adding as it happens + if (room != null) { + await showFutureLoadingDialog( + context: context, + future: () => add + ? _addSingleSpace(room!.id, possibleParent) + : possibleParent.removeSpaceChild(room!.id), + ); + } + + setState( + () => add + ? parents.add(SuggestionStatus(false, possibleParent)) + : parents.removeWhere((suggestionStatus) => + suggestionStatus.room.id == possibleParent.id), + ); + } + + Future setSuggested(bool suggest, Room possibleParent) async { + if (room != null) { + await showFutureLoadingDialog( + context: context, + future: () => room!.setSuggestedInSpace(suggest, possibleParent), + ); + } + + for (final SuggestionStatus suggestionStatus in parents) { + if (suggestionStatus.room.id == possibleParent.id) { + suggestionStatus.suggested = suggest; + } + } + + setState(() {}); + } + + bool isSuggestedInSpace(Room parent) => + parents.firstWhereOrNull((r) => r.room.id == parent.id)?.suggested ?? + false; + + Widget getAddToSpaceToggleItem(int index) { + final Room possibleParent = possibleParents[index]; + final String possibleParentName = possibleParent.getLocalizedDisplayname(); + final bool canAdd = possibleParent.canIAddSpaceChild(room); + + return Opacity( + opacity: canAdd ? 1 : 0.5, + child: Column( + children: [ + SwitchListTile.adaptive( + title: possibleParent.nameAndRoomTypeIcon(), + activeColor: AppConfig.activeToggleColor, + value: parents.any((r) => r.room.id == possibleParent.id), + onChanged: (bool add) => canAdd + ? handleAdd(add, possibleParent) + : ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.noPermission), + ), + ), + ), + if (parents.any((r) => r.room.id == possibleParent.id)) + SwitchListTile.adaptive( + title: Text( + L10n.of(context)!.suggestTo(possibleParentName), + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + subtitle: Text( + widget.mode == AddToClassMode.chat + ? L10n.of(context)!.suggestChatDesc(possibleParentName) + : L10n.of(context)!.suggestExchangeDesc(possibleParentName), + ), + activeColor: AppConfig.activeToggleColor, + value: isSuggestedInSpace(possibleParent), + onChanged: (bool suggest) => + setSuggested(suggest, possibleParent), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final String title = widget.mode == AddToClassMode.exchange + ? L10n.of(context)!.addToClass + : L10n.of(context)!.addToClassOrExchange; + final String subtitle = widget.mode == AddToClassMode.exchange + ? L10n.of(context)!.addToClassDesc + : L10n.of(context)!.addToClassOrExchangeDesc; + final scrollController = ScrollController(); + + return Column( + children: [ + ListTile( + title: Text( + title, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text(subtitle), + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon(Icons.workspaces_outlined), + ), + trailing: Icon( + isOpen + ? Icons.keyboard_arrow_down_outlined + : Icons.keyboard_arrow_right_outlined, + ), + onTap: () { + setState(() => isOpen = !isOpen); + }, + ), + if (isOpen) + Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + const Divider(height: 1), + SizedBox( + height: min(possibleParents.length * 55, 500), + child: ListView.builder( + shrinkWrap: true, + itemCount: possibleParents.length, + itemBuilder: (BuildContext context, int i) { + return getAddToSpaceToggleItem(i); + }, + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class SuggestionStatus { + bool suggested; + final Room room; + + SuggestionStatus(this.suggested, this.room); +} diff --git a/lib/pangea/widgets/class/invite_students_from_class.dart b/lib/pangea/widgets/class/invite_students_from_class.dart new file mode 100644 index 000000000..70bafeb7d --- /dev/null +++ b/lib/pangea/widgets/class/invite_students_from_class.dart @@ -0,0 +1,342 @@ +// import 'dart:developer'; + +// import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_gen/gen_l10n/l10n.dart'; +// import 'package:future_loading_dialog/future_loading_dialog.dart'; +// import 'package:matrix/matrix.dart'; + +// import '../../../utils/matrix_sdk_extensions/matrix_locals.dart'; +// import '../../../widgets/avatar.dart'; +// import '../../../widgets/matrix.dart'; +// import '../../utils/error_handler.dart'; +// import '../../utils/firebase_analytics.dart'; + +// class InviteStudentsFromClass extends StatefulWidget { +// final String? roomId; +// final bool startOpen; +// final Function setParentState; + +// const InviteStudentsFromClass({ +// Key? key, +// this.roomId, +// this.startOpen = false, +// required this.setParentState, +// }) : super(key: key); + +// @override +// InviteStudentsFromClassState createState() => InviteStudentsFromClassState(); +// } + +// class InviteStudentsFromClassState extends State { +// late Room? room; +// late List otherSpaces; +// late bool isOpen; +// final List invitedSpaces = []; +// final List kickedSpaces = []; + +// InviteStudentsFromClassState({Key? key, cont}); + +// @override +// void initState() { +// room = widget.roomId != null +// ? Matrix.of(context).client.getRoomById(widget.roomId!) +// : null; + +// otherSpaces = Matrix.of(context) +// .client +// .rooms +// .where((r) => r.isPangeaClass && r.id != widget.roomId) +// .toList(); + +// isOpen = widget.startOpen; + +// super.initState(); +// } + +// Future inviteSpaceMembers(BuildContext context, Room spaceToInvite) => +// showFutureLoadingDialog( +// context: context, +// future: () async { +// if (room == null) { +// ErrorHandler.logError(m: 'Room is null in inviteSpaceMembers'); +// debugger(when: kDebugMode); +// return; +// } +// final List> existingMembers = await Future.wait([ +// room!.requestParticipants(), +// spaceToInvite.requestParticipants(), +// ]); +// final List roomMembers = existingMembers[0]; +// final List spaceMembers = existingMembers[1]; +// final List> inviteFutures = []; +// for (final spaceMember in spaceMembers +// .where((element) => element.id != room!.client.userID)) { +// if (!roomMembers.any((m) => +// m.id == spaceMember.id && m.membership == Membership.join)) { +// inviteFutures.add(inviteSpaceMember(spaceMember)); +// //add to invitedSpaces +// invitedSpaces.add(spaceToInvite.id); +// //if in kickedSpaces, remove +// kickedSpaces.remove(spaceToInvite.id); +// } else { +// debugPrint('User ${spaceMember.id} is already in the room'); +// } +// } +// await Future.wait(inviteFutures); +// debugPrint('Invited ${spaceMembers.length} members'); +// GoogleAnalytics.inviteClassToExchange(room!.id, spaceToInvite.id); +// // setState(() { +// // widget.setParentState(); +// // }); +// }, +// onError: handleError, +// ); + +// //function for kicking single student and haandling error +// Future kickSpaceMember(User spaceMember) async { +// try { +// await room!.kick(spaceMember.id); +// debugPrint('Kicked ${spaceMember.id}'); +// } catch (e) { +// debugger(when: kDebugMode); +// ErrorHandler.logError(e: e); +// } +// } + +// //function for adding single student and haandling error +// Future inviteSpaceMember(User spaceMember) async { +// try { +// await room!.invite(spaceMember.id); +// debugPrint('added ${spaceMember.id}'); +// } catch (e) { +// debugger(when: kDebugMode); +// ErrorHandler.logError(e: e); +// } +// } + +// Future kickSpaceMembers(BuildContext context, Room spaceToKick) => +// showFutureLoadingDialog( +// context: context, +// future: () async { +// if (room == null) { +// ErrorHandler.logError(m: 'Room is null in kickSpaceMembers'); +// debugger(when: kDebugMode); +// return; +// } +// final List> existingMembers = await Future.wait([ +// room!.requestParticipants(), +// spaceToKick.requestParticipants(), +// ]); +// final List roomMembers = existingMembers[0]; +// final List spaceMembers = existingMembers[1]; +// final List toKick = spaceMembers +// .where((element) => +// element.id != room!.client.userID && +// roomMembers.any((m) => m.id == element.id)) +// .toList(); +// //add to kickedSpaces +// kickedSpaces.add(spaceToKick.id); +// //if in invitedSpaces, remove from invitedSpaces +// invitedSpaces.remove(spaceToKick.id); +// final List> kickFutures = []; + +// for (final spaceMember in toKick) { +// kickFutures.add(kickSpaceMember(spaceMember)); +// } +// await Future.wait(kickFutures); +// GoogleAnalytics.kickClassFromExchange(room!.id, spaceToKick.id); +// setState(() { +// widget.setParentState(); +// }); +// return; +// }, +// onError: handleError, +// ); + +// String handleError(dynamic exception) { +// ErrorHandler.logError( +// e: exception, m: 'Error inviting or kicking students'); +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text(exception.toString()), +// ), +// ); +// return exception.toString(); +// } + +// @override +// Widget build(BuildContext context) { +// if (room == null) return Container(); +// return Column( +// children: [ +// ListTile( +// title: Text( +// L10n.of(context)!.inviteStudentsFromOtherClasses, +// style: TextStyle( +// color: Theme.of(context).colorScheme.secondary, +// fontWeight: FontWeight.bold, +// ), +// ), +// leading: CircleAvatar( +// backgroundColor: Theme.of(context).primaryColor, +// foregroundColor: Colors.white, +// radius: Avatar.defaultSize / 2, +// child: const Icon(Icons.workspaces_outlined), +// ), +// trailing: Icon( +// isOpen +// ? Icons.keyboard_arrow_down_outlined +// : Icons.keyboard_arrow_right_outlined, +// ), +// onTap: () => setState(() => isOpen = !isOpen), +// ), +// if (isOpen) +// Padding( +// padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 0), +// child: Column( +// children: [ +// if (otherSpaces.isEmpty) +// ListTile(title: Text(L10n.of(context)!.noEligibleSpaces)), +// ListView.builder( +// shrinkWrap: true, +// itemCount: otherSpaces.length, +// itemBuilder: (BuildContext context, int i) { +// final bool canIAddSpaceChildren = +// otherSpaces[i].canIAddSpaceChild(room); +// return Column( +// children: [ +// Opacity( +// opacity: canIAddSpaceChildren ? 1 : 0.5, +// child: InviteKickClass( +// room: otherSpaces[i], +// inviteCallback: (Room room) => +// inviteSpaceMembers(context, otherSpaces[i]), +// kickCallback: (Room room) => +// kickSpaceMembers(context, otherSpaces[i]), +// controller: this, +// ), +// ), +// ], +// ); +// }, +// ), +// ], +// ), +// ), +// // END: ed8c6549bwf9 +// ], +// ); +// } +// } + +// //listTile with two buttons - one to invite all students in the class and the other to kick them all out +// // parameters +// // 1. Room +// // 2. invite callback +// // 3. kick callback +// // when the user clicks either button, a dialog pops up asking for confirmation +// // after the dialog is confirmed, the callback is executed +// class InviteKickClass extends StatelessWidget { +// final Room room; +// final Function inviteCallback; +// final Function kickCallback; +// final InviteStudentsFromClassState controller; + +// const InviteKickClass({ +// Key? key, +// required this.room, +// required this.inviteCallback, +// required this.kickCallback, +// required this.controller, +// }) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return ListTile( +// title: Text( +// room.getLocalizedDisplayname( +// MatrixLocals(L10n.of(context)!), +// ), +// ), +// leading: const SizedBox( +// height: Avatar.defaultSize, +// width: Avatar.defaultSize, +// ), +// trailing: Row( +// mainAxisSize: MainAxisSize.min, +// children: [ +// IconButton( +// icon: const Icon(Icons.add), +// isSelected: controller.invitedSpaces.contains(room.id), +// onPressed: () async { +// final bool? result = await showDialog( +// context: context, +// builder: (BuildContext context) => AlertDialog( +// title: Text( +// L10n.of(context)!.inviteAllStudents, +// style: TextStyle( +// color: Theme.of(context).colorScheme.secondary, +// fontWeight: FontWeight.bold, +// ), +// ), +// content: Text( +// L10n.of(context)!.inviteAllStudentsConfirmation, +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.of(context).pop(false), +// child: Text(L10n.of(context)!.cancel), +// ), +// TextButton( +// onPressed: () => Navigator.of(context).pop(true), +// child: Text(L10n.of(context)!.ok), +// ), +// ], +// ), +// ); +// if (result != null && result) { +// inviteCallback(room); +// } +// }, +// ), +// IconButton( +// icon: const Icon(Icons.remove), +// isSelected: controller.kickedSpaces.contains(room.id), +// onPressed: () async { +// final bool? result = await showDialog( +// context: context, +// builder: (BuildContext context) => AlertDialog( +// title: Text( +// L10n.of(context)!.kickAllStudents, +// style: TextStyle( +// color: Theme.of(context).colorScheme.secondary, +// fontWeight: FontWeight.bold, +// ), +// ), +// content: Text( +// L10n.of(context)!.kickAllStudentsConfirmation, +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.of(context).pop(false), +// child: Text(L10n.of(context)!.cancel), +// ), +// TextButton( +// onPressed: () => Navigator.of(context).pop(true), +// child: Text(L10n.of(context)!.ok), +// ), +// ], +// ), +// ); +// if (result != null && result) { +// kickCallback(room); +// } +// }, +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/pangea/widgets/class/join_with_link.dart b/lib/pangea/widgets/class/join_with_link.dart new file mode 100644 index 000000000..ef844e2ac --- /dev/null +++ b/lib/pangea/widgets/class/join_with_link.dart @@ -0,0 +1,75 @@ +import 'package:fluffychat/pangea/constants/url_query_parameter_keys.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/utils/class_code.dart'; +import 'package:fluffychat/widgets/layouts/empty_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../widgets/matrix.dart'; +import '../../constants/local.key.dart'; +import '../../utils/error_handler.dart'; + +//if on home with classcode in url and not logged in, then save it soemhow and after llogin, join class automatically +//if on home with classcode in url and logged in, then join class automatically +class JoinClassWithLink extends StatefulWidget { + const JoinClassWithLink({Key? key}) : super(key: key); + + @override + State createState() => _JoinClassWithLinkState(); +} + +//PTODO - show class info in field so they know they're joining the right class +class _JoinClassWithLinkState extends State { + String? classCode; + final PangeaController _pangeaController = MatrixState.pangeaController; + + @override + void initState() { + super.initState(); + + Future.delayed(Duration.zero, () { + classCode = GoRouterState.of(context) + .uri + .queryParameters[UrlQueryParameterKeys.classCode]; + + if (classCode == null) { + return ClassCodeUtil.messageDialog( + context, + L10n.of(context)!.unableToFindClassCode, + () => context.go("/rooms"), + ); + } + + if (!Matrix.of(context).client.isLogged()) { + return ClassCodeUtil.messageDialog( + context, L10n.of(context)!.pleaseLoginFirst, () async { + await _pangeaController.pStoreService.save( + PLocalKey.cachedClassCodeToJoin, + classCode, + addClientIdToKey: false, + ); + context.go("/home"); + }); + } + + _pangeaController.classController + .joinClasswithCode( + context, + classCode!, + ) + .onError( + (error, stackTrace) => ClassCodeUtil.messageSnack( + context, + ErrorCopy(context, error).body, + ), + ) + .whenComplete( + () => context.go("/rooms"), + ); + }); + } + + @override + Widget build(BuildContext context) => const EmptyPage(); +} diff --git a/lib/pangea/widgets/common/aligned_dialog.dart b/lib/pangea/widgets/common/aligned_dialog.dart new file mode 100644 index 000000000..c08513300 --- /dev/null +++ b/lib/pangea/widgets/common/aligned_dialog.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; + +Future showAlignedDialog({ + required BuildContext context, + required WidgetBuilder builder, + bool barrierDismissible = true, + Color? barrierColor = Colors.black54, + String? barrierLabel, + bool useRootNavigator = true, + RouteSettings? routeSettings, + Alignment followerAnchor = Alignment.center, + Alignment targetAnchor = Alignment.center, + Size? refChildSize, + Offset offset = Offset.zero, + bool avoidOverflow = false, + bool isGlobal = false, + RouteTransitionsBuilder? transitionsBuilder, + Duration? duration, +}) { + assert(debugCheckHasMaterialLocalizations(context)); + + final CapturedThemes themes = InheritedTheme.capture( + from: context, + to: Navigator.of( + context, + rootNavigator: useRootNavigator, + ).context, + ); + + final RenderBox targetBox = context.findRenderObject()! as RenderBox; + final RenderBox overlay = + Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; + Offset position = targetBox + .localToGlobal(targetAnchor.alongSize(targetBox.size), ancestor: overlay); + + if (isGlobal) { + position = overlay.localToGlobal(followerAnchor.alongSize(overlay.size)); + } + + return Navigator.of(context, rootNavigator: useRootNavigator) + .push(AlignedDialogRoute( + followerAlignment: followerAnchor, + position: position, + context: context, + builder: builder, + barrierColor: barrierColor, + barrierDismissible: barrierDismissible, + barrierLabel: barrierLabel, + useSafeArea: isGlobal == true, + settings: routeSettings, + themes: themes, + transitionsBuilder: transitionsBuilder, + duration: duration, + avoidOverflow: avoidOverflow, + offset: offset, + )); +} + +class AlignedDialogRoute extends RawDialogRoute { + /// A dialog route with Material entrance and exit animations, + /// modal barrier color, and modal barrier behavior (dialog is dismissible + /// with a tap on the barrier). + AlignedDialogRoute({ + required BuildContext context, + required WidgetBuilder builder, + required Alignment followerAlignment, + required Offset position, + CapturedThemes? themes, + Color? barrierColor = Colors.transparent, + bool barrierDismissible = true, + String? barrierLabel, + bool useSafeArea = false, + RouteSettings? settings, + RouteTransitionsBuilder? transitionsBuilder, + Duration? duration, + bool avoidOverflow = false, + Offset offset = Offset.zero, + }) : super( + pageBuilder: (BuildContext buildContext, Animation animation, + Animation secondaryAnimation) { + final Widget pageChild = Builder(builder: builder); + Widget dialog = Builder( + builder: (BuildContext context) { + final MediaQueryData mediaQuery = MediaQuery.of(context); + return CustomSingleChildLayout( + delegate: _FollowerDialogRouteLayout( + followerAlignment, + position, + Directionality.of(context), + mediaQuery.padding.top, + mediaQuery.padding.bottom, + offset, + avoidOverflow, + ), + child: pageChild, + ); + }, + ); + dialog = themes?.wrap(dialog) ?? dialog; + if (useSafeArea) { + dialog = SafeArea(child: dialog); + } + return dialog; + }, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel ?? + MaterialLocalizations.of(context).modalBarrierDismissLabel, + transitionDuration: duration ?? const Duration(milliseconds: 200), + transitionBuilder: + transitionsBuilder ?? _buildMaterialDialogTransitions, + settings: settings, + ); +} + +// Positioning of the menu on the screen. +class _FollowerDialogRouteLayout extends SingleChildLayoutDelegate { + _FollowerDialogRouteLayout( + this.followerAnchor, + this.position, + this.textDirection, + this.topPadding, + this.bottomPadding, + this.offset, + this.avoidOverflow, + ); + + final Alignment followerAnchor; + + final Offset position; + + final TextDirection textDirection; + + final double topPadding; + + final double bottomPadding; + + final Offset offset; + final bool avoidOverflow; + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + // The menu can be at most the size of the overlay minus 8.0 pixels in each + // direction. + return BoxConstraints.loose(constraints.biggest) + .deflate(EdgeInsets.only(top: topPadding, bottom: bottomPadding)); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + Offset rst = followerAnchor.alongSize(childSize); + rst = position - rst; + rst += offset; + if (avoidOverflow) { + if (rst.dx < 0) rst = Offset(0, rst.dy); + if (rst.dy < 0) rst = Offset(rst.dx, 0); + if (rst.dx + childSize.width > size.width) { + rst = Offset(size.width - childSize.width, rst.dy); + } + if (rst.dy + childSize.height > size.height) { + rst = Offset(rst.dx, size.height - childSize.height); + } + } + return rst; + } + + @override + bool shouldRelayout(_FollowerDialogRouteLayout oldDelegate) { + return followerAnchor != oldDelegate.followerAnchor || + position != oldDelegate.position || + offset != oldDelegate.offset || + avoidOverflow != oldDelegate.avoidOverflow || + textDirection != oldDelegate.textDirection || + topPadding != oldDelegate.topPadding || + bottomPadding != oldDelegate.bottomPadding; + } +} + +Widget _buildMaterialDialogTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + return FadeTransition( + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeOut, + ), + child: child, + ); +} + +Offset _buildOffSet(BuildContext context, + {required Size refChildSize, required Offset offset}) { + final Size screenSize = MediaQuery.of(context).size; + final Size maxAvilableArea = Size(screenSize.width - refChildSize.width, + screenSize.height - refChildSize.height); + return const Offset(0, 0); +} diff --git a/lib/pangea/widgets/common/bot_face_svg.dart b/lib/pangea/widgets/common/bot_face_svg.dart new file mode 100644 index 000000000..4fcf07fee --- /dev/null +++ b/lib/pangea/widgets/common/bot_face_svg.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +enum BotExpression { surprised, right, addled, left, down, shocked } + +class BotFace extends StatelessWidget { + const BotFace({ + Key? key, + required this.width, + required this.expression, + this.forceColor, + }) : super(key: key); + + final double width; + final Color? forceColor; + final BotExpression expression; + + @override + Widget build(BuildContext context) { + return Image.asset( + 'assets/pangea/bot_faces/${expression.toString().split('.').last}.png', + // 'assets/pangea/bot_faces/surprised.png', + width: width, + height: width, + // color: forceColor ?? + // (Theme.of(context).brightness == Brightness.light + // ? Theme.of(context).colorScheme.primary + // : Theme.of(context).colorScheme.primary), + ); + } +} + +// extension ParseToString on BotExpressions { +// String toShortString() { +// return toString().split('.').last; +// } +// } \ No newline at end of file diff --git a/lib/pangea/widgets/common/list_placeholder.dart b/lib/pangea/widgets/common/list_placeholder.dart new file mode 100644 index 000000000..0886967a5 --- /dev/null +++ b/lib/pangea/widgets/common/list_placeholder.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +class ListPlaceholder extends StatelessWidget { + static const dummyChatCount = 5; + + const ListPlaceholder({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final titleColor = + Theme.of(context).textTheme.bodyText1!.color!.withAlpha(100); + final subtitleColor = + Theme.of(context).textTheme.bodyText1!.color!.withAlpha(50); + + return ListView.builder( + itemCount: dummyChatCount, + itemBuilder: (context, i) => Opacity( + opacity: (dummyChatCount - i) / dummyChatCount, + child: Material( + child: ListTile( + leading: CircleAvatar( + backgroundColor: titleColor, + child: CircularProgressIndicator( + strokeWidth: 1, + color: Theme.of(context).textTheme.bodyText1!.color, + ), + ), + title: Row( + children: [ + Expanded( + child: Container( + height: 14, + decoration: BoxDecoration( + color: titleColor, + borderRadius: BorderRadius.circular(3), + ), + ), + ), + const SizedBox(width: 36), + Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(14), + ), + ), + const SizedBox(width: 12), + Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(14), + ), + ), + ], + ), + subtitle: Container( + decoration: BoxDecoration( + color: subtitleColor, + borderRadius: BorderRadius.circular(3), + ), + height: 12, + margin: const EdgeInsets.only(right: 22), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/common/p_circular_loader.dart b/lib/pangea/widgets/common/p_circular_loader.dart new file mode 100644 index 000000000..f292d9b23 --- /dev/null +++ b/lib/pangea/widgets/common/p_circular_loader.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class PCircular extends StatelessWidget { + final double? size; + const PCircular({super.key, this.size}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: size ?? 25, + width: size ?? 25, + child: const CircularProgressIndicator()), + ], + ); + } +} diff --git a/lib/pangea/widgets/common/pangea_logo_svg.dart b/lib/pangea/widgets/common/pangea_logo_svg.dart new file mode 100644 index 000000000..6407950e5 --- /dev/null +++ b/lib/pangea/widgets/common/pangea_logo_svg.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class PangeaLogoSvg extends StatelessWidget { + const PangeaLogoSvg({Key? key, required this.width, this.forceColor}) + : super(key: key); + + final double width; + final Color? forceColor; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + 'assets/pangea/pangea_logo.svg', + width: width, + height: width, + color: forceColor ?? + (Theme.of(context).brightness == Brightness.light + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.primary), + ); + } +} diff --git a/lib/pangea/widgets/common/star_rating.dart b/lib/pangea/widgets/common/star_rating.dart new file mode 100644 index 000000000..556b1cd09 --- /dev/null +++ b/lib/pangea/widgets/common/star_rating.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +typedef void RatingChangeCallback(double rating); + +class StarRating extends StatelessWidget { + final int starCount; + final double rating; + final RatingChangeCallback? onRatingChanged; + final Color color; + + StarRating( + {Key? key, + this.starCount = 5, + this.rating = 0, + this.onRatingChanged, + required this.color}) + : super(key: key); + + Widget buildStar(BuildContext context, int index) { + Icon icon; + if (index >= rating) { + icon = const Icon( + Icons.star_border, + size: 20, color: Color(0xffFFC403), + ); + } else if (index > rating - 1 && index < rating) { + icon = Icon( + Icons.star_half, + color: color, + //?? Theme.of(context).primaryColor, + size: 20, + ); + } else { + icon = Icon( + Icons.star, + color: color, + //?? Theme.of(context).primaryColor, + size: 20, + ); + } + return InkResponse( + onTap: + onRatingChanged == null ? null : () => onRatingChanged!(index + 1.0), + child: icon, + ); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: + List.generate(starCount, (index) => buildStar(context, index))); + } +} diff --git a/lib/pangea/widgets/common/switch_list_tile.dart b/lib/pangea/widgets/common/switch_list_tile.dart new file mode 100644 index 000000000..594a61e9d --- /dev/null +++ b/lib/pangea/widgets/common/switch_list_tile.dart @@ -0,0 +1,28 @@ +// import 'package:flutter/material.dart'; + +// import '../../../config/app_config.dart'; + +// class PSwitchListTile extends StatelessWidget { +// final String title; +// final String subtitle; +// final bool value; +// final Function(bool value) onChanged; +// const PSwitchListTile( +// {Key? key, +// required this.title, +// required this.subtitle, +// required this.value, +// required this.onChanged}) +// : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return SwitchListTile.adaptive( +// activeColor: AppConfig.activeToggleColor, +// title: Text(title), +// subtitle: Text(subtitle), +// value: value, +// onChanged: onChanged, +// ); +// } +// } diff --git a/lib/pangea/widgets/common_widgets/edit_list_tile.dart b/lib/pangea/widgets/common_widgets/edit_list_tile.dart new file mode 100644 index 000000000..4b6067371 --- /dev/null +++ b/lib/pangea/widgets/common_widgets/edit_list_tile.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import '../../../utils/url_launcher.dart'; +import '../../../widgets/avatar.dart'; + +class EditClassListTile extends StatefulWidget { + String title = ''; + Function() onTap; + String subtitle = ""; + EditClassListTile( + {Key? key, + required this.title, + required this.onTap, + required this.subtitle}) + : super(key: key); + + @override + State createState() => _EditClassListTileState(); +} + +class _EditClassListTileState extends State { + @override + Widget build(BuildContext context) { + final iconColor = Theme.of(context).textTheme.bodyLarge!.color; + return ListTile( + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: iconColor, + radius: Avatar.defaultSize / 2, + child: const Icon(Icons.edit_outlined), + ), + title: Text('${widget.title}:', + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold)), + subtitle: Text( + widget.subtitle, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).textTheme.bodyMedium!.color, + ), + ), + onTap: widget.onTap, + ); + } +} diff --git a/lib/pangea/widgets/common_widgets/overlay_container.dart b/lib/pangea/widgets/common_widgets/overlay_container.dart new file mode 100644 index 000000000..c836dffdb --- /dev/null +++ b/lib/pangea/widgets/common_widgets/overlay_container.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class OverlayContainer extends StatelessWidget { + final Widget cardToShow; + final Size cardSize; + final Color? borderColor; + + const OverlayContainer({ + Key? key, + required this.cardToShow, + this.cardSize = const Size(300.0, 300.0), + this.borderColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(10, 10, 10, 10), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + // color: Colors.purple, + border: Border.all( + width: 2, + color: borderColor ?? Theme.of(context).colorScheme.primary, + ), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), + ), + constraints: BoxConstraints( + maxWidth: cardSize.width, + maxHeight: cardSize.height, + minWidth: cardSize.width, + minHeight: cardSize.height, + ), + //PTODO - position card above input/message + // margin: const EdgeInsets.all(10), + child: cardToShow, + ); + } +} diff --git a/lib/pangea/widgets/common_widgets/p_input_field.dart b/lib/pangea/widgets/common_widgets/p_input_field.dart new file mode 100644 index 000000000..31bab0e58 --- /dev/null +++ b/lib/pangea/widgets/common_widgets/p_input_field.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class PInputTextField extends StatelessWidget { + TextEditingController controller; + Function(String) onSubmit; + String labelText; + String hintText; + PInputTextField( + {Key? key, + required this.controller, + required this.onSubmit, + required this.labelText, + required this.hintText}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: TextField( + controller: controller, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.go, + onSubmitted: onSubmit, + decoration: InputDecoration( + labelText: labelText, + prefixIcon: const Icon(Icons.people_outlined), + hintText: hintText), + ), + ); + } +} diff --git a/lib/pangea/widgets/flag.dart b/lib/pangea/widgets/flag.dart new file mode 100644 index 000000000..0e05311d9 --- /dev/null +++ b/lib/pangea/widgets/flag.dart @@ -0,0 +1,54 @@ +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:flutter/material.dart'; + +import '../models/language_model.dart'; + +class LanguageFlag extends StatelessWidget { + final LanguageModel? language; + final double size; + const LanguageFlag({ + Key? key, + required this.language, + this.size = 30, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Avatar( + name: language?.langCode, + size: size, + ); + + // return Center( + // child: Container( + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(size / 2), + // boxShadow: [ + // BoxShadow( + // color: Colors.grey.withOpacity(0.2), + // spreadRadius: 1, + // blurRadius: 15, + // offset: const Offset(0, 4), // changes position of shadow + // ), + // ], + // ), + // child: ClipRRect( + // borderRadius: BorderRadius.circular(50), + // child: SizedBox( + // height: size, + // width: size, + // child: language?.languageFlag != null + // ? language!.languageFlag.contains("media/flags") + // ? Image.network(language!.languageFlag) + // : Image.asset( + // language!.languageFlag, + // width: size, + // height: size, + // ) + // : const SizedBox.expand(), + // ), + // ), + // ), + // ); + } +} diff --git a/lib/pangea/widgets/igc/card_error_widget.dart b/lib/pangea/widgets/igc/card_error_widget.dart new file mode 100644 index 000000000..eba38718b --- /dev/null +++ b/lib/pangea/widgets/igc/card_error_widget.dart @@ -0,0 +1,47 @@ +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart'; +import 'package:fluffychat/pangea/widgets/igc/card_header.dart'; +import 'package:flutter/material.dart'; + +class CardErrorWidget extends StatelessWidget { + final Object? error; + final Choreographer? choreographer; + final int? offset; + const CardErrorWidget({ + Key? key, + this.error, + this.choreographer, + this.offset, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final ErrorCopy errorCopy = ErrorCopy(context, error); + + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CardHeader( + text: errorCopy.title, + botExpression: BotExpression.addled, + onClose: () => choreographer != null + ? choreographer!.onMatchError( + cursorOffset: offset, + ) + : null, + ), + const SizedBox(height: 10.0), + Center( + child: Text( + errorCopy.body, + style: BotStyle.text(context), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/igc/card_header.dart b/lib/pangea/widgets/igc/card_header.dart new file mode 100644 index 000000000..4907b5994 --- /dev/null +++ b/lib/pangea/widgets/igc/card_header.dart @@ -0,0 +1,60 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; + +import '../../../widgets/matrix.dart'; +import '../../utils/bot_style.dart'; +import '../common/bot_face_svg.dart'; + +class CardHeader extends StatelessWidget { + const CardHeader({ + Key? key, + required this.text, + required this.botExpression, + this.onClose, + }) : super(key: key); + + final BotExpression botExpression; + final String text; + final void Function()? onClose; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 3.0), + child: BotFace( + width: 50.0, + expression: botExpression, + ), + ), + const SizedBox(width: 5.0), + Expanded( + child: Text( + text, + style: BotStyle.text(context), + textAlign: TextAlign.left, + ), + ), + CircleAvatar( + backgroundColor: AppConfig.primaryColor.withOpacity(0.1), + child: IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: () { + if (onClose != null) onClose!(); + MatrixState.pAnyState.closeOverlay(); + }, + color: Theme.of(context).brightness == Brightness.dark + ? AppConfig.primaryColorLight + : AppConfig.primaryColor, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart new file mode 100644 index 000000000..a9a8ef88d --- /dev/null +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -0,0 +1,247 @@ +import 'dart:developer'; +import 'dart:ui'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../models/igc_text_data_model.dart'; +import '../../models/language_detection_model.dart'; +import '../../models/pangea_match_model.dart'; +import '../../models/pangea_representation_event.dart'; +import '../../utils/bot_style.dart'; +import '../../utils/instructions.dart'; + +class PangeaRichText extends StatefulWidget { + final PangeaMessageEvent pangeaMessageEvent; + final TextStyle? existingStyle; + final bool selected; + final LanguageModel? selectedDisplayLang; + final bool immersionMode; + final bool definitions; + final Choreographer? choreographer; + + const PangeaRichText({ + Key? key, + required this.pangeaMessageEvent, + required this.selected, + required this.selectedDisplayLang, + required this.immersionMode, + required this.definitions, + this.choreographer, + this.existingStyle, + }) : super(key: key); + + @override + PangeaRichTextState createState() => PangeaRichTextState(); +} + +class PangeaRichTextState extends State { + final PangeaController pangeaController = MatrixState.pangeaController; + bool _fetchingRepresentation = false; + bool _fetchingTokens = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + //TODO - take out of build function of every message + // if (areLanguagesSet) { + + final List textSpan = getTextSpan(context); + + final double blur = _fetchingRepresentation && widget.immersionMode ? 5 : 0; + + if (!widget.selected && + widget.selectedDisplayLang != null && + widget.selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) { + pangeaController.instructions.show( + context, + InstructionsEnum.clickMessage, + widget.pangeaMessageEvent.eventId, + ); + } else if (blur > 0) { + pangeaController.instructions.show( + context, + InstructionsEnum.blurMeansTranslate, + widget.pangeaMessageEvent.eventId, + ); + } + + final Widget richText = RichText( + text: TextSpan( + children: [ + ...textSpan, + if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) + // if (widget.selected) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: 5.0), + child: SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.secondaryColor, + ), + ), + ), + ), + ], + ), + ); + + return blur > 0 + ? ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), + child: richText, + ) + : richText; + } + + List getTextSpan(BuildContext context) { + final String? displayLangCode = + widget.selected ? widget.selectedDisplayLang?.langCode : userL2LangCode; + + if (displayLangCode == null || !widget.immersionMode) { + return simpleText(widget.pangeaMessageEvent.body); + } + + if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { + debugger(when: kDebugMode); + return simpleText(widget.pangeaMessageEvent.body); + } + + final RepresentationEvent? repEvent = + widget.pangeaMessageEvent.representationByLanguage( + displayLangCode, + ); + + if (repEvent == null) { + _fetchingRepresentation = true; + + setState(() => {}); + widget.pangeaMessageEvent + .representationByLanguageGlobal( + context: context, + langCode: displayLangCode, + ) + .onError((error, stackTrace) => ErrorHandler.logError()) + .whenComplete(() => setState(() => _fetchingRepresentation = false)); + return simpleText(widget.pangeaMessageEvent.body); + } + + if (repEvent.event?.eventId.contains("web") ?? false) { + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"repEvent.event": repEvent.event?.toJson()})); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "representationByLanguageGlobal returned RepEvent with event ID containing 'web' - ${repEvent.event?.eventId}", + ), + ); + // debugger(when: kDebugMode); + return textWithBotStyle(repEvent, context); + } + + if (!widget.selected || + displayLangCode != userL2LangCode || + !widget.definitions) { + return textWithBotStyle(repEvent, context); + } + + if (repEvent.tokens == null) { + setState(() => _fetchingTokens = true); + repEvent + .tokensGlobal(context) + .onError((error, stackTrace) => ErrorHandler.logError()) + .whenComplete(() => setState(() => _fetchingTokens = false)); + + return textWithBotStyle(repEvent, context); + } + + return IGCTextData( + originalInput: repEvent.text, + fullTextCorrection: repEvent.text, + matches: [], + detections: [LanguageDetection(langCode: displayLangCode)], + tokens: repEvent.tokens!, + enableIT: true, + enableIGC: true, + userL2: userL2LangCode ?? LanguageKeys.unknownLanguage, + userL1: userL1LangCode ?? LanguageKeys.unknownLanguage, + ).constructTokenSpan( + context: context, + defaultStyle: textStyle(repEvent, context), + handleClick: true, + spanCardModel: null, + showTokens: widget.definitions, + transformTargetId: widget.pangeaMessageEvent.eventId, + room: widget.pangeaMessageEvent.room, + ); + } + + List simpleText(String text) => [ + TextSpan( + text: text, + style: widget.existingStyle, + ) + ]; + + List textWithBotStyle( + RepresentationEvent repEvent, BuildContext context) => + [ + TextSpan( + text: repEvent.text, + style: textStyle(repEvent, context), + ) + ]; + + TextStyle? textStyle(RepresentationEvent repEvent, BuildContext context) => + // !repEvent.botAuthored + true + ? widget.existingStyle + : BotStyle.text(context, + existingStyle: widget.existingStyle, setColor: false); + + bool get areLanguagesSet => + userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; + + String? get userL2LangCode => + pangeaController.languageController.activeL2Code( + roomID: widget.pangeaMessageEvent.room.id, + ); + + String? get userL1LangCode => + pangeaController.languageController.activeL1Code( + roomID: widget.pangeaMessageEvent.room.id, + ); + + Future onIgnore() async { + debugPrint("PTODO implement onIgnore"); + } + + Future onITStart() async { + debugPrint("PTODO implement onITStart"); + } + + Future onReplacementSelect( + PangeaMatch pangeaMatch, String replacement) async { + debugPrint("PTODO implement onReplacementSelect"); + } + + Future onSentenceRewrite(String sentenceRewrite) async { + debugPrint("PTODO implement onSentenceRewrite"); + } +} diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart new file mode 100644 index 000000000..64f8aa0a5 --- /dev/null +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -0,0 +1,152 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../choreographer/controllers/choreographer.dart'; +import '../../enum/edit_type.dart'; +import '../../models/pangea_token_model.dart'; +import '../../models/span_card_model.dart'; +import '../../models/widget_measurement.dart'; +import '../../utils/overlay.dart'; + +class PangeaTextController extends TextEditingController { + Choreographer choreographer; + + EditType editType = EditType.keyboard; + WidgetMeasurements? measurements; + PangeaTextController({ + String? text, + required this.choreographer, + }) { + text ??= ''; + this.text = text; + } + bool forceKeepOpen = false; + + setSystemText(String text, EditType type) { + editType = type; + this.text = text; + } + + void onBarSizeChange(Size? size, Offset? position, String? uid) { + measurements = WidgetMeasurements(position: position, size: size, uid: uid); + } + + void onInputTap(BuildContext context, {required FocusNode fNode}) { + fNode.requestFocus(); + forceKeepOpen = true; + if (!context.mounted) { + debugger(when: kDebugMode); + return; + } + if (choreographer.igc.igcTextData == null) return; + + // debugPrint( + // "onInputTap matches are ${choreographer.igc.igcTextData?.matches.map((e) => e.match.rule.id).toList().toString()}"); + + final int tokenIndex = choreographer.igc.igcTextData!.tokenIndexByOffset( + selection.baseOffset, + ); + + if (tokenIndex == -1) return; + + final PangeaToken token = choreographer.igc.igcTextData!.tokens[tokenIndex]; + final int matchIndex = + choreographer.igc.igcTextData!.getTopMatchIndexForOffset( + selection.baseOffset, + ); + final Widget cardToShow = matchIndex != -1 + ? SpanCard( + scm: SpanCardModel( + // igcTextData: choreographer.igc.igcTextData!, + matchIndex: matchIndex, + onReplacementSelect: choreographer.onReplacementSelect, + // may not need this + onSentenceRewrite: ((sentenceRewrite) async { + debugPrint("onSentenceRewrite $tokenIndex $sentenceRewrite"); + }), + onIgnore: () => choreographer.onIgnoreMatch( + cursorOffset: selection.baseOffset), + onITStart: () { + choreographer.onITStart( + choreographer.igc.igcTextData!.matches[matchIndex], + ); + }, + choreographer: choreographer, + ), + roomId: choreographer.roomId, + ) + : WordDataCard( + fullText: text, + fullTextLang: + choreographer.igc.igcTextData!.detections.first.langCode, + word: token.text.content, + //Note: this assumes that the token must be in the target language + //since it didn't have a match + wordLang: choreographer.itController.targetLangCode, + hasInfo: token.hasInfo, + room: choreographer.chatController.room, + ); + + OverlayUtil.showPositionedCard( + context: context, + cardSize: matchIndex != -1 && + choreographer.igc.igcTextData!.matches[matchIndex].isITStart + ? const Size(350, 220) + : const Size(350, 400), + cardToShow: cardToShow, + transformTargetId: choreographer.inputTransformTargetKey, + ); + } + + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + // If the composing range is out of range for the current text, ignore it to + // preserve the tree integrity, otherwise in release mode a RangeError will + // be thrown and this EditableText will be built with a broken subtree. + // debugPrint("composing? $withComposing"); + // if (!value.isComposingRangeValid || !withComposing) { + // debugPrint("just returning straight text"); + // // debugger(when: kDebugMode); + // return TextSpan(style: style, text: text); + // } + // if (value.isComposingRangeValid) { + // debugPrint("composing before ${value.composing.textBefore(value.text)}"); + // debugPrint("composing inside ${value.composing.textInside(value.text)}"); + // debugPrint("composing after ${value.composing.textAfter(value.text)}"); + // } + + if (choreographer.igc.igcTextData == null || text.isEmpty) { + return TextSpan(text: text, style: style); + } else { + final parts = text.split(choreographer.igc.igcTextData!.originalInput); + + if (parts.length == 1 || parts.length > 2) { + return TextSpan(text: text, style: style); + } + + return TextSpan( + style: style, + children: [ + ...choreographer.igc.igcTextData!.constructTokenSpan( + context: context, + defaultStyle: style, + showTokens: choreographer.definitionsEnabled, + spanCardModel: null, + handleClick: false, + transformTargetId: choreographer.inputTransformTargetKey, + room: choreographer.chatController.room, + ), + TextSpan(text: parts[1], style: style) + ], + ); + } + } +} diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart new file mode 100644 index 000000000..945bf1770 --- /dev/null +++ b/lib/pangea/widgets/igc/span_card.dart @@ -0,0 +1,419 @@ +import 'dart:developer'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/enum/span_data_type.dart'; +import 'package:fluffychat/pangea/models/span_data.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/match_copy.dart'; +import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../widgets/matrix.dart'; +import '../../choreographer/widgets/choice_array.dart'; +import '../../controllers/pangea_controller.dart'; +import '../../enum/span_choice_type.dart'; +import '../../models/span_card_model.dart'; +import '../common/bot_face_svg.dart'; +import 'card_header.dart'; + +const wordMatchResultsCount = 5; + +//switch for definition vs correction vs practice + +//always show a title +//if description then show description +//choices then show choices +// + +class SpanCard extends StatefulWidget { + final PangeaController pangeaController = MatrixState.pangeaController; + final SpanCardModel scm; + final String? roomId; + + SpanCard({ + Key? key, + required this.scm, + this.roomId, + }) : super(key: key); + + @override + State createState() => SpanCardState(); +} + +class SpanCardState extends State { + Object? error; + bool fetchingData = false; + int? selectedChoiceIndex; + + //on initState, get SpanDetails + @override + void initState() { + // debugger(when: kDebugMode); + super.initState(); + getSpanDetails(); + } + + //get selected choice + SpanChoice? get selectedChoice => selectedChoiceIndex != null && + widget.scm.pangeaMatch?.match.choices != null + ? widget.scm.pangeaMatch!.match.choices![selectedChoiceIndex!] + : null; + + Future getSpanDetails() async { + try { + if (widget.scm.pangeaMatch?.isITStart ?? false) return; + setState(() { + fetchingData = true; + }); + + await widget.scm.choreographer.igc.getSpanDetails(widget.scm.matchIndex); + + setState(() => fetchingData = false); + } catch (e) { + // debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: StackTrace.current); + setState(() { + error = e; + fetchingData = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return WordMatchContent(controller: this); + } +} + +class WordMatchContent extends StatelessWidget { + final PangeaController pangeaController = MatrixState.pangeaController; + final SpanCardState controller; + + WordMatchContent({ + required this.controller, + Key? key, + }) : super(key: key); + + Future onChoiceSelect(int index) async { + controller.selectedChoiceIndex = index; + controller + .widget + .scm + .choreographer + .igc + .igcTextData! + .matches[controller.widget.scm.matchIndex] + .match + .choices![index] + .selected = true; + + controller.setState(() => ()); + // if (controller.widget.scm.pangeaMatch.match.choices![index].type == + // SpanChoiceType.distractor) { + // await controller.getSpanDetails(); + // } + // controller.setState(() {}); + } + + void onReplaceSelected() { + controller.widget.scm + .onReplacementSelect( + matchIndex: controller.widget.scm.matchIndex, + choiceIndex: controller.selectedChoiceIndex!, + ) + .then((value) { + controller.setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + if (controller.widget.scm.pangeaMatch == null) { + return const SizedBox(); + } + if (controller.error != null) { + return CardErrorWidget( + error: controller.error!, + choreographer: controller.widget.scm.choreographer, + offset: controller.widget.scm.pangeaMatch?.match.offset, + ); + } + final MatchCopy matchCopy = MatchCopy( + context, + controller.widget.scm.pangeaMatch!, + ); + + final ScrollController scrollController = ScrollController(); + + try { + return Column( + children: [ + CardHeader( + text: controller.error?.toString() ?? matchCopy.title, + botExpression: controller.error == null + ? BotExpression.right + : BotExpression.addled, + ), + Expanded( + child: Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // const SizedBox(height: 10.0), + // if (matchCopy.description != null) + // Padding( + // padding: const EdgeInsets.only(), + // child: Text( + // matchCopy.description!, + // style: BotStyle.text(context), + // ), + // ), + const SizedBox(height: 8), + if (!controller.widget.scm.pangeaMatch!.isITStart) + ChoicesArray( + originalSpan: + controller.widget.scm.pangeaMatch!.matchContent, + isLoading: controller.fetchingData, + choices: + controller.widget.scm.pangeaMatch!.match.choices + ?.map((e) => Choice( + text: e.value, + color: e.selected ? e.type.color : null, + )) + .toList(), + onPressed: onChoiceSelect, + uniqueKeyForLayerLink: (int index) => "wordMatch$index", + selectedChoiceIndex: controller.selectedChoiceIndex, + ), + const SizedBox(height: 12), + PromptAndFeedback(controller: controller), + ], + ), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 10), + Expanded( + child: Opacity( + opacity: 0.8, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + AppConfig.primaryColor.withOpacity(0.1)), + ), + onPressed: () { + MatrixState.pAnyState.closeOverlay(); + Future.delayed(Duration.zero, + () => controller.widget.scm.onIgnore()); + }, + child: Center( + child: Text(L10n.of(context)!.ignoreInThisText), + ), + ), + ), + ), + const SizedBox(width: 10), + if (!controller.widget.scm.pangeaMatch!.isITStart) + Expanded( + child: Opacity( + opacity: controller.selectedChoiceIndex != null ? 1.0 : 0.5, + child: TextButton( + onPressed: controller.selectedChoiceIndex != null + ? onReplaceSelected + : null, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + (controller.selectedChoice != null + ? controller.selectedChoice!.color + : AppConfig.primaryColor) + .withOpacity(0.2)), + ), + child: Text(L10n.of(context)!.replace), + ), + ), + ), + const SizedBox(width: 10), + if (controller.widget.scm.pangeaMatch!.isITStart) + Expanded( + child: TextButton( + onPressed: () { + MatrixState.pAnyState.closeOverlay(); + Future.delayed(Duration.zero, + () => controller.widget.scm.onITStart()); + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + (AppConfig.primaryColor).withOpacity(0.1)), + ), + child: Text(L10n.of(context)!.helpMeTranslate), + ), + ), + ], + ), + ], + ); + } on Exception catch (e) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: StackTrace.current); + print(e); + rethrow; + } + } +} + +class PromptAndFeedback extends StatelessWidget { + const PromptAndFeedback({ + super.key, + required this.controller, + }); + + final SpanCardState controller; + + @override + Widget build(BuildContext context) { + return Container( + constraints: controller.widget.scm.pangeaMatch!.isITStart + ? null + : const BoxConstraints(minHeight: 100), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (controller.selectedChoice == null && controller.fetchingData) + const Center( + child: SizedBox( + width: 24.0, + height: 24.0, + child: CircularProgressIndicator(), + ), + ), + if (controller.selectedChoice != null) ...[ + Text( + controller.selectedChoice!.feedbackToDisplay(context), + style: BotStyle.text(context), + ), + const SizedBox(height: 8), + if (controller.selectedChoice?.feedback == null) + TextButton( + onPressed: () { + if (!controller.fetchingData) { + controller.getSpanDetails(); + } + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + AppConfig.primaryColor.withOpacity(0.1)), + ), + child: SizedBox( + width: 150, // set the width of the button contents here + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!controller.fetchingData) + const Icon(Icons.lightbulb_outline), + if (controller.fetchingData) + const Center( + child: SizedBox( + width: 24.0, + height: 24.0, + child: CircularProgressIndicator(), + ), + ), + const SizedBox(width: 8), + Text(L10n.of(context)!.why), + ], + ), + ), + ) + ], + if (!controller.fetchingData && + controller.selectedChoiceIndex == null) + Text( + controller.widget.scm.pangeaMatch!.match.type.typeName + .defaultPrompt(context), + style: BotStyle.text(context), + ), + ], + ), + ); + } +} + +class LoadingText extends StatefulWidget { + const LoadingText({ + Key? key, + }) : super(key: key); + + @override + _LoadingTextState createState() => _LoadingTextState(); +} + +class _LoadingTextState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + )..repeat(); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + L10n.of(context)!.makingActivity, + style: BotStyle.text(context), + ), + AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + return Text( + _controller.isAnimating ? '.' * _controller.value.toInt() : '', + style: BotStyle.text(context), + ); + }, + ), + ], + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class StartITButton extends StatelessWidget { + const StartITButton({ + Key? key, + required this.onITStart, + }) : super(key: key); + + final void Function() onITStart; + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: ListTile( + leading: const Icon(Icons.translate_outlined), + title: Text(L10n.of(context)!.helpMeTranslate), + onTap: () { + MatrixState.pAnyState.closeOverlay(); + Future.delayed(Duration.zero, () => onITStart()); + }, + ), + ); + } +} diff --git a/lib/pangea/widgets/igc/span_data.dart b/lib/pangea/widgets/igc/span_data.dart new file mode 100644 index 000000000..067adc5d8 --- /dev/null +++ b/lib/pangea/widgets/igc/span_data.dart @@ -0,0 +1,184 @@ +//Possible actions/effects from cards +// Nothing +// useType of viewed definitions +// SpanChoice of text in message from options +// Call to server for additional/followup info + +import 'package:collection/collection.dart'; + +import '../../enum/span_choice_type.dart'; +import '../../enum/span_data_type.dart'; + +class SpanData { + String? message; + String? shortMessage; + List? choices; + List? replacements; + int offset; + int length; + String fullText; + Context? context; + SpanDataTypeEnum type; + Rule? rule; + + SpanData({ + this.message, + this.shortMessage, + this.choices, + this.replacements, + required this.offset, + required this.length, + required this.fullText, + this.context, + required this.type, + this.rule, + }); + + factory SpanData.fromJson(Map json) { + return SpanData( + message: json['message'], + shortMessage: json['shortMessage'], + choices: json['choices'] != null + ? List.from( + json['choices'].map((x) => SpanChoice.fromJson(x))) + : null, + replacements: json['replacements'] != null + ? List.from( + json['replacements'].map((x) => Replacement.fromJson(x))) + : null, + offset: json['offset'], + length: json['length'], + fullText: json['full_text'], + context: json['context'] != null + ? Context.fromJson(json['context']) + : null, + type: SpanDataTypeEnum.values.firstWhereOrNull( + (e) => e.toString() == 'SpanDataTypeEnum.${json['type']}') ?? + SpanDataTypeEnum.correction, + rule: json['rule'] != null ? Rule.fromJson(json['rule']) : null, + ); + } + + Map toJson() { + final Map data = {}; + data['message'] = message; + data['shortMessage'] = shortMessage; + if (choices != null) { + data['choices'] = choices!.map((x) => x.toJson()).toList(); + } + if (replacements != null) { + data['replacements'] = + replacements!.map((x) => x.toJson()).toList(); + } + data['offset'] = offset; + data['length'] = length; + data['full_text'] = fullText; + if (context != null) { + data['context'] = context!.toJson(); + } + data['type'] = type.toString().split('.').last; + if (rule != null) { + data['rule'] = rule!.toJson(); + } + return data; + } +} +class Context { + String sentence; + int offset; + int length; + + Context({required this.sentence, required this.offset, required this.length}); + + factory Context.fromJson(Map json) { + return Context( + sentence: json['sentence'], + offset: json['offset'], + length: json['length'], + ); + } + + Map toJson() { + final Map data = {}; + data['sentence'] = sentence; + data['offset'] = offset; + data['length'] = length; + return data; + } +} + +class SpanChoice { + String value; + bool selected; + + SpanChoice({required this.value, required this.selected}); + + factory SpanChoice.fromJson(Map json) { + return SpanChoice( + value: json['value'], + selected: json['selected'], + ); + } + + Map toJson() { + final Map data = {}; + data['value'] = value; + data['selected'] = selected; + return data; + } +} + +class Replacement { + String value; + + Replacement({required this.value}); + + factory Replacement.fromJson(Map json) { + return Replacement( + value: json['value'], + ); + } + + Map toJson() { + final Map data = {}; + data['value'] = value; + return data; + } +} + +class Rule { + String id; + + Rule({required this.id}); + + factory Rule.fromJson(Map json) { + return Rule( + id: json['id'], + ); + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + return data; + } +} + +class SpanDataType { + String type; + + SpanDataType({required this.type}); + + factory SpanDataType.fromJson(Map json) { + return SpanDataType( + type: json['type'], + ); + } + + Map toJson() { + final Map data = {}; + data['type'] = type; + return data; + } +} + diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart new file mode 100644 index 000000000..85437f347 --- /dev/null +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -0,0 +1,369 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart'; +import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; +import 'package:fluffychat/pangea/widgets/igc/card_header.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:http/http.dart'; +import 'package:matrix/matrix.dart'; + +import '../../models/word_data_model.dart'; +import '../flag.dart'; +import 'card_error_widget.dart'; + +class WordDataCard extends StatefulWidget { + final bool hasInfo; + final String word; + final String fullText; + final String? choiceFeedback; + final String wordLang; + final String fullTextLang; + final Room room; + + const WordDataCard({ + Key? key, + required this.word, + required this.wordLang, + required this.hasInfo, + required this.fullText, + required this.fullTextLang, + required this.room, + this.choiceFeedback, + }) : super(key: key); + + @override + State createState() => WordDataCardController(); +} + +class WordDataCardController extends State { + final PangeaController controller = MatrixState.pangeaController; + + bool isLoadingWordNet = false; + bool isLoadingContextualDefinition = false; + ContextualDefinitionResponseModel? contextualDefinitionRes; + WordData? wordData; + Object? wordNetError; + + Object? definitionError; + LanguageModel? activeL1; + LanguageModel? activeL2; + + Response get noLanguages => Response("", 405); + + @override + void initState() { + if (!mounted) return; + activeL1 = + controller.languageController.activeL1Model(roomID: widget.room.id)!; + activeL2 = + controller.languageController.activeL2Model(roomID: widget.room.id)!; + if (activeL1 == null || activeL2 == null) { + wordNetError = noLanguages; + definitionError = noLanguages; + } else if (!widget.hasInfo) { + getContextualDefinition(); + } else { + getWordNet(); + } + super.initState(); + } + + Future getContextualDefinition() async { + ContextualDefinitionRequestModel? req; + try { + req = ContextualDefinitionRequestModel( + fullText: widget.fullText, + word: widget.word, + feedbackLang: activeL1?.langCode ?? LanguageKeys.defaultLanguage, + fullTextLang: widget.fullTextLang, + wordLang: widget.wordLang, + ); + if (mounted) setState(() => isLoadingContextualDefinition = true); + contextualDefinitionRes = await controller.definitions.get(req); + if (contextualDefinitionRes == null) { + definitionError = Exception("Error getting definition"); + } + GoogleAnalytics.contextualRequest(); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack, data: req?.toJson()); + definitionError = Exception("Error getting definition"); + } finally { + if (mounted) setState(() => isLoadingContextualDefinition = false); + } + } + + Future getWordNet() async { + if (mounted) setState(() => isLoadingWordNet = true); + try { + wordData = await controller.wordNet.getWordDataGlobal( + word: widget.word, + fullText: widget.fullText, + userL1: activeL1?.langCode, + userL2: activeL2?.langCode, + ); + } catch (err) { + ErrorHandler.logError( + e: err, + s: StackTrace.current, + data: {"word": widget.word, "hasInfo": widget.hasInfo}, + ); + wordNetError = err; + } finally { + if (mounted) { + setState(() => isLoadingWordNet = false); + } + } + } + + void handleGetDefinitionButtonPress() { + if (isLoadingContextualDefinition) return; + getContextualDefinition(); + } + + @override + Widget build(BuildContext context) => WordDataCardView(controller: this); +} + +class WordDataCardView extends StatelessWidget { + const WordDataCardView({ + Key? key, + required this.controller, + }) : super(key: key); + + final WordDataCardController controller; + + @override + Widget build(BuildContext context) { + if (controller.wordNetError != null) { + return CardErrorWidget(error: controller.wordNetError); + } + if (controller.activeL1 == null || controller.activeL2 == null) { + ErrorHandler.logError(m: "should not be here"); + return CardErrorWidget(error: controller.noLanguages); + } + + final ScrollController scrollController = ScrollController(); + + return Scrollbar( + thumbVisibility: true, + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CardHeader( + text: controller.widget.word, + botExpression: BotExpression.down), + if (controller.widget.choiceFeedback != null) + Text( + controller.widget.choiceFeedback!, + style: BotStyle.text(context), + ), + const SizedBox(height: 5.0), + if (controller.wordData != null && controller.wordNetError == null) + WordNetInfo( + wordData: controller.wordData!, + activeL1: controller.activeL1!, + activeL2: controller.activeL2!, + ), + if (controller.isLoadingWordNet) const PCircular(), + const SizedBox(height: 5.0), + // if (controller.widget.hasInfo && + // !controller.isLoadingContextualDefinition && + // controller.contextualDefinitionRes == null) + // Material( + // type: MaterialType.transparency, + // child: ListTile( + // leading: const BotFace( + // width: 40, expression: BotExpression.surprised), + // title: Text(L10n.of(context)!.askPangeaBot), + // onTap: controller.handleGetDefinitionButtonPress, + // ), + // ), + if (controller.isLoadingContextualDefinition) const PCircular(), + if (controller.contextualDefinitionRes != null) + Text( + controller.contextualDefinitionRes!.text, + style: BotStyle.text(context), + ), + if (controller.definitionError != null) + Text( + L10n.of(context)!.sorryNoResults, + style: BotStyle.text(context), + ), + ], + ), + ), + ); + } +} + +class WordNetInfo extends StatelessWidget { + final WordData wordData; + final LanguageModel activeL1; + final LanguageModel activeL2; + + const WordNetInfo({ + Key? key, + required this.wordData, + required this.activeL1, + required this.activeL2, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SensesForLanguage( + wordData: wordData, + languageType: LanguageType.target, + language: activeL2, + ), + SensesForLanguage( + wordData: wordData, + languageType: LanguageType.base, + language: activeL1, + ), + ], + ); + } +} + +enum LanguageType { + target, + base, +} + +class SensesForLanguage extends StatelessWidget { + const SensesForLanguage({ + Key? key, + required this.wordData, + required this.languageType, + required this.language, + }) : super(key: key); + + final LanguageModel language; + final LanguageType languageType; + final WordData wordData; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(7, 0, 0, 0), + child: LanguageFlag( + language: language, + ), + ), + Expanded( + child: PartOfSpeechBlock( + wordData: wordData, + languageType: languageType, + ), + ) + ], + ), + ); + } +} + +class PartOfSpeechBlock extends StatelessWidget { + final WordData wordData; + final LanguageType languageType; + + const PartOfSpeechBlock({ + Key? key, + required this.wordData, + required this.languageType, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + languageType == LanguageType.target + ? "${wordData.targetWord} (${wordData.formattedPartOfSpeech(languageType)})" + : "${wordData.baseWord} (${wordData.formattedPartOfSpeech(languageType)})", + style: BotStyle.text(context, italics: true, bold: false), + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.only(left: 14.0, bottom: 10.0), + child: Align( + alignment: Alignment.centerLeft, + child: Column( + children: [ + RichText( + text: TextSpan( + style: BotStyle.text( + context, + italics: false, + bold: false, + ), + children: [ + TextSpan( + text: "${L10n.of(context)!.definition}: ", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: languageType == LanguageType.target + ? wordData.targetDefinition + : wordData.baseDefinition, + ), + ], + ), + ), + const SizedBox(height: 10), + RichText( + text: TextSpan( + style: BotStyle.text( + context, + italics: false, + bold: false, + ), + children: [ + TextSpan( + text: "${L10n.of(context)!.exampleSentence}: ", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: languageType == LanguageType.target + ? wordData.targetExampleSentence + : wordData.baseExampleSentence, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/login/home_picker_logo.dart b/lib/pangea/widgets/login/home_picker_logo.dart new file mode 100644 index 000000000..2864020b7 --- /dev/null +++ b/lib/pangea/widgets/login/home_picker_logo.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import '../../../config/app_config.dart'; +import '../common/pangea_logo_svg.dart'; + +class PangeaLogoAndNameWidget extends StatelessWidget { + const PangeaLogoAndNameWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + alignment: Alignment.center, + height: 120, + child: const PangeaLogoSvg(width: 120, forceColor: Colors.white), + ), + //PTODO - widget to display app name with consistent style + Text( + AppConfig.applicationName, + style: const TextStyle( + fontSize: 26, + color: Colors.white, + fontWeight: FontWeight.w800, + ), + ), + // const Text( + // "Life is Learning", + // style: TextStyle( + // fontSize: 18, + // color: Colors.white, + // ), + // ), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/new_group/topics_list.dart b/lib/pangea/widgets/new_group/topics_list.dart new file mode 100644 index 000000000..e720b95af --- /dev/null +++ b/lib/pangea/widgets/new_group/topics_list.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../../models/chat_topic_model.dart'; + +/// the widget loads the list of ChatTopic from assets/chat_data.json +/// displays the topics in a list with image, name and total number of vocab +/// when a topic is selected, the ChatTopic is passed to the parent widget +/// while a topic is selected, it displays expanded details about the topic, +/// including description, bot prompt, and vocab +class SelectTopicList extends StatefulWidget { + final Function(ChatTopic) onTopicSelected; + + const SelectTopicList({super.key, required this.onTopicSelected}); + + @override + SelectTopicListState createState() => SelectTopicListState(); +} + +class SelectTopicListState extends State { + late Future> _futureTopics; + + ChatTopic? selected; + + @override + void initState() { + super.initState(); + _futureTopics = _loadTopics(); + } + + Future> _loadTopics() async { + final String data = await DefaultAssetBundle.of(context) + .loadString("assets/chat_data.json"); + final jsonResult = json.decode(data); + final List topics = []; + for (final topic in jsonResult['chats']) { + topics.add(ChatTopic.fromJson(topic)); + } + return topics; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _futureTopics, + builder: (context, snapshot) { + if (snapshot.hasData) { + return SelectTopicListWidget( + topics: snapshot.data!, + onTopicSelected: ((topic) { + widget.onTopicSelected(topic); + selected = topic; + setState(() {}); + }), + selectedTopicState: this, + ); + } else if (snapshot.hasError) { + return Text("${snapshot.error}"); + } + return const CircularProgressIndicator(); + }, + ); + } +} + +class SelectTopicListWidget extends StatelessWidget { + final List topics; + final Function(ChatTopic) onTopicSelected; + final SelectTopicListState selectedTopicState; + + const SelectTopicListWidget({ + super.key, + required this.topics, + required this.onTopicSelected, + required this.selectedTopicState, + }); + + @override + Widget build(BuildContext context) { + final list = ListView.builder( + itemCount: topics.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return SelectTopicListItem( + topic: topics[index], + isSelected: topics[index] == selectedTopicState.selected, + onTopicSelected: onTopicSelected, + ); + }, + ); + + /// wrap list in scroll area + return SingleChildScrollView( + child: list, + ); + } +} + +/// while a topic is selected, it displays expanded details about the topic, +/// including description, bot prompt, and vocab +/// use ThemeData to get colors, text styles, etc. +class SelectTopicListItem extends StatelessWidget { + final ChatTopic topic; + final Function(ChatTopic) onTopicSelected; + final bool isSelected; + + const SelectTopicListItem({ + super.key, + required this.topic, + required this.isSelected, + required this.onTopicSelected, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + onTopicSelected(topic); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSelected ? Colors.grey[100] : Colors.white, + border: Border.all( + color: isSelected ? Colors.grey[300]! : Colors.white, + ), + ), + child: Row( + children: [ + // Image.asset( + // Icons.airplane_ticket_outlined, + // width: 50, + // height: 50, + // ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + topic.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + if (isSelected) ...[ + const SizedBox(height: 8), + Text( + topic.description, + style: const TextStyle( + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + Text( + topic.vocab.join(", "), + style: const TextStyle( + color: Colors.grey, + ), + ), + ], + ], + ), + ), + const Icon(Icons.chevron_right), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/new_group/vocab_list.dart b/lib/pangea/widgets/new_group/vocab_list.dart new file mode 100644 index 000000000..ae3a5d59c --- /dev/null +++ b/lib/pangea/widgets/new_group/vocab_list.dart @@ -0,0 +1,526 @@ +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 '../../models/chat_topic_model.dart'; +import '../../models/lemma.dart'; +import '../../repo/topic_data_repo.dart'; +import '../common/bot_face_svg.dart'; + +/// stateless widget that displays a list of vocabulary words +/// parameters +/// 1) list of words +/// 2) one callback function that handles all changes to the list (passes full list back to parent) +/// view +/// 1) a list of words (chips) in a wrapped row within a scrollable area with an x button to remove the word +/// 2) in the bottom center is a text field to type a new word and add it to the list on submit +/// 3) at the top right is a button to clear the list with trash icon and confirm dialog +/// 4) at the top left is a button to call an API to get a list of words with the BotFace widget +/// the user can +/// 1) type a new word and add it to the list +/// 2) remove a word from the list +/// 3) clear all words from the list +/// 4) widget button called +/// uses app theme colors, text styles, and icons + +class ChatVocabularyList extends StatelessWidget { + const ChatVocabularyList({ + Key? key, + required this.topic, + required this.onChanged, + }) : super(key: key); + + final ChatTopic topic; + final ValueChanged> onChanged; + + @override + Widget build(BuildContext context) { + final deleteButton = Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(L10n.of(context)!.clearAll), + actions: [ + TextButton( + child: Text(L10n.of(context)!.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text(L10n.of(context)!.confirm), + onPressed: () { + onChanged([]); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + }, + ), + ], + ); + final words = Wrap( + spacing: 8, + children: [ + for (final word in topic.vocab) + Chip( + labelStyle: Theme.of(context).textTheme.bodyMedium, + label: Text(word.form), + onDeleted: () { + onChanged(topic.vocab..remove(word)); + }, + ), + ], + ); + final vocabColumn = Column( + children: [ + // clear all words button + deleteButton, + // list of words + words, + // add word from text field to words + WordAddTextField( + words: topic.vocab, + onSubmitted: (value) { + //PTODO - error message if + if (value.isEmpty) return; + onChanged(topic.vocab..add(Lemma.create(value))); + }, + ), + GenerateVocabButton( + topic: topic, + onWordsGenerated: (newWords) { + onChanged(topic.vocab..addAll(newWords)); + }, + onPressed: () {}, + ), + ], + ); + + /// return vocabColumn wrapped in a scrollable area with border + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + width: 2, + ), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + child: vocabColumn, + ), + ); + } +} + +class VocabWord { + final String word; + + VocabWord({ + required this.word, + }); + + factory VocabWord.fromJson(Map json) { + return VocabWord( + word: json['word'], + ); + } + + Map toJson() { + return { + 'word': word, + }; + } + + /// set equals operator + @override + bool operator ==(Object other) => + identical(this, other) || + other is VocabWord && + runtimeType == other.runtimeType && + word == other.word; + + /// set hashcode + @override + int get hashCode => word.hashCode; +} + +/// text field widget for adding a word +/// parameters +/// 1) callback passes the word back to the parent widget +/// 2) word list to check if the word is already in the list +/// new word cn be added by pressing enter or the add button +/// uses app theme colors, text styles, and icons +/// function for checking word, reused in the button and textfield onSubmitted +/// 1) if the word is already in the list, it is not added and an error message is displayed +/// 2) if whitespace is detected anywhere in the word, display an error message + +class WordAddTextField extends StatefulWidget { + const WordAddTextField({ + Key? key, + required this.onSubmitted, + required this.words, + }) : super(key: key); + + final ValueChanged onSubmitted; + final List words; + + @override + WordAddTextFieldState createState() => WordAddTextFieldState(); +} + +class WordAddTextFieldState extends State { + final _controller = TextEditingController(); + String? _errorText; + + @override + void initState() { + super.initState(); + _controller.addListener(() { + setState(() { + _errorText = null; + }); + }); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: _controller, + decoration: InputDecoration( + labelText: 'Add a word', + errorText: _errorText, + suffixIcon: IconButton( + icon: const Icon(Icons.add), + onPressed: () { + _addWord(); + }, + ), + ), + onSubmitted: (value) { + _addWord(); + }, + ); + } + + void _addWord() { + final word = _controller.text.trim(); + if (word.isEmpty) { + setState(() { + _errorText = 'Please enter a word'; + }); + return; + } + if (widget.words.map((e) => e.text).contains(word)) { + setState(() { + _errorText = 'Word already in list'; + }); + return; + } + widget.onSubmitted(word); + _controller.clear(); + } +} + +/// widget button to call an API to get a list of words +/// button has a BotFace icon and text saying "Generate Vocabulary", use L10n for copy +/// parameters +/// 1) callback function to pass the list of words back to the parent widget +/// 2) callback function to notify the parent widget that the button was pressed +/// 3) topic information to pass to the API +/// display loading indicator while waiting for response +/// display error message if there is an error +/// uses app theme colors, text styles, and icons +class GenerateVocabButton extends StatefulWidget { + const GenerateVocabButton({ + Key? key, + required this.onWordsGenerated, + required this.onPressed, + required this.topic, + }) : super(key: key); + + final ChatTopic topic; + final ValueChanged> onWordsGenerated; + final VoidCallback onPressed; + + @override + GenerateVocabButtonState createState() => GenerateVocabButtonState(); +} + +class GenerateVocabButtonState extends State { + bool _loading = false; + String? _errorText; + final PangeaController _pangeaController = MatrixState.pangeaController; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // button to call API + ElevatedButton.icon( + icon: const BotFace( + width: 50.0, + expression: BotExpression.right, + ), + label: Text(L10n.of(context)!.generateVocabulary), + onPressed: () async { + // if widget.topic.name is null, give error message + if (widget.topic.name.isEmpty) { + setState(() { + _errorText = + 'Please enter a topic name before generating vocabulary'; + }); + return; + } + setState(() { + _loading = true; + _errorText = null; + }); + try { + final words = await _getWords(); + widget.onWordsGenerated(words); + } catch (e) { + setState(() { + _errorText = e.toString(); + }); + } finally { + setState(() { + _loading = false; + }); + } + widget.onPressed(); + }, + ), + + // loading indicator + if (_loading) const CircularProgressIndicator(), + // error message + if (_errorText != null) Text(_errorText!), + ], + ); + } + + Future> _getWords() async { + final ChatTopic topic = await TopicDataRepo.generate( + await _pangeaController.userController.accessToken, + request: TopicDataRequest( + topicInfo: widget.topic, + numWords: 10, + numPrompts: 0, + ), + ); + return topic.vocab; + } +} + +/// text field for entering a chat description, max 250 characters +/// parameters +/// 1) ChatTopic object +/// 2) initial value for the text field +/// 3) onChanged callback function to pass the updated ChatTopic back to the parent widget +class DescriptionField extends StatelessWidget { + const DescriptionField({ + Key? key, + required this.topic, + required this.initialValue, + required this.onChanged, + }) : super(key: key); + + final ChatTopic topic; + final String initialValue; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: initialValue, + decoration: InputDecoration( + labelText: L10n.of(context)!.groupDescription, + hintText: L10n.of(context)!.addGroupDescription, + ), + maxLength: 250, + maxLines: 5, + onChanged: (value) { + onChanged(value); + }, + ); + } +} + +/// text field for entering a chat name, max 50 characters +/// parameters +/// 1) ChatTopic object +/// 2) initial value for the text field +/// 3) onChanged callback function to pass the updated ChatTopic back to the parent widget +class NameField extends StatelessWidget { + const NameField({ + Key? key, + required this.topic, + required this.onChanged, + }) : super(key: key); + + final ChatTopic topic; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return TextFormField( + initialValue: topic.name, + decoration: InputDecoration( + labelText: L10n.of(context)!.optionalGroupName, + hintText: L10n.of(context)!.enterAGroupName, + ), + maxLength: 50, + onChanged: (value) { + onChanged(value); + }, + ); + } +} + +///widget for displaying, adding, deleting and generating discussion prompts +/// parameters +/// 1) chatTopic object +/// 2) callback function to pass the updated ChatTopic back to the parent widget +class PromptsField extends StatefulWidget { + const PromptsField({ + Key? key, + required this.topic, + required this.onChanged, + }) : super(key: key); + + final ChatTopic topic; + final ValueChanged onChanged; + + @override + PromptsFieldState createState() => PromptsFieldState(); +} + +class PromptsFieldState extends State { + final TextEditingController _controller = TextEditingController(); + String? _errorText; + + final PangeaController _pangeaController = MatrixState.pangeaController; + + @override + void initState() { + super.initState(); + _controller.addListener(() { + setState(() { + _errorText = null; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // text field for entering a prompt + TextField( + controller: _controller, + decoration: InputDecoration( + labelText: 'Add a prompt', + errorText: _errorText, + suffixIcon: IconButton( + icon: const Icon(Icons.add), + onPressed: () { + _addPrompt(); + }, + ), + ), + onSubmitted: (value) { + _addPrompt(); + }, + ), + + // list of prompts + ListView.builder( + shrinkWrap: true, + itemCount: widget.topic.discussionPrompts.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(widget.topic.discussionPrompts[index].text), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + _deletePrompt(index); + }, + ), + ); + }, + ), + + // button to call API + ElevatedButton.icon( + icon: const BotFace( + width: 50.0, + expression: BotExpression.right, + ), + label: Text(L10n.of(context)!.generatePrompts), + onPressed: () async { + setState(() { + _errorText = null; + }); + try { + final prompts = await _getPrompts(); + widget.topic.discussionPrompts = prompts; + widget.onChanged(widget.topic); + } catch (e) { + setState(() { + _errorText = e.toString(); + }); + } + }, + ), + ], + ); + } + + void _addPrompt() { + final text = _controller.text.trim(); + if (text.isEmpty) { + setState(() { + _errorText = 'Please enter a prompt'; + }); + return; + } + final prompt = DiscussionPrompt(text: text); + if (widget.topic.discussionPrompts.contains(prompt)) { + setState(() { + _errorText = 'Prompt already in list'; + }); + return; + } + widget.topic.discussionPrompts.add(prompt); + widget.onChanged(widget.topic); + _controller.clear(); + } + + void _deletePrompt(int index) { + widget.topic.discussionPrompts.removeAt(index); + widget.onChanged(widget.topic); + } + + Future> _getPrompts() async { + final ChatTopic res = await TopicDataRepo.generate( + await _pangeaController.userController.accessToken, + request: TopicDataRequest( + topicInfo: widget.topic, + numPrompts: 10, + numWords: 0, + ), + ); + return res.discussionPrompts; + } +} diff --git a/lib/pangea/widgets/signup/signup_buttons.dart b/lib/pangea/widgets/signup/signup_buttons.dart new file mode 100644 index 000000000..cc312a8f3 --- /dev/null +++ b/lib/pangea/widgets/signup/signup_buttons.dart @@ -0,0 +1,218 @@ +import 'dart:typed_data'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; + +class SignupButtons extends StatefulWidget { + const SignupButtons({Key? key}) : super(key: key); + + @override + State createState() => SignupButtonsState(); +} + +class SignupButtonsState extends State { + final PangeaController pangeaController = MatrixState.pangeaController; + + void pickAvatar() async { + final source = !PlatformInfos.isMobile + ? ImageSource.gallery + : await showModalActionSheet( + context: context, + title: L10n.of(context)!.changeYourAvatar, + actions: [ + SheetAction( + key: ImageSource.camera, + label: L10n.of(context)!.openCamera, + isDefaultAction: true, + icon: Icons.camera_alt_outlined, + ), + SheetAction( + key: ImageSource.gallery, + label: L10n.of(context)!.openGallery, + icon: Icons.photo_outlined, + ), + ], + ); + if (source == null) return; + final picked = await ImagePicker().pickImage( + source: source, + imageQuality: 50, + maxWidth: 512, + maxHeight: 512, + ); + setState(() { + Matrix.of(context).loginAvatar = picked; + }); + } + + final TextEditingController usernameController = TextEditingController(); + String? signupError; + + void signUp() async { + usernameController.text = usernameController.text.trim(); + final localpart = + usernameController.text.toLowerCase().replaceAll(' ', '_'); + if (localpart.isEmpty) { + setState(() { + signupError = L10n.of(context)!.pleaseChooseAUsername; + }); + return; + } + + setState(() { + signupError = null; + }); + + try { + try { + await Matrix.of(context).getLoginClient().register(username: localpart); + } on MatrixException catch (e) { + if (!e.requireAdditionalAuthentication) rethrow; + } + Matrix.of(context).loginUsername = usernameController.text; + context.go('/home/signup'); + } catch (e, s) { + Logs().d('Sign up failed', e, s); + setState(() { + signupError = e.toLocalizedString(context); + }); + } + } + + @override + Widget build(BuildContext context) { + final avatar = Matrix.of(context).loginAvatar; + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: Center( + child: Stack( + children: [ + Material( + borderRadius: BorderRadius.circular(64), + elevation: + Theme.of(context).appBarTheme.scrolledUnderElevation ?? + 10, + color: Colors.transparent, + shadowColor: + Theme.of(context).colorScheme.onBackground.withAlpha(64), + clipBehavior: Clip.hardEdge, + child: CircleAvatar( + radius: 64, + backgroundColor: Colors.white, + child: avatar == null + ? const Icon( + Icons.person_outlined, + color: Colors.black, + size: 64, + ) + : FutureBuilder( + future: avatar.readAsBytes(), + builder: (context, snapshot) { + final bytes = snapshot.data; + if (bytes == null) { + return const CircularProgressIndicator + .adaptive(); + } + return Image.memory( + bytes, + fit: BoxFit.cover, + width: 128, + height: 128, + ); + }, + ), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton( + mini: true, + onPressed: pickAvatar, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + child: const Icon(Icons.camera_alt_outlined), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: TextField( + controller: usernameController, + onSubmitted: (_) => signUp(), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.account_box_outlined), + hintText: L10n.of(context)!.chooseAUsername, + errorText: signupError, + errorStyle: const TextStyle(color: Colors.orange), + fillColor: + Theme.of(context).colorScheme.background.withOpacity(0.75), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: Hero( + tag: 'signUpButton', + child: ElevatedButton( + onPressed: signUp, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const PangeaLogoSvg(width: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text(L10n.of(context)!.signUp), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + const Expanded( + child: Divider( + thickness: 1, + color: Colors.white, + ), + ), + Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + L10n.of(context)!.or, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + ), + ), + const Expanded( + child: Divider( + thickness: 1, + color: Colors.white, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/signup/tos_checkbox.dart b/lib/pangea/widgets/signup/tos_checkbox.dart new file mode 100644 index 000000000..08a973065 --- /dev/null +++ b/lib/pangea/widgets/signup/tos_checkbox.dart @@ -0,0 +1,60 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/pages/sign_up/signup.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class TosCheckbox extends StatelessWidget { + final SignupPageController controller; + const TosCheckbox(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + value: controller.isTnCChecked, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: controller.onTncChange, + title: InkWell( + onTap: () => + UrlLauncher(context, AppConfig.termsOfServiceUrl).launchUrl(), + child: RichText( + maxLines: 2, + text: TextSpan( + text: L10n.of(context)!.iAgreeToThe, + children: [ + //PTODO - make sure this is actually a link + TextSpan( + text: L10n.of(context)!.termsAndConditions, + style: const TextStyle(color: Colors.blue), + ), + TextSpan( + text: L10n.of(context)!.andCertifyIAmAtLeast13YearsOfAge, + ), + ], + style: const TextStyle(color: Colors.white), + ), + ), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + margin: const EdgeInsets.only(top: 5), + child: Text( + controller.signupError ?? '', + style: const TextStyle(color: Colors.orange, fontSize: 12), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pangea/widgets/space/class_settings.dart b/lib/pangea/widgets/space/class_settings.dart new file mode 100644 index 000000000..a891377ce --- /dev/null +++ b/lib/pangea/widgets/space/class_settings.dart @@ -0,0 +1,235 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../widgets/matrix.dart'; +import '../../constants/language_keys.dart'; +import '../../constants/language_level_type.dart'; +import '../../constants/pangea_event_types.dart'; +import '../../controllers/language_list_controller.dart'; +import '../../controllers/pangea_controller.dart'; +import '../../extensions/pangea_room_extension.dart'; +import '../../models/language_model.dart'; +import '../../utils/error_handler.dart'; +import '../../utils/language_level_copy.dart'; +import '../user_settings/p_language_dropdown.dart'; +import '../user_settings/p_question_container.dart'; + +class ClassSettings extends StatefulWidget { + final String? roomId; + final bool startOpen; + + const ClassSettings({Key? key, this.roomId, this.startOpen = false}) + : super(key: key); + + @override + ClassSettingsState createState() => ClassSettingsState(); +} + +class ClassSettingsState extends State { + Room? room; + late ClassSettingsModel classSettings; + late bool isOpen; + final PangeaController pangeaController = MatrixState.pangeaController; + + final cityController = TextEditingController(); + final countryController = TextEditingController(); + final schoolController = TextEditingController(); + + ClassSettingsState({Key? key}); + + @override + void initState() { + room = widget.roomId != null + ? Matrix.of(context).client.getRoomById(widget.roomId!) + : null; + + classSettings = room?.classSettings ?? ClassSettingsModel(); + + isOpen = widget.startOpen; + + super.initState(); + } + + bool get sameLanguages => + classSettings.targetLanguage == classSettings.dominantLanguage; + + LanguageModel getLanguage({required bool isBase, required String? langCode}) { + final LanguageModel backup = isBase + ? pangeaController.pLanguageStore.baseOptions.first + : pangeaController.pLanguageStore.targetOptions.first; + if (langCode == null) return backup; + final LanguageModel byCode = PangeaLanguage.byLangCode(langCode); + return byCode.langCode != LanguageKeys.unknownLanguage ? byCode : backup; + } + + Future updatePermission(void Function() makeLocalRuleChange) async { + makeLocalRuleChange(); + if (room != null) { + await showFutureLoadingDialog( + context: context, + future: () => setClassSettings(room!.id), + ); + } + setState(() {}); + } + + void setTextControllerValues() { + classSettings.city = cityController.text; + classSettings.country = countryController.text; + classSettings.schoolName = schoolController.text; + } + + Future setClassSettings(String roomId) async { + try { + setTextControllerValues(); + + await Matrix.of(context).client.setRoomStateWithKey( + roomId, + PangeaEventTypes.classSettings, + '', + classSettings.toJson(), + ); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + @override + Widget build(BuildContext context) => Column( + children: [ + ListTile( + 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: Theme.of(context).textTheme.bodyLarge!.color, + child: const Icon(Icons.language), + ), + trailing: Icon( + isOpen + ? Icons.keyboard_arrow_down_outlined + : Icons.keyboard_arrow_right_outlined, + ), + onTap: () => setState(() => isOpen = !isOpen), + ), + if (isOpen) + AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: isOpen ? null : 0, + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 8.0), + child: Column( + children: [ + PQuestionContainer( + title: + L10n.of(context)!.selectClassRoomDominantLanguage), + PLanguageDropdown( + onChange: (p0) => updatePermission(() { + classSettings.dominantLanguage = p0.langCode; + }), + initialLanguage: getLanguage( + isBase: true, + langCode: classSettings.dominantLanguage, + ), + languages: pangeaController.pLanguageStore.baseOptions, + showMultilingual: true, + ), + PQuestionContainer( + title: L10n.of(context)!.selectTargetLanguage), + PLanguageDropdown( + onChange: (p0) => updatePermission(() { + classSettings.targetLanguage = p0.langCode; + }), + initialLanguage: getLanguage( + isBase: false, + langCode: classSettings.targetLanguage, + ), + languages: pangeaController.pLanguageStore.targetOptions, + ), + PQuestionContainer( + title: L10n.of(context)!.whatIsYourClassLanguageLevel), + Padding( + padding: const EdgeInsets.all(12.0), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + width: 0.5, + ), + borderRadius: + const BorderRadius.all(Radius.circular(10)), + ), + child: DropdownButton( + // Initial Value + hint: Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + classSettings.languageLevel == null + ? L10n.of(context)!.selectLanguageLevel + : LanguageLevelTextPicker.languageLevelText( + context, + classSettings.languageLevel!, + ), + style: const TextStyle().copyWith( + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, + fontSize: 14, + ), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + ), + isExpanded: true, + underline: Container(), + // Down Arrow Icon + icon: const Icon(Icons.keyboard_arrow_down), + // Array list of items + items: + LanguageLevelType.allInts.map((int levelOption) { + return DropdownMenuItem( + value: levelOption, + child: Text( + LanguageLevelTextPicker.languageLevelText( + context, levelOption), + style: const TextStyle().copyWith( + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, + fontSize: 14, + ), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ), + ); + }).toList(), + // After selecting the desired option,it will + // change button value to selected value + onChanged: (int? newValue) => updatePermission(() { + classSettings.languageLevel = newValue!; + }), + ), + ), + ), + ], + ), + ), + ), + ], + ); +} diff --git a/lib/pangea/widgets/subscription/subscription_buttons.dart b/lib/pangea/widgets/subscription/subscription_buttons.dart new file mode 100644 index 000000000..d17c47465 --- /dev/null +++ b/lib/pangea/widgets/subscription/subscription_buttons.dart @@ -0,0 +1,57 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/pages/settings_subscription/settings_subscription.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SubscriptionButtons extends StatelessWidget { + final SubscriptionManagementController controller; + final PangeaController pangeaController = MatrixState.pangeaController; + SubscriptionButtons({ + required this.controller, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + itemCount: pangeaController + .subscriptionController.subscription!.availableSubscriptions.length, + itemBuilder: (BuildContext context, int i) => Column( + children: [ + ListTile( + title: pangeaController.subscriptionController.subscription! + .availableSubscriptions[i].isTrial + ? Text(L10n.of(context)!.oneWeekTrial) + : Text( + pangeaController.subscriptionController.subscription! + .availableSubscriptions[i] + .displayName(context), + ), + subtitle: Text( + pangeaController.subscriptionController.subscription! + .availableSubscriptions[i] + .displayPrice(context), + ), + trailing: const Icon(Icons.keyboard_arrow_right_outlined), + selected: controller.selectedSubscription == + pangeaController.subscriptionController.subscription! + .availableSubscriptions[i], + selectedTileColor: + Theme.of(context).colorScheme.secondary.withAlpha(16), + onTap: () { + final SubscriptionDetails selected = pangeaController + .subscriptionController + .subscription! + .availableSubscriptions[i]; + controller.selectSubscription(selected); + }, + ), + const Divider(height: 1), + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/subscription/subscription_options.dart b/lib/pangea/widgets/subscription/subscription_options.dart new file mode 100644 index 000000000..6301ccb29 --- /dev/null +++ b/lib/pangea/widgets/subscription/subscription_options.dart @@ -0,0 +1,91 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SubscriptionOptions extends StatelessWidget { + final PangeaController pangeaController; + const SubscriptionOptions({ + super.key, + required this.pangeaController, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: ListView( + children: [ + const SizedBox(height: 20), + Wrap( + alignment: WrapAlignment.center, + direction: Axis.horizontal, + children: pangeaController + .subscriptionController.subscription!.availableSubscriptions + .map( + (subscription) => SubscriptionCard( + subscription: subscription, + pangeaController: pangeaController, + ), + ) + .toList(), + ), + ], + ), + ); + } +} + +class SubscriptionCard extends StatelessWidget { + final SubscriptionDetails subscription; + final PangeaController pangeaController; + + const SubscriptionCard({ + Key? key, + required this.subscription, + required this.pangeaController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder( + side: BorderSide( + color: AppConfig.primaryColorLight.withAlpha(64), + ), + borderRadius: const BorderRadius.all(Radius.zero), + ), + child: SizedBox( + height: 250, + width: AppConfig.columnWidth * 0.75, + child: Padding( + padding: const EdgeInsets.all(25), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + subscription.isTrial + ? L10n.of(context)!.oneWeekTrial + : subscription.displayName(context), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 24), + ), + Text( + subscription.displayPrice(context), + textAlign: TextAlign.center, + ), + OutlinedButton( + onPressed: () { + pangeaController.subscriptionController + .submitSubscriptionChange(subscription, context); + }, + child: Text(L10n.of(context)!.subscribe), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/subscription/subscription_paywall.dart b/lib/pangea/widgets/subscription/subscription_paywall.dart new file mode 100644 index 000000000..fd24f4662 --- /dev/null +++ b/lib/pangea/widgets/subscription/subscription_paywall.dart @@ -0,0 +1,53 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/widgets/subscription/subscription_options.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SubscriptionPaywall extends StatelessWidget { + final PangeaController pangeaController; + const SubscriptionPaywall({ + Key? key, + required this.pangeaController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + leading: CloseButton(onPressed: Navigator.of(context).pop), + title: Text( + L10n.of(context)!.getAccess, + style: const TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ), + body: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + if (pangeaController.matrixState.client.rooms.length > 1) ...[ + Text( + L10n.of(context)!.welcomeBack, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + ], + Text( + L10n.of(context)!.subscriptionDesc, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 20), + SubscriptionOptions( + pangeaController: pangeaController, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/user_settings/country_picker_tile.dart b/lib/pangea/widgets/user_settings/country_picker_tile.dart new file mode 100644 index 000000000..8b765551d --- /dev/null +++ b/lib/pangea/widgets/user_settings/country_picker_tile.dart @@ -0,0 +1,47 @@ +import 'dart:developer'; + +import 'package:country_picker/country_picker.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; + +import '../../models/user_model.dart'; + +class CountryPickerTile extends StatelessWidget { + final PangeaController pangeaController = MatrixState.pangeaController; + + CountryPickerTile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final Profile? profile = pangeaController.userController.userModel?.profile; + return ListTile( + title: Text( + "${L10n.of(context)!.countryInformation}: ${profile?.countryDisplayName(context) ?? ''} ${profile?.flagEmoji}", + ), + trailing: const Icon(Icons.edit_outlined), + onTap: () => showCountryPicker( + context: context, + showPhoneCode: + false, // optional. Shows phone code before the country name. + onSelect: (Country country) async { + showFutureLoadingDialog( + context: context, + future: () async { + try { + await pangeaController.userController.updateUserProfile( + country: country.displayNameNoCountryCode, + ); + } catch (err) { + debugger(when: kDebugMode); + } + }, + ); + }, + ), + ); + } +} diff --git a/lib/pangea/widgets/user_settings/language_tile.dart b/lib/pangea/widgets/user_settings/language_tile.dart new file mode 100644 index 000000000..db5d7f0d3 --- /dev/null +++ b/lib/pangea/widgets/user_settings/language_tile.dart @@ -0,0 +1,81 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/language_model.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 PangeaController pangeaController = MatrixState.pangeaController; + + LanguageTile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final LanguageModel? sourceLanguage = + pangeaController.languageController.userL1; + + final LanguageModel? targetLanguage = + pangeaController.languageController.userL2; + + //PTODO - placeholder saying 'select your languages' + // if (targetLanguage == null || sourceLanguage == null) { + // debugger(when: kDebugMode); + // return const SizedBox(); + // } + + return ListTile( + // title: Row( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.start, + // children: const [ + // Text("Source Language"), + // SizedBox( + // width: 10, + // ), + // Icon(Icons.arrow_right_alt_outlined, size: 20), + // SizedBox( + // width: 10, + // ), + // Text("Target Language"), + // ]), + title: Text(L10n.of(context)!.myLanguages), + subtitle: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + LanguageFlag( + language: sourceLanguage, + ), + const SizedBox( + width: 10, + ), + Text(sourceLanguage?.getDisplayName(context) ?? + L10n.of(context)!.sourceLanguage), + const SizedBox( + width: 10, + ), + const Icon(Icons.arrow_right_alt_outlined, size: 20), + const SizedBox( + width: 10, + ), + LanguageFlag( + language: targetLanguage, + ), + const SizedBox( + width: 10, + ), + Text(targetLanguage?.getDisplayName(context) ?? + L10n.of(context)!.targetLanguage), + ]), + trailing: const Icon(Icons.edit_outlined), + onTap: () => pLanguageDialog(context, () {}), + ); + } +} diff --git a/lib/pangea/widgets/user_settings/languages_i_speak_tile.dart b/lib/pangea/widgets/user_settings/languages_i_speak_tile.dart new file mode 100644 index 000000000..a7482b399 --- /dev/null +++ b/lib/pangea/widgets/user_settings/languages_i_speak_tile.dart @@ -0,0 +1,107 @@ +// import 'package:fluffychat/pangea/models/language_model.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_gen/gen_l10n/l10n.dart'; +// import 'package:future_loading_dialog/future_loading_dialog.dart'; + +// import '../../../widgets/matrix.dart'; +// import '../../controllers/pangea_controller.dart'; + +// class LanguagesISpeakTile extends StatelessWidget { +// final PangeaController pangeaController = MatrixState.pangeaController; + +// LanguagesISpeakTile({ +// Key? key, +// }) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return ListTile( +// trailing: const Icon(Icons.edit_outlined), +// title: Text(L10n.of(context)!.languagesISpeak), +// //PTODO - make a nice display of flags +// // subtitle: Text( +// // pangeaController.userController.userModel?.profile?.speaks +// // ?.toString() ?? +// // "", +// // ), +// onTap: () => showDialog( +// context: context, +// builder: (BuildContext context) { +// return const MultiSelectDialog(); +// }, +// ), +// ); +// } +// } + +// class MultiSelectDialog extends StatefulWidget { +// const MultiSelectDialog({Key? key}) : super(key: key); + +// @override +// State createState() => _MultiSelectDialogState(); +// } + +// class _MultiSelectDialogState extends State { +// final List _selectedValues = []; +// PangeaController pangeaController = MatrixState.pangeaController; + +// @override +// void initState() { +// super.initState(); +// // _selectedValues.addAll( +// // pangeaController.userController.userModel?.profile?.speaks ?? []); +// } + +// void _onItemCheckedChange(LanguageModel itemValue, bool? checked) { +// if (checked == null) return; +// setState(() { +// if (checked) { +// _selectedValues.add(itemValue.langCode); +// } else { +// _selectedValues.remove(itemValue.langCode); +// } +// }); +// } + +// @override +// Widget build(BuildContext context) { +// return AlertDialog( +// title: Text(L10n.of(context)!.languagesISpeak), +// contentPadding: const EdgeInsets.only(top: 12.0), +// content: SingleChildScrollView( +// child: ListTileTheme( +// contentPadding: const EdgeInsets.fromLTRB(14.0, 0.0, 24.0, 0.0), +// child: ListBody( +// children: +// pangeaController.pLanguageStore.baseOptions.map((language) { +// return CheckboxListTile( +// value: _selectedValues.contains(language.langCode), +// //PTODO - show flag with name +// title: Text(language.displayName), +// controlAffinity: ListTileControlAffinity.leading, +// onChanged: (checked) => _onItemCheckedChange(language, checked), +// ); +// }).toList(), +// ), +// ), +// ), +// actions: [ +// TextButton( +// onPressed: () => Navigator.pop(context), +// child: Text(L10n.of(context)!.cancel), +// ), +// TextButton( +// onPressed: () => showFutureLoadingDialog( +// context: context, +// future: (() async { +// await pangeaController.userController +// .updateUserProfile(speaks: _selectedValues); +// Navigator.pop(context); +// }), +// ), +// child: Text(L10n.of(context)!.ok), +// ) +// ], +// ); +// } +// } diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart new file mode 100644 index 000000000..8dc4936a2 --- /dev/null +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -0,0 +1,112 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; + +import '../../../config/themes.dart'; +import '../../../widgets/matrix.dart'; +import 'p_language_dropdown.dart'; +import 'p_question_container.dart'; + +pLanguageDialog(BuildContext parentContext, Function callback) { + final PangeaController pangeaController = MatrixState.pangeaController; + //PTODO: if source language not set by user, default to languge from device settings + LanguageModel selectedSourceLanguage = + pangeaController.languageController.userL1 ?? + pangeaController.pLanguageStore.targetOptions[0]; + LanguageModel selectedTargetLanguage = + pangeaController.languageController.userL2 ?? + pangeaController.pLanguageStore.targetOptions[1]; + + return showDialog( + useRootNavigator: false, + context: parentContext, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Scaffold( + backgroundColor: Colors.transparent, + body: AlertDialog( + title: Text(L10n.of(parentContext)!.updateLanguage), + content: SizedBox( + width: FluffyThemes.columnWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PQuestionContainer( + title: L10n.of(parentContext)!.whatIsYourBaseLanguage), + PLanguageDropdown( + onChange: (p0) => + setState(() => selectedSourceLanguage = p0), + initialLanguage: selectedSourceLanguage, + languages: pangeaController.pLanguageStore.baseOptions, + ), + PQuestionContainer( + title: + L10n.of(parentContext)!.whatLanguageYouWantToLearn), + PLanguageDropdown( + onChange: (p0) => + setState(() => selectedTargetLanguage = p0), + initialLanguage: selectedTargetLanguage, + languages: pangeaController.pLanguageStore.targetOptions, + ), + ], + ), + ), + actions: [ + TextButton( + child: Text(L10n.of(parentContext)!.cancel), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + onPressed: () { + selectedSourceLanguage.langCode != + selectedTargetLanguage.langCode + ? showFutureLoadingDialog( + context: context, + future: () async { + try { + await pangeaController.userController + .updateUserProfile( + sourceLanguage: + selectedSourceLanguage.langCode, + targetLanguage: + selectedTargetLanguage.langCode, + ); + Navigator.pop(context); + } catch (err, s) { + debugger(when: kDebugMode); + //PTODO-Lala add standard error message + ErrorHandler.logError(e: err, s: s); + rethrow; + } finally { + callback(); + } + }, + ) + : ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(parentContext)!.noIdenticalLanguages), + backgroundColor: + Theme.of(context).colorScheme.primary, + ), + ); + }, + child: Text(L10n.of(parentContext)!.saveChanges), + ), + ], + ), + ); + }, + ); + }, + ); +} diff --git a/lib/pangea/widgets/user_settings/p_language_dropdown.dart b/lib/pangea/widgets/user_settings/p_language_dropdown.dart new file mode 100644 index 000000000..3c40f9011 --- /dev/null +++ b/lib/pangea/widgets/user_settings/p_language_dropdown.dart @@ -0,0 +1,124 @@ +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:flutter/material.dart'; + +import '../../widgets/flag.dart'; + +class PLanguageDropdown extends StatefulWidget { + final List languages; + final LanguageModel initialLanguage; + final Function(LanguageModel) onChange; + final bool showMultilingual; + + const PLanguageDropdown( + {Key? key, + required this.languages, + required this.onChange, + required this.initialLanguage, + this.showMultilingual = false}) + : super(key: key); + + @override + State createState() => _PLanguageDropdownState(); +} + +class _PLanguageDropdownState extends State { + @override + Widget build(BuildContext context) { + final List sortedLanguages = widget.languages; + sortedLanguages.sort((a, b) => + a.getDisplayName(context)!.compareTo(b.getDisplayName(context)!)); + + return Padding( + padding: const EdgeInsets.all(12), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + width: 0.5, + ), + borderRadius: const BorderRadius.all(Radius.circular(10))), + child: DropdownButton( + // Initial Value + hint: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: LanguageFlag( + language: widget.initialLanguage, + ), + ), + const SizedBox(width: 10), + Text( + widget.initialLanguage.getDisplayName(context) ?? "", + style: const TextStyle().copyWith( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 14), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ) + ], + ), + + isExpanded: true, + // Down Arrow Icon + icon: const Icon(Icons.keyboard_arrow_down), + underline: Container(), + // Array list of items + items: [ + if (widget.showMultilingual) + DropdownMenuItem( + value: LanguageModel.multiLingual(context), + child: LanguageDropDownEntry( + languageModel: LanguageModel.multiLingual(context), + )), + ...sortedLanguages + .map( + (languageModel) => DropdownMenuItem( + value: languageModel, + child: LanguageDropDownEntry( + languageModel: languageModel, + )), + ) + .toList() + ], + onChanged: (value) => widget.onChange(value!), + ), + ), + ); + } +} + +class LanguageDropDownEntry extends StatelessWidget { + final LanguageModel languageModel; + const LanguageDropDownEntry({ + Key? key, + required this.languageModel, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LanguageFlag( + language: languageModel, + ), + const SizedBox(width: 10), + Text( + languageModel.getDisplayName(context) ?? "", + style: const TextStyle().copyWith( + color: Theme.of(context).textTheme.bodyLarge!.color, + fontSize: 14), + overflow: TextOverflow.clip, + textAlign: TextAlign.center, + ) + ], + ), + ); + } +} diff --git a/lib/pangea/widgets/user_settings/p_question_container.dart b/lib/pangea/widgets/user_settings/p_question_container.dart new file mode 100644 index 000000000..ac6c48409 --- /dev/null +++ b/lib/pangea/widgets/user_settings/p_question_container.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class PQuestionContainer extends StatelessWidget { + final String title; + const PQuestionContainer({super.key, required this.title}); + @override + Widget build(BuildContext context) { + final Size size = MediaQuery.of(context).size; + return Container( + constraints: const BoxConstraints(minWidth: 100, maxWidth: 650), + padding: EdgeInsets.all(size.height * 0.01), + alignment: Alignment.centerLeft, + child: Text( + title, + style: const TextStyle().copyWith( + color: Theme.of(context).textTheme.bodyText1!.color, fontSize: 14), + overflow: TextOverflow.clip, + textAlign: TextAlign.left, + ), + ); + } +} diff --git a/lib/pangea/widgets/user_settings/p_settings_switch_list_tile.dart b/lib/pangea/widgets/user_settings/p_settings_switch_list_tile.dart new file mode 100644 index 000000000..9b37a5719 --- /dev/null +++ b/lib/pangea/widgets/user_settings/p_settings_switch_list_tile.dart @@ -0,0 +1,40 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + +class PSettingsSwitchListTile extends StatefulWidget { + final bool defaultValue; + final String pStoreKey; + final String title; + final String? subtitle; + + const PSettingsSwitchListTile.adaptive({ + Key? key, + this.defaultValue = false, + required this.pStoreKey, + required this.title, + this.subtitle, + }) : super(key: key); + + @override + PSettingsSwitchListTileState createState() => PSettingsSwitchListTileState(); +} + +class PSettingsSwitchListTileState extends State { + @override + Widget build(BuildContext context) { + final PangeaController pangeaController = MatrixState.pangeaController; + return SwitchListTile.adaptive( + value: pangeaController.pStoreService.read(widget.pStoreKey) ?? + widget.defaultValue, + title: Text(widget.title), + activeColor: AppConfig.activeToggleColor, + subtitle: widget.subtitle != null ? Text(widget.subtitle!) : null, + onChanged: (bool newValue) async { + pangeaController.pStoreService.save(widget.pStoreKey, newValue); + setState(() {}); + }, + ); + } +} diff --git a/lib/pangea/word_cloud/word_cloud.dart b/lib/pangea/word_cloud/word_cloud.dart new file mode 100644 index 000000000..4a51ab761 --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud.dart @@ -0,0 +1,8 @@ +library word_cloud; + +export 'word_cloud_data.dart'; +export 'word_cloud_setting.dart'; +export 'word_cloud_shape.dart'; +export 'word_cloud_tap_view.dart'; +export 'word_cloud_tap.dart'; +export 'word_cloud_view.dart'; \ No newline at end of file diff --git a/lib/pangea/word_cloud/word_cloud_data.dart b/lib/pangea/word_cloud/word_cloud_data.dart new file mode 100644 index 000000000..abc1c0d1a --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud_data.dart @@ -0,0 +1,29 @@ +class WordCloudData { + List data = []; + + WordCloudData({ + required this.data, + }) { + data = (data..sort((a, b) => (a['value'] ?? 0).compareTo(b['value'] ?? 0))) + .reversed + .toList(); + } + + void addDataAsMapList(List newdata) { + data.addAll(newdata); + data = (data..sort((a, b) => a['value'].compareTo(b['value']))) + .reversed + .toList(); + } + + void addData(String word, double value) { + data.add({'word': word, 'value': value}); + data = (data..sort((a, b) => a['value'].compareTo(b['value']))) + .reversed + .toList(); + } + + List getData() { + return data; + } +} diff --git a/lib/pangea/word_cloud/word_cloud_setting.dart b/lib/pangea/word_cloud/word_cloud_setting.dart new file mode 100644 index 000000000..06c80933d --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud_setting.dart @@ -0,0 +1,349 @@ +import 'dart:math'; + +import 'package:fluffychat/pangea/word_cloud/word_cloud_shape.dart'; +import 'package:flutter/material.dart'; + +class WordCloudSetting { + double mapX = 0; + double mapY = 0; + String? fontFamily; + FontStyle? fontStyle; + FontWeight? fontWeight; + List data = []; + List map = [[]]; + List textCenter = []; + List textPoints = []; + List textlist = []; + List isdrawed = []; + double centerX = 0; + double centerY = 0; + double minTextSize; + double maxTextSize; + WordCloudShape shape; + int attempt; + List? colorList = [Colors.black]; + + WordCloudSetting({ + Key? key, + required this.data, + required this.minTextSize, + required this.maxTextSize, + required this.attempt, + required this.shape, + }); + + void setMapSize(double x, double y) { + mapX = x; + mapY = y; + } + + void setColorList(List? colors) { + colorList = colors; + } + + void setFont(String? family, FontStyle? style, FontWeight? weight) { + fontFamily = family; + fontStyle = style; + fontWeight = weight; + } + + List setMap(dynamic shape) { + final List makemap = [[]]; + switch (shape.getType()) { + case 'normal': + for (var i = 0; i < mapX; i++) { + for (var j = 0; j < mapY; j++) { + makemap[i].add(0); + } + makemap.add([]); + } + break; + + case 'circle': + for (var i = 0; i < mapX; i++) { + for (var j = 0; j < mapY; j++) { + if (pow(i - (mapX / 2), 2) + pow(j - (mapY / 2), 2) > + pow(shape.getRadius(), 2)) { + makemap[i].add(1); + } else { + makemap[i].add(0); + } + } + makemap.add([]); + } + break; + + case 'ellipse': + for (var i = 0; i < mapX; i++) { + for (var j = 0; j < mapY; j++) { + if (pow(i - (mapX / 2), 2) / pow(shape.getMajorAxis(), 2) + + pow(j - (mapY / 2), 2) / pow(shape.getMinorAxis(), 2) > + 1) { + makemap[i].add(1); + } else { + makemap[i].add(0); + } + } + makemap.add([]); + } + break; + } + return makemap; + } + + void setInitial() { + //map = [[]]; + textCenter = []; + textPoints = []; + textlist = []; + isdrawed = []; + + centerX = mapX / 2; + centerY = mapY / 2; + + map = setMap(shape); + + // for (var i = 0; i < mapX; i++) { + // for (var j = 0; j < mapY; j++) { + // if (pow(i - (mapX / 2), 2) + pow(j - (mapY / 2), 2) > pow(250, 2)) { + // map[i].add(1); + // } else { + // map[i].add(0); + // } + // } + // map.add([]); + // } + + for (var i = 0; i < data.length; i++) { + final double getTextSize = + (minTextSize * (data[0]['value'] - data[i]['value']) + + maxTextSize * + (data[i]['value'] - data[data.length - 1]['value'])) / + (data[0]['value'] - data[data.length - 1]['value']); + + final textSpan = TextSpan( + text: data[i]['word'], + style: TextStyle( + color: colorList?[Random().nextInt(colorList!.length)], + fontSize: getTextSize, + fontWeight: fontWeight, + fontFamily: fontFamily, + fontStyle: fontStyle, + ), + ); + + final textPainter = TextPainter() + ..text = textSpan + ..textDirection = TextDirection.ltr + ..textAlign = TextAlign.center + ..layout(); + + textlist.add(textPainter); + + final double centerCorrectionX = centerX - textlist[i].width / 2; + final double centerCorrectionY = centerY - textlist[i].height / 2; + textCenter.add([centerCorrectionX, centerCorrectionY]); + textPoints.add([]); + isdrawed.add(false); + } + } + + void setTextStyle(List newstyle) { + //only support color, weight, family, fontstyle + textlist = []; + textCenter = []; + textPoints = []; + isdrawed = []; + + for (var i = 0; i < data.length; i++) { + final double getTextSize = + (minTextSize * (data[0]['value'] - data[i]['value']) + + maxTextSize * + (data[i]['value'] - data[data.length - 1]['value'])) / + (data[0]['value'] - data[data.length - 1]['value']); + + final textSpan = TextSpan( + text: data[i]['word'], + style: TextStyle( + color: newstyle[i].color, + fontSize: getTextSize, + fontWeight: newstyle[i].fontWeight, + fontFamily: newstyle[i].fontFamily, + fontStyle: newstyle[i].fontStyle, + ), + ); + + final textPainter = TextPainter() + ..text = textSpan + ..textDirection = TextDirection.ltr + ..textAlign = TextAlign.center + ..layout(); + + textlist.add(textPainter); + + final double centerCorrectionX = centerX - textlist[i].width / 2; + final double centerCorrectionY = centerY - textlist[i].height / 2; + textCenter.add([centerCorrectionX, centerCorrectionY]); + textPoints.add([]); + isdrawed.add(false); + } + } + + bool checkMap(double x, double y, double w, double h) { + if (mapX - x < w) { + return false; + } + if (mapY - y < h) { + return false; + } + for (int i = x.toInt(); i < x.toInt() + w; i++) { + for (int j = y.toInt(); j < y.toInt() + h; j++) { + if (map[i][j] == 1) { + return false; + } + } + } + return true; + } + + bool checkMapOptimized(int x, int y, double w, double h) { + if (mapX - x < w) { + return false; + } + if (mapY - y < h) { + return false; + } + for (int i = x.toInt(); i < x.toInt() + w; i++) { + if (map[i][y + h - 1] == 1) { + return false; + } + if (map[i][y + 1] == 1) { + return false; + } + } + return true; + } + + void drawIn(int index, double x, double y) { + textPoints[index] = [x, y]; + for (int i = x.toInt(); i < x.toInt() + textlist[index].width; i++) { + for (int j = y.toInt(); + j < y.toInt() + textlist[index].height.floor(); + j++) { + map[i][j] = 1; + } + } + } + + void drawTextOptimized() { + drawIn(0, textCenter[0][0], textCenter[0][1]); + isdrawed[0] = true; + bool checkattempt = false; + for (var i = 1; i < textlist.length; i++) { + final double w = textlist[i].width; + final double h = textlist[i].height; + int attempts = 0; + + bool isadded = false; + + while (!isadded) { + final int getX = Random().nextInt(mapX.toInt() - w.toInt()); + final int direction = Random().nextInt(2); + if (direction == 0) { + for (int y = textCenter[i][1].toInt(); y > 0; y--) { + if (checkMapOptimized(getX, y, w, h)) { + drawIn(i, getX.toDouble(), y.toDouble()); + isadded = true; + isdrawed[i] = true; + break; + } + } + } else if (direction == 1) { + for (int y = textCenter[i][1].toInt(); y < mapY; y++) { + if (checkMapOptimized(getX, y, w, h)) { + drawIn(i, getX.toDouble(), y.toDouble()); + isadded = true; + isdrawed[i] = true; + break; + } + } + } + attempts += 1; + if (attempts > attempt) { + isadded = true; + checkattempt = true; + } + } + if (checkattempt) { + return; + } + } + } + + void drawText() { + drawIn(0, textCenter[0][0], textCenter[0][1]); + for (var i = 1; i < textlist.length; i++) { + final double w = textlist[i].width; + final double h = textlist[i].height; + int attempts = 0; + + bool isadded = false; + + while (!isadded) { + final int getX = Random().nextInt(mapX.toInt() - w.toInt()); + final int direction = Random().nextInt(2); + if (direction == 0) { + for (int y = textCenter[i][1].toInt(); y > 0; y--) { + if (checkMap(getX.toDouble(), y.toDouble(), w, h)) { + drawIn(i, getX.toDouble(), y.toDouble()); + isadded = true; + break; + } + } + if (!isadded) { + for (int y = textCenter[i][1].toInt(); y < mapY; y++) { + if (checkMap(getX.toDouble(), y.toDouble(), w, h)) { + drawIn(i, getX.toDouble(), y.toDouble()); + isadded = true; + break; + } + } + } + } else if (direction == 1) { + for (int y = textCenter[i][1].toInt(); y < mapY; y++) { + if (checkMap(getX.toDouble(), y.toDouble(), w, h)) { + drawIn(i, getX.toDouble(), y.toDouble()); + isadded = true; + break; + } + } + if (!isadded) { + for (int y = textCenter[i][1].toInt(); y > 0; y--) { + if (checkMap(getX.toDouble(), y.toDouble(), w, h)) { + drawIn(i, getX.toDouble(), y.toDouble()); + isadded = true; + break; + } + } + } + } + attempts += 1; + if (attempts > attempt) { + isadded = true; + } + } + } + } + + List getWordPoint() { + return textPoints; + } + + List getTextPainter() { + return textlist; + } + + int getDataLength() { + return data.length; + } +} diff --git a/lib/pangea/word_cloud/word_cloud_shape.dart b/lib/pangea/word_cloud/word_cloud_shape.dart new file mode 100644 index 000000000..2c5b1bd83 --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud_shape.dart @@ -0,0 +1,44 @@ +class WordCloudShape { + double boundaryStartX = 0; + double boundaryEndX = 0; + double boundaryStartY = 0; + double boundaryEndY = 0; + String type = 'normal'; + + String getType() { + return type; + } +} + +class WordCloudCircle extends WordCloudShape { + double radius; + + WordCloudCircle({required this.radius}) { + type = 'circle'; + } + + double getRadius() { + return radius; + } +} + +class WordCloudEllipse extends WordCloudShape { + double majoraxis; + double minoraxis; + WordCloudEllipse({ + required this.majoraxis, + required this.minoraxis, + }) { + type = 'ellipse'; + } + + double getMajorAxis() { + return majoraxis; + } + + double getMinorAxis() { + return minoraxis; + } + + +} diff --git a/lib/pangea/word_cloud/word_cloud_tap.dart b/lib/pangea/word_cloud/word_cloud_tap.dart new file mode 100644 index 000000000..0583fd214 --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud_tap.dart @@ -0,0 +1,11 @@ +class WordCloudTap { + Map wordtap = {}; + + void addWordtap(String word, Function func) { + wordtap[word] = func; + } + + Map getWordTaps() { + return wordtap; + } +} diff --git a/lib/pangea/word_cloud/word_cloud_tap_view.dart b/lib/pangea/word_cloud/word_cloud_tap_view.dart new file mode 100644 index 000000000..a062acb71 --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud_tap_view.dart @@ -0,0 +1,126 @@ +import 'package:fluffychat/pangea/word_cloud/word_cloud_data.dart'; +import 'package:fluffychat/pangea/word_cloud/word_cloud_setting.dart'; +import 'package:fluffychat/pangea/word_cloud/word_cloud_shape.dart'; +import 'package:fluffychat/pangea/word_cloud/word_cloud_tap.dart'; +import 'package:flutter/material.dart'; + +class WordCloudTapView extends StatefulWidget { + final WordCloudData data; + final Color? mapcolor; + final Decoration? decoration; + final double mapwidth; + final String? fontFamily; + final FontStyle? fontStyle; + final FontWeight? fontWeight; + final double mapheight; + final List? colorlist; + final int attempt; + final double mintextsize; + final double maxtextsize; + final WordCloudShape? shape; + final WordCloudTap wordtap; + + const WordCloudTapView({ + super.key, + required this.data, + required this.mapwidth, + required this.mapheight, + required this.wordtap, + this.mintextsize = 10, + this.maxtextsize = 100, + this.attempt = 30, + this.shape, + this.fontFamily, + this.fontStyle, + this.fontWeight, + this.mapcolor, + this.decoration, + this.colorlist, + }); + @override + State createState() => _WordCloudTapViewState(); +} + +class _WordCloudTapViewState extends State { + late WordCloudShape wcshape; + late WordCloudSetting wordcloudsetting; + + @override + void initState() { + super.initState(); + if (widget.shape == null) { + wcshape = WordCloudShape(); + } else { + wcshape = widget.shape!; + } + + wordcloudsetting = WordCloudSetting( + data: widget.data.getData(), + minTextSize: widget.mintextsize, + maxTextSize: widget.maxtextsize, + attempt: widget.attempt, + shape: wcshape, + ); + + wordcloudsetting.setMapSize(widget.mapwidth, widget.mapheight); + wordcloudsetting.setFont( + widget.fontFamily, widget.fontStyle, widget.fontWeight); + wordcloudsetting.setColorList(widget.colorlist); + wordcloudsetting.setInitial(); + wordcloudsetting.drawTextOptimized(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onVerticalDragDown: (details) { + for (var i = 0; i < widget.data.getData().length; i++) { + final List points = wordcloudsetting.textPoints; + final double w = wordcloudsetting.textlist[i].width; + final double h = wordcloudsetting.textlist[i].height; + if (points[i][0] < details.localPosition.dx && + details.localPosition.dx < (points[i][0] + w) && + points[i][1] < details.localPosition.dy && + details.localPosition.dy < (points[i][1] + h)) { + if (widget.wordtap + .getWordTaps() + .containsKey(widget.data.getData()[i]['word'])) { + widget.wordtap.getWordTaps()[widget.data.getData()[i]['word']]!(); + } + } + } + }, + child: Container( + width: widget.mapwidth, + height: widget.mapheight, + color: widget.mapcolor, + decoration: widget.decoration, + child: CustomPaint( + painter: WCTpaint(wordcloudpaint: wordcloudsetting), + ), + ), + ); + } +} + +class WCTpaint extends CustomPainter { + final WordCloudSetting wordcloudpaint; + WCTpaint({ + required this.wordcloudpaint, + }); + + @override + void paint(Canvas canvas, Size size) { + for (var i = 0; i < wordcloudpaint.getDataLength(); i++) { + if (wordcloudpaint.isdrawed[i]) { + wordcloudpaint.getTextPainter()[i].paint( + canvas, + Offset(wordcloudpaint.getWordPoint()[i][0], + wordcloudpaint.getWordPoint()[i][1])); + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/pangea/word_cloud/word_cloud_view.dart b/lib/pangea/word_cloud/word_cloud_view.dart new file mode 100644 index 000000000..e4c95b953 --- /dev/null +++ b/lib/pangea/word_cloud/word_cloud_view.dart @@ -0,0 +1,105 @@ +import 'package:fluffychat/pangea/word_cloud/word_cloud_data.dart'; +import 'package:fluffychat/pangea/word_cloud/word_cloud_setting.dart'; +import 'package:fluffychat/pangea/word_cloud/word_cloud_shape.dart'; +import 'package:flutter/material.dart'; + +class WordCloudView extends StatefulWidget { + final WordCloudData data; + final Color? mapcolor; + final Decoration? decoration; + final double mapwidth; + final String? fontFamily; + final FontStyle? fontStyle; + final FontWeight? fontWeight; + final double mapheight; + final List? colorlist; + final int attempt; + final double mintextsize; + final double maxtextsize; + final WordCloudShape? shape; + + const WordCloudView({ + super.key, + required this.data, + required this.mapwidth, + required this.mapheight, + this.mintextsize = 10, + this.maxtextsize = 100, + this.attempt = 30, + this.shape, + this.fontFamily, + this.fontStyle, + this.fontWeight, + this.mapcolor, + this.decoration, + this.colorlist, + }); + @override + State createState() => _WordCloudViewState(); +} + +class _WordCloudViewState extends State { + late WordCloudShape wcshape; + late WordCloudSetting wordcloudsetting; + + @override + void initState() { + super.initState(); + if (widget.shape == null) { + wcshape = WordCloudShape(); + } else { + wcshape = widget.shape!; + } + + wordcloudsetting = WordCloudSetting( + data: widget.data.getData(), + minTextSize: widget.mintextsize, + maxTextSize: widget.maxtextsize, + attempt: widget.attempt, + shape: wcshape, + ); + + wordcloudsetting.setMapSize(widget.mapwidth, widget.mapheight); + wordcloudsetting.setFont( + widget.fontFamily, widget.fontStyle, widget.fontWeight); + wordcloudsetting.setColorList(widget.colorlist); + wordcloudsetting.setInitial(); + + wordcloudsetting.drawTextOptimized(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: widget.mapwidth, + height: widget.mapheight, + color: widget.mapcolor, + decoration: widget.decoration, + child: CustomPaint( + painter: WCpaint(wordcloudpaint: wordcloudsetting), + ), + ); + } +} + +class WCpaint extends CustomPainter { + final WordCloudSetting wordcloudpaint; + WCpaint({ + required this.wordcloudpaint, + }); + + @override + void paint(Canvas canvas, Size size) { + for (var i = 0; i < wordcloudpaint.getDataLength(); i++) { + if (wordcloudpaint.isdrawed[i]) { + wordcloudpaint.getTextPainter()[i].paint( + canvas, + Offset(wordcloudpaint.getWordPoint()[i][0], + wordcloudpaint.getWordPoint()[i][1])); + } + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/utils/adaptive_bottom_sheet.dart b/lib/utils/adaptive_bottom_sheet.dart index d2a9a8b6d..8b4a96c30 100644 --- a/lib/utils/adaptive_bottom_sheet.dart +++ b/lib/utils/adaptive_bottom_sheet.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; Future showAdaptiveBottomSheet({ required BuildContext context, diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 1f04d4d69..41b225473 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -21,9 +21,16 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; +import 'package:fluffychat/utils/push_helper.dart'; +import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -31,9 +38,6 @@ import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; import 'package:unifiedpush/unifiedpush.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; -import 'package:fluffychat/utils/push_helper.dart'; -import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import '../config/app_config.dart'; import '../config/setting_keys.dart'; import '../widgets/matrix.dart'; @@ -66,13 +70,21 @@ class BackgroundPush { final pendingTests = >{}; - final dynamic firebase = null; //FcmSharedIsolate(); + // final dynamic firebase = null; //FcmSharedIsolate(); + // #Pangea + // uncommented to enable notifications on IOS + final FcmSharedIsolate? firebase = FcmSharedIsolate(); + // Pangea# DateTime? lastReceivedPush; bool upAction = false; BackgroundPush._(this.client) { + // #Pangea + onLogin ??= + client.onLoginStateChanged.stream.listen(handleLoginStateChanged); + // Pangea# onRoomSync ??= client.onSync.stream .where((s) => s.hasRoomUpdate) .listen((s) => _onClearingPush(getFromServer: false)); @@ -86,6 +98,9 @@ class BackgroundPush { activeRoomId: matrix?.activeRoomId, onSelectNotification: goToRoom, ), + // #Pangea + onNewToken: _newFcmToken, + // Pangea# ); if (Platform.isAndroid) { UnifiedPush.initialize( @@ -110,9 +125,27 @@ class BackgroundPush { instance.matrix = matrix; // ignore: prefer_initializing_formals instance.onFcmError = onFcmError; + // #Pangea + instance.fullInit(); + // Pangea# return instance; } + // #Pangea + Future fullInit() => setupPush(); + + void handleLoginStateChanged(_) => setupPush(); + + StreamSubscription? onLogin; + StreamSubscription? onRoomSync; + + void _newFcmToken(String token) { + _fcmToken = token; + debugPrint('Fcm foken $_fcmToken'); + setupPush(); + } + // Pangea# + Future cancelNotification(String roomId) async { Logs().v('Cancel notification for room', roomId); final id = await mapRoomIdToInt(roomId); @@ -132,8 +165,6 @@ class BackgroundPush { } } - StreamSubscription? onRoomSync; - Future setupPusher({ String? gatewayUrl, String? token, @@ -171,6 +202,9 @@ class BackgroundPush { currentPushers.first.appDisplayName == clientName && currentPushers.first.deviceDisplayName == client.deviceName && currentPushers.first.lang == 'en' && + // #Pangea + currentPushers.first.lang == LanguageKeys.defaultLanguage && + // Pangea# currentPushers.first.data.url.toString() == gatewayUrl && currentPushers.first.data.format == AppConfig.pushNotificationsPusherFormat && @@ -210,7 +244,10 @@ class BackgroundPush { appId: thisAppId, appDisplayName: clientName, deviceDisplayName: client.deviceName!, - lang: 'en', + //#Pangea + // lang: 'en', + lang: LanguageKeys.defaultLanguage, + // Pangea# data: PusherData( url: Uri.parse(gatewayUrl!), format: AppConfig.pushNotificationsPusherFormat, @@ -222,6 +259,9 @@ class BackgroundPush { ); } catch (e, s) { Logs().e('[Push] Unable to set pushers', e, s); + // #Pangea + ErrorHandler.logError(e: e, s: s); + // Pangea# } } } @@ -271,9 +311,14 @@ class BackgroundPush { if (matrix == null) { return; } + // #Pangea if (await store.getItemBool(SettingKeys.showNoGoogle, true) == true) { return; } + // if (await store.getItemBool(SettingKeys.showNoGoogle, false) == true) { + // return; + // } + // Pangea# await loadLocale(); WidgetsBinding.instance.addPostFrameCallback((_) { if (PlatformInfos.isAndroid) { @@ -293,7 +338,10 @@ class BackgroundPush { Logs().v('Setup firebase'); if (_fcmToken?.isEmpty ?? true) { try { - _fcmToken = await firebase?.getToken(); + // #Pangea + // _fcmToken = await firebase?.getToken(); + _fcmToken = await _getToken(); + // Pangea# if (_fcmToken == null) throw ('PushToken is null'); } catch (e, s) { Logs().w('[Push] cannot get token', e, e is String ? null : s); @@ -326,6 +374,9 @@ class BackgroundPush { .go('/${isStory ? 'rooms/stories' : 'rooms'}/$roomId'); } catch (e, s) { Logs().e('[Push] Failed to open room', e, s); + // #Pangea + ErrorHandler.logError(e: e, s: s); + // Pangea# } } @@ -365,7 +416,10 @@ class BackgroundPush { Logs().i('[Push] UnifiedPush using endpoint $endpoint'); final oldTokens = {}; try { - final fcmToken = await firebase?.getToken(); + // #Pangea + // final fcmToken = await firebase?.getToken(); + final fcmToken = await _getToken(); + // Pangea# oldTokens.add(fcmToken); } catch (_) {} await setupPusher( @@ -497,4 +551,16 @@ class BackgroundPush { _clearingPushLock = false; } } + + // #Pangea + Future _getToken() async { + if (Platform.isAndroid) { + await Firebase.initializeApp( + // options: DefaultFirebaseOptions.currentPlatform, + ); + return (await FirebaseMessaging.instance.getToken()); + } + return await firebase?.getToken(); + } + // Pangea# } diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index 1ee71d491..0c983f99b 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -1,16 +1,16 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; +import 'package:fluffychat/utils/custom_http_client.dart'; +import 'package:fluffychat/utils/custom_image_resizer.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; - import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:fluffychat/utils/custom_http_client.dart'; -import 'package:fluffychat/utils/custom_image_resizer.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'famedlysdk_store.dart'; abstract class ClientManager { @@ -107,6 +107,13 @@ abstract class ClientManager { 'im.ponies.room_emotes', // To check which story room we can post in EventTypes.RoomPowerLevels, + // #Pangea + PangeaEventTypes.classSettings, + PangeaEventTypes.rules, + PangeaEventTypes.vocab, + EventTypes.RoomTopic, + EventTypes.RoomAvatar, + // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, databaseBuilder: FlutterHiveCollectionsDatabase.databaseBuilder, diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index 121eaf72e..fac35352d 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:intl/intl.dart'; diff --git a/lib/utils/error_reporter.dart b/lib/utils/error_reporter.dart index 3f8a003d4..8869966eb 100644 --- a/lib/utils/error_reporter.dart +++ b/lib/utils/error_reporter.dart @@ -1,16 +1,14 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; -import 'package:url_launcher/url_launcher.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher.dart'; class ErrorReporter { final BuildContext context; diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart index 92f60b1dd..598e88d34 100644 --- a/lib/utils/fluffy_share.dart +++ b/lib/utils/fluffy_share.dart @@ -1,10 +1,9 @@ +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import '../widgets/matrix.dart'; abstract class FluffyShare { diff --git a/lib/utils/matrix_sdk_extensions/client_stories_extension.dart b/lib/utils/matrix_sdk_extensions/client_stories_extension.dart index dd11af0f2..184afc8fd 100644 --- a/lib/utils/matrix_sdk_extensions/client_stories_extension.dart +++ b/lib/utils/matrix_sdk_extensions/client_stories_extension.dart @@ -1,11 +1,9 @@ -import 'package:flutter/cupertino.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; - extension ClientStoriesExtension on Client { static const String storiesRoomType = 'msc3588.stories.stories-room'; static const String storiesBlockListType = 'msc3588.stories.block-list'; diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index 241ae1a6f..989d0d25b 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart' hide Key; import 'package:flutter/services.dart'; - import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/utils/matrix_sdk_extensions/ios_badge_client_extension.dart b/lib/utils/matrix_sdk_extensions/ios_badge_client_extension.dart new file mode 100644 index 000000000..bea7713d0 --- /dev/null +++ b/lib/utils/matrix_sdk_extensions/ios_badge_client_extension.dart @@ -0,0 +1,20 @@ +import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/utils/platform_infos.dart'; + +extension IosBadgeClientExtension on Client { + void updateIosBadge() { + if (PlatformInfos.isIOS) { + // Workaround for iOS not clearing notifications with fcm_shared_isolate + if (!rooms.any( + (r) => r.membership == Membership.invite || (r.notificationCount > 0), + )) { + // ignore: unawaited_futures + FlutterLocalNotificationsPlugin().cancelAll(); + FlutterAppBadger.removeBadge(); + } + } + } +} diff --git a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart index 183fb753f..438af8b00 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_file_extension.dart @@ -1,8 +1,9 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/size_string.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'; @@ -10,9 +11,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/size_string.dart'; - extension MatrixFileExtension on MatrixFile { void save(BuildContext context) async { if (PlatformInfos.isIOS) { diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index 44eab72dc..4a9b68274 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -317,4 +317,7 @@ class MatrixLocals extends MatrixLocalizations { @override String get unknownUser => l10n.user; + + @override + String hasKnocked(String targetName) => l10n.hasKnocked(targetName); } diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index b96104df4..6cb6bbb16 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -2,7 +2,6 @@ import 'dart:io'; 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:package_info_plus/package_info_plus.dart'; @@ -46,6 +45,9 @@ abstract class PlatformInfos { final version = await PlatformInfos.getVersion(); showAboutDialog( context: context, + // #Pangea + useRootNavigator: false, + // Pangea# children: [ Text('Version: $version'), TextButton.icon( diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index b5111c4c0..6c361de32 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -2,19 +2,20 @@ import 'dart:convert'; import 'dart:io'; import 'dart:ui'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/voip/callkeep_manager.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/setting_keys.dart'; -import 'package:fluffychat/utils/client_manager.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/voip/callkeep_manager.dart'; - Future pushHelper( PushNotification notification, { Client? client, @@ -44,11 +45,14 @@ Future pushHelper( onDidReceiveBackgroundNotificationResponse: onSelectNotification, ); - l10n ??= lookupL10n(const Locale('en')); + // #Pangea + debugPrint('Push notification ${notification.content}'); + // l10n ??= lookupL10n(const Locale('en')); + // Pangea# flutterLocalNotificationsPlugin.show( 0, - l10n.newMessageInFluffyChat, - l10n.openAppToReadMessages, + l10n?.newMessageInFluffyChat, + l10n?.openAppToReadMessages, NotificationDetails( iOS: const DarwinNotificationDetails(), android: AndroidNotificationDetails( @@ -56,7 +60,7 @@ Future pushHelper( AppConfig.pushNotificationsChannelName, channelDescription: AppConfig.pushNotificationsChannelDescription, number: notification.counts?.unread, - ticker: l10n.unreadChats(notification.counts?.unread ?? 1), + ticker: l10n?.unreadChats(notification.counts?.unread ?? 1), importance: Importance.max, priority: Priority.max, ), @@ -174,6 +178,9 @@ Future _tryPushHelper( : await DefaultCacheManager().getSingleFile(avatar); } catch (e, s) { Logs().e('Unable to get avatar picture', e, s); + // #Pangea + ErrorHandler.logError(e: e, s: s); + // Pangea# } final id = await mapRoomIdToInt(event.room.id); diff --git a/lib/utils/uia_request_manager.dart b/lib/utils/uia_request_manager.dart index 04c8b4844..716c0d172 100644 --- a/lib/utils/uia_request_manager.dart +++ b/lib/utils/uia_request_manager.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - extension UiaRequestManager on MatrixState { Future uiaRequestHandler(UiaRequest uiaRequest) async { final l10n = L10n.of(context)!; diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index d6c868381..8aded8f5d 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -1,8 +1,12 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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'; @@ -10,11 +14,6 @@ import 'package:matrix/matrix.dart'; import 'package:punycode/punycode.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; -import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import 'platform_infos.dart'; class UrlLauncher { diff --git a/lib/utils/voip_plugin.dart b/lib/utils/voip_plugin.dart index 3cc9d5161..8a3a0f2ee 100644 --- a/lib/utils/voip_plugin.dart +++ b/lib/utils/voip_plugin.dart @@ -1,16 +1,15 @@ import 'dart:core'; +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/dialer/dialer.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as webrtc_impl; import 'package:matrix/matrix.dart'; import 'package:webrtc_interface/webrtc_interface.dart' hide Navigator; -import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/dialer/dialer.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import '../../utils/famedlysdk_store.dart'; import '../../utils/voip/callkeep_manager.dart'; import '../../utils/voip/user_media_manager.dart'; diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 7d43f8f3c..76a3a49dd 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -1,9 +1,7 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; class Avatar extends StatelessWidget { final Uri? mxContent; @@ -13,6 +11,9 @@ class Avatar extends StatelessWidget { static const double defaultSize = 44; final Client? client; final double fontSize; + //#Pangea + final IconData? littleIcon; + // Pangea# const Avatar({ this.mxContent, @@ -21,6 +22,9 @@ class Avatar extends StatelessWidget { this.onTap, this.client, this.fontSize = 18, + //#Pangea + this.littleIcon, + // Pangea# Key? key, }) : super(key: key); @@ -48,27 +52,74 @@ class Avatar extends StatelessWidget { ), ); final borderRadius = BorderRadius.circular(size / 2); - final container = ClipRRect( - borderRadius: borderRadius, - child: Container( - width: size, - height: size, - color: noPic - ? name?.lightColorAvatar - : Theme.of(context).secondaryHeaderColor, - child: noPic - ? textWidget - : MxcImage( - key: Key(mxContent.toString()), - uri: mxContent, - fit: BoxFit.cover, - width: size, - height: size, - placeholder: (_) => textWidget, - cacheKey: mxContent.toString(), + // #Pangea + // final container = ClipRRect( + // borderRadius: borderRadius, + // child: Container( + // width: size, + // height: size, + // color: noPic + // ? name?.lightColorAvatar + // : Theme.of(context).secondaryHeaderColor, + // child: noPic + // ? textWidget + // : MxcImage( + // key: Key(mxContent.toString()), + // uri: mxContent, + // fit: BoxFit.cover, + // width: size, + // height: size, + // placeholder: (_) => textWidget, + // cacheKey: mxContent.toString(), + // ), + // ), + // ); + final container = Stack( + children: [ + ClipRRect( + borderRadius: borderRadius, + child: Container( + width: size, + height: size, + color: noPic + ? name?.lightColorAvatar + : Theme.of(context).secondaryHeaderColor, + child: noPic + ? textWidget + : MxcImage( + key: Key(mxContent.toString()), + uri: mxContent, + fit: BoxFit.cover, + width: size, + height: size, + placeholder: (_) => textWidget, + cacheKey: mxContent.toString(), + ), + ), + ), + if (littleIcon != null) + Positioned( + bottom: 0, + right: 0, + child: ClipRRect( + borderRadius: borderRadius, + child: Container( + height: 16, + width: 16, + color: Colors.white, + child: Icon( + littleIcon, + color: noPic + ? name?.lightColorAvatar + : Theme.of(context).secondaryHeaderColor, + size: 14, + ), ), - ), + ), + ), + ], ); + // Pangea# if (onTap == null) return container; return InkWell( onTap: onTap, diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index fbbb4978e..4d6abdd89 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; +import 'package:fluffychat/pangea/utils/download_chat.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.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'; @@ -16,8 +19,7 @@ class ChatSettingsPopupMenu extends StatefulWidget { final Room room; final bool displayChatDetails; - const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {Key? key}) - : super(key: key); + const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {super.key}); @override ChatSettingsPopupMenuState createState() => ChatSettingsPopupMenuState(); @@ -34,6 +36,13 @@ class ChatSettingsPopupMenuState extends State { @override Widget build(BuildContext context) { + // #Pangea + final PangeaController pangeaController = MatrixState.pangeaController; + final ClassSettingsModel? classSettings = pangeaController + .matrixState.client + .getRoomById(widget.room.id) + ?.firstLanguageSettings; + // Pangea# notificationChangeSub ??= Matrix.of(context) .client .onAccountData @@ -43,6 +52,28 @@ class ChatSettingsPopupMenuState extends State { (u) => setState(() {}), ); final items = >[ + // #Pangea + // PopupMenuItem( + // value: 'widgets', + // child: Row( + // children: [ + // const Icon(Icons.widgets_outlined), + // const SizedBox(width: 12), + // Text(L10n.of(context)!.matrixWidgets), + // ], + // ), + // ), + PopupMenuItem( + value: 'learning_settings', + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 12), + Text(L10n.of(context)!.learningSettings), + ], + ), + ), + // Pangea# widget.room.pushRuleState == PushRuleState.notify ? PopupMenuItem( value: 'mute', @@ -64,16 +95,66 @@ class ChatSettingsPopupMenuState extends State { ], ), ), + // #Pangea + // PopupMenuItem( + // value: 'todos', + // child: Row( + // children: [ + // const Icon(Icons.task_alt_outlined), + // const SizedBox(width: 12), + // Text(L10n.of(context)!.todoLists), + // ], + // ), + // ), + // Pangea# PopupMenuItem( value: 'leave', child: Row( children: [ - const Icon(Icons.delete_outlined), + // #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( + value: 'download txt', + child: Row( + children: [ + const Icon(Icons.download_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.downloadTxtFile), + ], + ), + ), + if (classSettings != null) + PopupMenuItem( + value: 'download csv', + child: Row( + children: [ + const Icon(Icons.download_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.downloadCSVFile), + ], + ), + ), + if (classSettings != null) + PopupMenuItem( + value: 'download xlsx', + child: Row( + children: [ + const Icon(Icons.download_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.downloadXLSXFile), + ], + ), + ), + // Pangea# ]; if (widget.displayChatDetails) { items.insert( @@ -112,6 +193,7 @@ class ChatSettingsPopupMenuState extends State { title: L10n.of(context)!.areYouSure, okLabel: L10n.of(context)!.ok, cancelLabel: L10n.of(context)!.cancel, + message: L10n.of(context)!.archiveRoomDescription, ); if (confirmed == OkCancelResult.ok) { final success = await showFutureLoadingDialog( @@ -137,9 +219,55 @@ class ChatSettingsPopupMenuState extends State { widget.room.setPushRuleState(PushRuleState.notify), ); break; + // #Pangea + // case 'todos': + // context.go('/rooms/${widget.room.id}/tasks'); + // break; + // Pangea# case 'details': _showChatDetails(); break; + // #Pangea + case 'download txt': + showFutureLoadingDialog( + context: context, + future: () => downloadChat( + widget.room, + classSettings!, + DownloadType.txt, + Matrix.of(context).client, + context, + ), + ); + break; + case 'download csv': + showFutureLoadingDialog( + context: context, + future: () => downloadChat( + widget.room, + classSettings!, + DownloadType.csv, + Matrix.of(context).client, + context, + ), + ); + break; + case 'download xlsx': + showFutureLoadingDialog( + context: context, + future: () => downloadChat( + widget.room, + classSettings!, + DownloadType.xlsx, + Matrix.of(context).client, + context, + ), + ); + break; + case 'learning_settings': + context.go('/rooms/settings/learning'); + break; + // #Pangea } }, itemBuilder: (BuildContext context) => items, diff --git a/lib/widgets/fluffy_chat_app.dart b/lib/widgets/fluffy_chat_app.dart index 45852dfcc..e1237fa8b 100644 --- a/lib/widgets/fluffy_chat_app.dart +++ b/lib/widgets/fluffy_chat_app.dart @@ -1,13 +1,15 @@ +import 'package:country_picker/country_picker.dart'; +import 'package:fluffychat/config/routes.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/widgets/theme_builder.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/routes.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/widgets/app_lock.dart'; -import 'package:fluffychat/widgets/theme_builder.dart'; import '../config/app_config.dart'; import '../utils/custom_scroll_behaviour.dart'; import 'matrix.dart'; @@ -18,11 +20,11 @@ class FluffyChatApp extends StatelessWidget { final String? pincode; const FluffyChatApp({ - Key? key, + super.key, this.testWidget, required this.clients, this.pincode, - }) : super(key: key); + }); /// getInitialLink may rereturn the value multiple times if this view is /// opened multiple times for example if the user logs out after they logged @@ -31,7 +33,14 @@ class FluffyChatApp extends StatelessWidget { // Router must be outside of build method so that hot reload does not reset // the current path. - static final GoRouter router = GoRouter(routes: AppRoutes.routes); + static final GoRouter router = GoRouter( + routes: AppRoutes.routes, + // #Pangea + observers: [ + GoogleAnalytics.getAnalyticsObserver(), + ], + // Pangea# + ); @override Widget build(BuildContext context) { @@ -43,7 +52,16 @@ class FluffyChatApp extends StatelessWidget { darkTheme: FluffyThemes.buildTheme(context, Brightness.dark, primaryColor), scrollBehavior: CustomScrollBehavior(), - localizationsDelegates: L10n.localizationsDelegates, + // #Pangea + // localizationsDelegates: L10n.localizationsDelegates, + localizationsDelegates: const [ + L10n.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + CountryLocalizations.delegate, + ], + // Pangea# supportedLocales: L10n.supportedLocales, routerConfig: router, builder: (context, child) => AppLockWidget( diff --git a/lib/widgets/layouts/empty_page.dart b/lib/widgets/layouts/empty_page.dart index 9f377f65b..75550d942 100644 --- a/lib/widgets/layouts/empty_page.dart +++ b/lib/widgets/layouts/empty_page.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart'; import 'package:flutter/material.dart'; class EmptyPage extends StatelessWidget { @@ -23,12 +24,15 @@ class EmptyPage extends StatelessWidget { Center( child: Hero( tag: 'info-logo', - child: Image.asset( - 'assets/favicon.png', - width: width, - height: width, - filterQuality: FilterQuality.medium, - ), + // #Pangea + // child: Image.asset( + // 'assets/favicon.png', + // width: width, + // height: width, + // filterQuality: FilterQuality.medium, + // ), + child: PangeaLogoSvg(width: width), + // Pangea# ), ), if (loading) diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index bc9e04c2d..5eaa3f86d 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -1,11 +1,9 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class LoginScaffold extends StatelessWidget { final Widget body; @@ -13,11 +11,11 @@ class LoginScaffold extends StatelessWidget { final bool enforceMobileMode; const LoginScaffold({ - Key? key, + super.key, required this.body, this.appBar, this.enforceMobileMode = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -25,84 +23,109 @@ class LoginScaffold extends StatelessWidget { enforceMobileMode || !FluffyThemes.isColumnMode(context); final scaffold = Scaffold( key: const Key('LoginScaffold'), - appBar: appBar == null - ? null - : AppBar( - titleSpacing: appBar?.titleSpacing, - automaticallyImplyLeading: - appBar?.automaticallyImplyLeading ?? true, - centerTitle: appBar?.centerTitle, - title: appBar?.title, - leading: appBar?.leading, - actions: appBar?.actions, - backgroundColor: isMobileMode ? null : Colors.transparent, - ), + // Pangea# + // appBar: appBar == null + // ? null + // : AppBar( + appBar: AppBar( + // Pangea# + titleSpacing: appBar?.titleSpacing, + automaticallyImplyLeading: appBar?.automaticallyImplyLeading ?? true, + centerTitle: appBar?.centerTitle, + title: appBar?.title, + leading: appBar?.leading, + actions: appBar?.actions, + // #Pangea + // backgroundColor: isMobileMode ? null : Colors.transparent, + backgroundColor: Colors.transparent, + // Pangea# + ), extendBodyBehindAppBar: true, extendBody: true, - body: body, - bottomNavigationBar: isMobileMode - ? Material( - elevation: 4, - shadowColor: Theme.of(context).colorScheme.onBackground, - child: const _PrivacyButtons( - mainAxisAlignment: MainAxisAlignment.center, - ), - ) - : null, - ); - if (isMobileMode) return scaffold; - return Container( - decoration: BoxDecoration( - gradient: FluffyThemes.backgroundGradient(context, 255), - ), - child: Column( - children: [ - const SizedBox(height: 16), - Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Material( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - elevation: - Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4, - shadowColor: Theme.of(context).appBarTheme.shadowColor, - child: ConstrainedBox( - constraints: isMobileMode - ? const BoxConstraints() - : const BoxConstraints(maxWidth: 960, maxHeight: 640), - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: Image.asset( - 'assets/login_wallpaper.png', - fit: BoxFit.cover, - ), - ), - Container( - width: 1, - color: Theme.of(context).dividerTheme.color, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: scaffold, - ), - ), - ], - ), - ), - ), - ), - ), + // #Pangea + // body: body, + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: AssetImage('assets/login_wallpaper.png'), ), - const _PrivacyButtons(mainAxisAlignment: MainAxisAlignment.end), - ], + ), + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: body, + ), ), + // bottomNavigationBar: isMobileMode + // ? Material( + // elevation: 4, + // shadowColor: Theme.of(context).colorScheme.onBackground, + // child: const _PrivacyButtons( + // mainAxisAlignment: MainAxisAlignment.center, + // ), + // ) + // : null, ); + + // #Pangea + return scaffold; + // if (isMobileMode) { + // return scaffold; + // } + // return Container( + // decoration: BoxDecoration( + // gradient: FluffyThemes.backgroundGradient(context, 255), + // ), + // child: Column( + // children: [ + // const SizedBox(height: 16), + // Expanded( + // child: Center( + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16.0), + // child: Material( + // color: Theme.of(context).scaffoldBackgroundColor, + // borderRadius: BorderRadius.circular(AppConfig.borderRadius), + // clipBehavior: Clip.hardEdge, + // elevation: + // Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4, + // shadowColor: Theme.of(context).appBarTheme.shadowColor, + // child: ConstrainedBox( + // constraints: isMobileMode + // ? const BoxConstraints() + // : const BoxConstraints(maxWidth: 960, maxHeight: 640), + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // Expanded( + // child: Image.asset( + // 'assets/login_wallpaper.png', + // fit: BoxFit.cover, + // ), + // ), + // Container( + // width: 1, + // color: Theme.of(context).dividerTheme.color, + // ), + // Expanded( + // child: Padding( + // padding: const EdgeInsets.all(8.0), + // child: scaffold, + // ), + // ), + // ], + // ), + // ), + // ), + // ), + // ), + // ), + // const _PrivacyButtons(mainAxisAlignment: MainAxisAlignment.end), + // ], + // ), + // ); + // Pangea# } } diff --git a/lib/widgets/layouts/max_width_body.dart b/lib/widgets/layouts/max_width_body.dart index 31f1e7bef..36e21a368 100644 --- a/lib/widgets/layouts/max_width_body.dart +++ b/lib/widgets/layouts/max_width_body.dart @@ -1,8 +1,7 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; +import 'package:flutter/material.dart'; class MaxWidthBody extends StatelessWidget { final Widget? child; diff --git a/lib/widgets/local_notifications_extension.dart b/lib/widgets/local_notifications_extension.dart index 44754030c..e326b9aa8 100644 --- a/lib/widgets/local_notifications_extension.dart +++ b/lib/widgets/local_notifications_extension.dart @@ -1,9 +1,12 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; - import 'package:desktop_lifecycle/desktop_lifecycle.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; @@ -11,11 +14,6 @@ import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_html/html.dart' as html; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - extension LocalNotificationsExtension on MatrixState { static final html.AudioElement _audioPlayer = html.AudioElement() ..src = 'assets/assets/sounds/notification.ogg' diff --git a/lib/widgets/lock_screen.dart b/lib/widgets/lock_screen.dart index 8825f6cd8..775b132b1 100644 --- a/lib/widgets/lock_screen.dart +++ b/lib/widgets/lock_screen.dart @@ -1,13 +1,11 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/theme_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class LockScreen extends StatefulWidget { const LockScreen({super.key}); diff --git a/lib/widgets/log_view.dart b/lib/widgets/log_view.dart index 4c58f8b4b..43e5b6c38 100644 --- a/lib/widgets/log_view.dart +++ b/lib/widgets/log_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 2c26de59a..61d13d5ed 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -2,12 +2,19 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/uia_request_manager.dart'; +import 'package:fluffychat/utils/voip_plugin.dart'; +import 'package:fluffychat/widgets/fluffy_chat_app.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:http/http.dart' as http; @@ -18,12 +25,6 @@ import 'package:provider/provider.dart'; import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/utils/client_manager.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/uia_request_manager.dart'; -import 'package:fluffychat/utils/voip_plugin.dart'; -import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import '../config/app_config.dart'; import '../config/setting_keys.dart'; import '../pages/key_verification/key_verification_dialog.dart'; @@ -45,8 +46,8 @@ class Matrix extends StatefulWidget { this.child, required this.clients, this.queryParameters, - Key? key, - }) : super(key: key); + super.key, + }); @override MatrixState createState() => MatrixState(); @@ -60,6 +61,10 @@ class MatrixState extends State with WidgetsBindingObserver { int _activeClient = -1; String? activeBundle; Store store = Store(); + // #Pangea + static late PangeaController pangeaController; + static PangeaAnyState pAnyState = PangeaAnyState(); + // Pangea# HomeserverSummary? loginHomeserverSummary; XFile? loginAvatar; @@ -249,6 +254,10 @@ class MatrixState extends State with WidgetsBindingObserver { initSettings(); } initLoadingDialog(); + // #Pangea + pangeaController = PangeaController(matrix: widget, matrixState: this); + // PAuthGaurd.isLogged = client.isLogged(); + // Pangea# } void initLoadingDialog() { @@ -309,8 +318,12 @@ class MatrixState extends State with WidgetsBindingObserver { hidPopup = true; await KeyVerificationDialog(request: request).show(context); }); - onLoginStateChanged[name] ??= c.onLoginStateChanged.stream.listen((state) { + onLoginStateChanged[name] ??= + c.onLoginStateChanged.stream.listen((state) async { final loggedInWithMultipleClients = widget.clients.length > 1; + // #Pangea + // PAuthGaurd.isLogged = state == LoginState.loggedIn; + // Pangea# if (loggedInWithMultipleClients && state != LoginState.loggedIn) { _cancelSubs(c.clientName); widget.clients.remove(c); @@ -325,8 +338,23 @@ class MatrixState extends State with WidgetsBindingObserver { FluffyChatApp.router.go('/rooms'); } } else { - FluffyChatApp.router - .go(state == LoginState.loggedIn ? '/rooms' : '/home'); + // #Pangea + if (state == LoginState.loggedIn) { + await (await pangeaController.userController.completer).future; + } + String routeDestination; + if (state == LoginState.loggedIn) { + routeDestination = await pangeaController + .userController.isUserDataAvailableAndDateOfBirthSet + ? '/rooms' + : "/rooms/user_age"; + } else { + routeDestination = '/home'; + } + FluffyChatApp.router.go(routeDestination); + // FluffyChatApp.router + // .go(state == LoginState.loggedIn ? '/rooms' : '/home'); + // Pangea# } }); onUiaRequest[name] ??= c.onUiaRequest.stream.listen(uiaRequestHandler); @@ -376,14 +404,19 @@ class MatrixState extends State with WidgetsBindingObserver { final result = await showOkCancelAlertDialog( barrierDismissible: true, context: context, - title: L10n.of(context)!.oopsSomethingWentWrong, + title: L10n.of(context)!.pushNotificationsNotAvailable, message: errorMsg, - okLabel: - link == null ? L10n.of(context)!.ok : L10n.of(context)!.help, + fullyCapitalizedForMaterial: false, + okLabel: link == null + ? L10n.of(context)!.ok + : L10n.of(context)!.learnMore, cancelLabel: L10n.of(context)!.doNotShowAgain, ); if (result == OkCancelResult.ok && link != null) { - launchUrlString(link.toString()); + launchUrlString( + link.toString(), + mode: LaunchMode.externalApplication, + ); } if (result == OkCancelResult.cancel) { await store.setItemBool(SettingKeys.showNoGoogle, true); @@ -437,12 +470,6 @@ class MatrixState extends State with WidgetsBindingObserver { store .getItemBool(SettingKeys.hideUnknownEvents, AppConfig.hideUnknownEvents) .then((value) => AppConfig.hideUnknownEvents = value); - store - .getItemBool( - SettingKeys.showDirectChatsInSpaces, - AppConfig.showDirectChatsInSpaces, - ) - .then((value) => AppConfig.showDirectChatsInSpaces = value); store .getItemBool(SettingKeys.separateChatTypes, AppConfig.separateChatTypes) .then((value) => AppConfig.separateChatTypes = value); diff --git a/lib/widgets/permission_slider_dialog.dart b/lib/widgets/permission_slider_dialog.dart index 8f8958982..b67729978 100644 --- a/lib/widgets/permission_slider_dialog.dart +++ b/lib/widgets/permission_slider_dialog.dart @@ -1,7 +1,6 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; enum PermissionLevel { diff --git a/lib/widgets/profile_bottom_sheet.dart b/lib/widgets/profile_bottom_sheet.dart new file mode 100644 index 000000000..2fe882447 --- /dev/null +++ b/lib/widgets/profile_bottom_sheet.dart @@ -0,0 +1,99 @@ +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +class ProfileBottomSheet extends StatelessWidget { + final String userId; + final BuildContext outerContext; + + const ProfileBottomSheet({ + required this.userId, + required this.outerContext, + Key? key, + }) : super(key: key); + + void _startDirectChat(BuildContext context) async { + final client = Matrix.of(context).client; + final result = await showFutureLoadingDialog( + context: context, + //#Pangea + // future: () => client.startDirectChat(userId), + future: () => client.startDirectChat(userId, enableEncryption: false), + //Pangea# + ); + if (result.error == null) { + context.go('/rooms/${result.result!}'); + Navigator.of(context, rootNavigator: false).pop(); + return; + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: FutureBuilder( + future: Matrix.of(context).client.getProfileFromUserId(userId), + builder: (context, snapshot) { + final profile = snapshot.data; + return Scaffold( + appBar: AppBar( + leading: CloseButton( + onPressed: Navigator.of(context, rootNavigator: false).pop, + ), + title: ListTile( + contentPadding: const EdgeInsets.only(right: 16.0), + title: Text( + profile?.displayName ?? userId.localpart ?? userId, + style: const TextStyle(fontSize: 18), + ), + subtitle: Text( + userId, + style: const TextStyle(fontSize: 12), + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: OutlinedButton.icon( + onPressed: () => _startDirectChat(context), + icon: Icon(Icons.adaptive.share_outlined), + label: Text(L10n.of(context)!.share), + ), + ), + ], + ), + body: ListView( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Avatar( + mxContent: profile?.avatarUrl, + name: profile?.displayName ?? userId, + size: Avatar.defaultSize * 3, + fontSize: 36, + ), + ), + ), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + child: FloatingActionButton.extended( + onPressed: () => _startDirectChat(context), + label: Text(L10n.of(context)!.newChat), + icon: const Icon(Icons.send_outlined), + ), + ), + const SizedBox(height: 8), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index 87ee50256..b49b2e6d2 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -1,14 +1,13 @@ +import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/utils/url_launcher.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../utils/localized_exception_extension.dart'; class PublicRoomBottomSheet extends StatelessWidget { diff --git a/lib/widgets/unread_rooms_badge.dart b/lib/widgets/unread_rooms_badge.dart index 2110e4e33..53582391c 100644 --- a/lib/widgets/unread_rooms_badge.dart +++ b/lib/widgets/unread_rooms_badge.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:badges/badges.dart' as b; +import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'matrix.dart'; @@ -11,11 +10,11 @@ class UnreadRoomsBadge extends StatelessWidget { final Widget? child; const UnreadRoomsBadge({ - Key? key, + super.key, required this.filter, this.badgePosition, this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -26,12 +25,22 @@ class UnreadRoomsBadge extends StatelessWidget { .stream .where((syncUpdate) => syncUpdate.hasRoomUpdate), builder: (context, _) { - final unreadCount = Matrix.of(context) + // #Pangea + // final unreadCount = Matrix.of(context) + // .client + // .rooms + // .where(filter) + // .where((r) => (r.isUnread || r.membership == Membership.invite)) + // .length; + final unreadCounts = Matrix.of(context) .client .rooms .where(filter) .where((r) => (r.isUnread || r.membership == Membership.invite)) - .length; + .map((r) => r.notificationCount); + final unreadCount = + unreadCounts.isEmpty ? 0 : unreadCounts.reduce((a, b) => a + b); + // Pangea# return b.Badge( badgeStyle: b.BadgeStyle( badgeColor: Theme.of(context).colorScheme.primary, diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 258591b6b..5b46b2157 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -86,7 +86,6 @@ set_target_properties(${BINARY_NAME} RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) - # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 6af12d051..eb3c11cf1 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); + g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); + sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d24b1b225..044576a2d 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_webrtc pasteboard record_linux + sentry_flutter url_launcher_linux window_to_front ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e47880af4..16eea7f4d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,25 +6,32 @@ import FlutterMacOS import Foundation import audio_session +import connectivity_plus import desktop_drop import desktop_lifecycle import device_info_plus import dynamic_color import emoji_picker_flutter import file_selector_macos +import firebase_analytics +import firebase_core +import firebase_messaging import flutter_app_badger import flutter_local_notifications import flutter_secure_storage_macos import flutter_web_auth_2 import flutter_webrtc import geolocator_apple +import in_app_purchase_storekit import just_audio import macos_ui import macos_window_utils import package_info_plus import pasteboard import path_provider_foundation +import purchases_flutter import record_macos +import sentry_flutter import share_plus import shared_preferences_foundation import sqflite @@ -35,25 +42,32 @@ import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DesktopLifecyclePlugin.register(with: registry.registrar(forPlugin: "DesktopLifecyclePlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin")) RecordMacosPlugin.register(with: registry.registrar(forPlugin: "RecordMacosPlugin")) + SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index eba1335b9..0eaf115df 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1 +1 @@ -{"images":[{"size":"1024x1024","filename":"1024-mac.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file +{"images":[{"size":"1024x1024","filename":"1024-mac.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..fb1434bfe Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..34d07d5ea Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..e78ba9054 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..f7508e359 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..6154ba14c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..6245280ee Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..7de4d1f3e Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/needed-translations.txt b/needed-translations.txt new file mode 100644 index 000000000..fd4622756 --- /dev/null +++ b/needed-translations.txt @@ -0,0 +1,43489 @@ +{ + "ar": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "bn": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "bo": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "about", + "updateAvailable", + "updateNow", + "accept", + "acceptedTheInvitation", + "account", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ca": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "badServerVersionsException", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_unban", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "editRoomAliases", + "editRoomAvatar", + "emoteKeyboardNoRecents", + "homeserver", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inoffensive", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loginWithOneClick", + "messagesStyle", + "noMatrixServer", + "shareInviteLink", + "offensive", + "oopsPushError", + "openVideoCamera", + "oneClientLoggedOut", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "serverRequiresEmail", + "participant", + "pleaseChoose", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPin", + "redactedBy", + "directChat", + "redactedByBecause", + "register", + "removeYourAvatar", + "replaceRoomWithNewerVersion", + "recoveryKey", + "recoveryKeyLost", + "separateChatTypes", + "setChatDescription", + "showDirectChatsInSpaces", + "startFirstChat", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "cs": [ + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "emoteKeyboardNoRecents", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "hasKnocked", + "callingAccount", + "appearOnTopDetails", + "noKeyForThisMessage", + "enterSpace", + "enterRoom", + "hideUnimportantStateEvents", + "noBackupWarning", + "readUpToHere", + "reportErrorDescription", + "report", + "invite", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "de": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "emoteKeyboardNoRecents", + "hasKnocked", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "el": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "about", + "updateAvailable", + "updateNow", + "accept", + "acceptedTheInvitation", + "account", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "eo": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "bubbleSize", + "yourChatBackupHasBeenSetUp", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "loginWithOneClick", + "messagesStyle", + "shareInviteLink", + "scanQrCode", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "pleaseEnterRecoveryKey", + "redactedBy", + "directChat", + "redactedByBecause", + "recoveryKey", + "recoveryKeyLost", + "separateChatTypes", + "setChatDescription", + "showDirectChatsInSpaces", + "startFirstChat", + "unverified", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "es": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "savedEmotePack", + "sendTypingNotifications", + "createGroup", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "et": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "eu": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "fa": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "sendTypingNotifications", + "classes", + "createGroup", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "fi": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "sendTypingNotifications", + "classes", + "createGroup", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "fr": [ + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "sendTypingNotifications", + "classes", + "emoteKeyboardNoRecents", + "chatDescription", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ga": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_discardsession", + "commandHint_dm", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loginWithOneClick", + "messagesStyle", + "shareInviteLink", + "oneClientLoggedOut", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "pleaseEnterRecoveryKey", + "redactedBy", + "directChat", + "redactedByBecause", + "recoveryKey", + "recoveryKeyLost", + "separateChatTypes", + "setChatDescription", + "showDirectChatsInSpaces", + "startFirstChat", + "unverified", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "experimentalVideoCalls", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "gl": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "he": [ + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "enableEmotesGlobally", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "messagesStyle", + "noEmotesFound", + "shareInviteLink", + "ok", + "pleaseEnterRecoveryKey", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "hi": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "about", + "updateAvailable", + "updateNow", + "accept", + "acceptedTheInvitation", + "account", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "hr": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "hu": [ + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "hugContent", + "sendTypingNotifications", + "classes", + "commandHint_markasdm", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "hydrateTorLong", + "messagesStyle", + "shareInviteLink", + "oneClientLoggedOut", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "serverRequiresEmail", + "pleaseChooseAPasscode", + "pleaseClickOnLink", + "pleaseEnterRecoveryKey", + "pleaseFollowInstructionsOnWeb", + "pushRules", + "redactedBy", + "directChat", + "redactedByBecause", + "removeYourAvatar", + "replaceRoomWithNewerVersion", + "recoveryKey", + "recoveryKeyLost", + "sendAsText", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setPermissionsLevel", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "singlesignon", + "spaceIsPublic", + "spaceName", + "startFirstChat", + "status", + "synchronizingPleaseWait", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "unverified", + "verified", + "whoCanPerformWhichAction", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "id": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ie": [ + "passwordsDoNotMatch", + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "activatedEndToEndEncryption", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "appLock", + "areGuestsAllowedToJoin", + "askSSSSSign", + "sendTypingNotifications", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changeTheNameOfTheGroup", + "channelCorruptedDecryptError", + "yourChatBackupHasBeenSetUp", + "chatBackupDescription", + "chatHasBeenAddedToThisSpace", + "classes", + "chooseAStrongPassword", + "commandHint_markasdm", + "commandHint_ban", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_react", + "commandHint_unban", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "contactHasBeenInvitedToTheGroup", + "contentHasBeenReported", + "couldNotDecryptMessage", + "createdTheChat", + "createGroup", + "deactivateAccountWarning", + "defaultPermissionLevel", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "chatPermissions", + "editChatPermissions", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteWarnNeedToPick", + "enableEmotesGlobally", + "enableEncryptionWarning", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "errorObtainingLocation", + "goToTheNewRoom", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupDescriptionHasBeenChanged", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "hideRedactedEvents", + "howOffensiveIsThisContent", + "ignoreListDescription", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "inviteText", + "joinedTheChat", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastSeenLongTimeAgo", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "logInTo", + "loginWithOneClick", + "makeSureTheIdentifierIsValid", + "messagesStyle", + "messageWillBeRemovedWarning", + "needPantalaimonWarning", + "newMessageInFluffyChat", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "noPasswordRecoveryDescription", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openVideoCamera", + "oneClientLoggedOut", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "serverRequiresEmail", + "passphraseOrKey", + "passwordHasBeenChanged", + "pickImage", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "rejectedTheInvitation", + "removeAllOtherDevices", + "removedBy", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "roomHasBeenUpgraded", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "sendAMessage", + "sendAsText", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "sharedTheLocation", + "showDirectChatsInSpaces", + "singlesignon", + "spaceIsPublic", + "startedACall", + "startFirstChat", + "statusExampleMessage", + "synchronizingPleaseWait", + "theyDontMatch", + "toggleFavorite", + "toggleMuted", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unbannedUser", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "userSentUnknownEvent", + "verifySuccess", + "verifyTitle", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "removeFromSpace", + "addToSpaceDescription", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "whoCanSeeMyStories", + "thisUserHasNotPostedAnythingYet", + "replyHasBeenSent", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "storyPrivacyWarning", + "markAsRead", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "placeCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "foregroundServiceRunning", + "screenSharingDetail", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "it": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "sendOnEnter", + "bubbleSize", + "chatHasBeenAddedToThisSpace", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_react", + "commandHint_unban", + "commandMissing", + "createGroup", + "createNewSpace", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "enterASpacepName", + "homeserver", + "errorObtainingLocation", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationPermissionDeniedNotice", + "loginWithOneClick", + "messagesStyle", + "noMatrixServer", + "shareInviteLink", + "obtainingLocation", + "openVideoCamera", + "oneClientLoggedOut", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "pleaseEnterRecoveryKey", + "redactedBy", + "directChat", + "redactedByBecause", + "recoveryKey", + "recoveryKeyLost", + "separateChatTypes", + "setChatDescription", + "showDirectChatsInSpaces", + "spaceIsPublic", + "spaceName", + "startFirstChat", + "synchronizingPleaseWait", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ja": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_cuddle", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "classes", + "commandHint_kick", + "commandHint_me", + "commandHint_op", + "commandHint_unban", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "openInMaps", + "redactedBy", + "directChat", + "redactedByBecause", + "separateChatTypes", + "setChatDescription", + "whoCanSeeMyStories", + "whatIsGoingOn", + "dismiss", + "indexedDbErrorLong", + "widgetEtherpad", + "hasKnocked", + "saveKeyManuallyDescription", + "callingAccountDetails", + "appearOnTop", + "noKeyForThisMessage", + "newSpaceDescription", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ko": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_cuddle", + "commandHint_hug", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "classes", + "commandHint_markasdm", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "messagesStyle", + "shareInviteLink", + "pleaseEnterRecoveryKey", + "redactedBy", + "directChat", + "redactedByBecause", + "recoveryKey", + "recoveryKeyLost", + "separateChatTypes", + "setChatDescription", + "showDirectChatsInSpaces", + "startFirstChat", + "pleaseEnterRecoveryKeyDescription", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "lt": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "classes", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "startFirstChat", + "hasKnocked", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "lv": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "about", + "updateAvailable", + "updateNow", + "accept", + "acceptedTheInvitation", + "account", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "nb": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "bubbleSize", + "yourChatBackupHasBeenSetUp", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "clearArchive", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "createGroup", + "createNewSpace", + "allRooms", + "discover", + "chatPermissions", + "editRoomAliases", + "emoteKeyboardNoRecents", + "enterASpacepName", + "homeserver", + "errorObtainingLocation", + "goToTheNewRoom", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "loginWithOneClick", + "messagesStyle", + "noEncryptionForPublicRooms", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "obtainingLocation", + "oopsPushError", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "or", + "people", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseEnter4Digits", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPin", + "redactedBy", + "directChat", + "redactedByBecause", + "redactMessage", + "register", + "removeYourAvatar", + "roomVersion", + "saveFile", + "recoveryKey", + "recoveryKeyLost", + "sendAsText", + "sendSticker", + "separateChatTypes", + "setAsCanonicalAlias", + "setChatDescription", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "singlesignon", + "spaceIsPublic", + "spaceName", + "startFirstChat", + "synchronizingPleaseWait", + "unverified", + "verified", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "wipeChatBackup", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "nl": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "pl": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "sendTypingNotifications", + "classes", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "pt": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accept", + "acceptedTheInvitation", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "deactivateAccountWarning", + "defaultPermissionLevel", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "logInTo", + "loginWithOneClick", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "publicRooms", + "pushRules", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "pt_BR": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "createGroup", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "pt_PT": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "fromJoining", + "fromTheInvitation", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "scanQrCode", + "openVideoCamera", + "pleaseEnterRecoveryKey", + "pushRules", + "redactedBy", + "directChat", + "redactedByBecause", + "recoveryKey", + "recoveryKeyLost", + "seenByUserAndCountOthers", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ro": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "sendTypingNotifications", + "classes", + "createGroup", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ru": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "sk": [ + "notAnImage", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "autoplayImages", + "sendTypingNotifications", + "blocked", + "botMessages", + "bubbleSize", + "changeYourAvatar", + "chatBackupDescription", + "chatHasBeenAddedToThisSpace", + "classes", + "clearArchive", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "configureChat", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copyToClipboard", + "createGroup", + "createNewSpace", + "deactivateAccountWarning", + "defaultPermissionLevel", + "deleteAccount", + "deviceId", + "directChats", + "allRooms", + "discover", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editRoomAliases", + "editRoomAvatar", + "emoteKeyboardNoRecents", + "emotePacks", + "enableEmotesGlobally", + "enableEncryption", + "encrypted", + "endedTheCall", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fontSize", + "goToTheNewRoom", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groups", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "inoffensive", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "inviteForMe", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "loginWithOneClick", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "newChat", + "next", + "no", + "noConnectionToTheServer", + "noEncryptionForPublicRooms", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "noPasswordRecoveryDescription", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "online", + "oopsPushError", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "or", + "participant", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "pin", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPin", + "pleaseFollowInstructionsOnWeb", + "privacy", + "pushRules", + "reason", + "redactedBy", + "directChat", + "redactedByBecause", + "redactMessage", + "register", + "removeYourAvatar", + "replaceRoomWithNewerVersion", + "saveFile", + "recoveryKey", + "recoveryKeyLost", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "singlesignon", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "synchronizingPleaseWait", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "unavailable", + "unpin", + "unverified", + "verified", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "sl": [ + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "bubbleSize", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "createGroup", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "sr": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "addToSpace", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "autoplayImages", + "sendTypingNotifications", + "sendOnEnter", + "bubbleSize", + "cantOpenUri", + "yourChatBackupHasBeenSetUp", + "chatHasBeenAddedToThisSpace", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_myroomavatar", + "commandInvalid", + "commandMissing", + "createGroup", + "createNewSpace", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "enterASpacepName", + "homeserver", + "errorObtainingLocation", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "loginWithOneClick", + "messagesStyle", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "obtainingLocation", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "pleaseEnterRecoveryKey", + "redactedBy", + "directChat", + "redactedByBecause", + "saveFile", + "recoveryKey", + "recoveryKeyLost", + "sendAsText", + "sendSticker", + "separateChatTypes", + "setChatDescription", + "shareLocation", + "showDirectChatsInSpaces", + "spaceIsPublic", + "spaceName", + "startFirstChat", + "synchronizingPleaseWait", + "unverified", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "sv": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "sendTypingNotifications", + "classes", + "createGroup", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "messagesStyle", + "shareInviteLink", + "redactedBy", + "directChat", + "redactedByBecause", + "setChatDescription", + "hasKnocked", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "ta": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "account", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "th": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "about", + "updateAvailable", + "updateNow", + "accept", + "acceptedTheInvitation", + "account", + "accountInformation", + "activatedEndToEndEncryption", + "addEmail", + "confirmMatrixId", + "supposedMxid", + "addGroupDescription", + "addNewFriend", + "addToSpace", + "admin", + "alias", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "answeredTheCall", + "anyoneCanJoin", + "appLock", + "archive", + "areGuestsAllowedToJoin", + "areYouSure", + "areYouSureYouWantToLogout", + "askSSSSSign", + "askVerificationRequest", + "autoplayImages", + "badServerLoginTypesException", + "sendTypingNotifications", + "sendOnEnter", + "badServerVersionsException", + "banFromChat", + "banned", + "bannedUser", + "blockDevice", + "blocked", + "botMessages", + "bubbleSize", + "cancel", + "cantOpenUri", + "changeDeviceName", + "changedTheChatAvatar", + "changedTheChatDescriptionTo", + "changedTheChatNameTo", + "changedTheChatPermissions", + "changedTheDisplaynameTo", + "changedTheGuestAccessRules", + "changedTheGuestAccessRulesTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changedTheProfileAvatar", + "changedTheRoomAliases", + "changedTheRoomInvitationLink", + "changePassword", + "changeTheHomeserver", + "changeTheme", + "changeTheNameOfTheGroup", + "changeWallpaper", + "changeYourAvatar", + "channelCorruptedDecryptError", + "chat", + "yourChatBackupHasBeenSetUp", + "chatBackup", + "chatBackupDescription", + "chatDetails", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "chooseAStrongPassword", + "chooseAUsername", + "clearArchive", + "close", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "compareEmojiMatch", + "compareNumbersMatch", + "configureChat", + "confirm", + "connect", + "contactHasBeenInvitedToTheGroup", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copy", + "copyToClipboard", + "couldNotDecryptMessage", + "countParticipants", + "create", + "createdTheChat", + "createGroup", + "createNewSpace", + "createNewGroup", + "currentlyActive", + "darkTheme", + "dateAndTimeOfDay", + "dateWithoutYear", + "dateWithYear", + "deactivateAccountWarning", + "defaultPermissionLevel", + "delete", + "deleteAccount", + "deleteMessage", + "deny", + "device", + "deviceId", + "devices", + "directChats", + "allRooms", + "discover", + "displaynameHasBeenChanged", + "downloadFile", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editDisplayname", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteSettings", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "everythingReady", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "next", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noEncryptionForPublicRooms", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "pleaseFollowInstructionsOnWeb", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "showPassword", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "transferFromAnotherDevice", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "tr": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "uk": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "vi": [ + "passwordsDoNotMatch", + "pleaseEnterValidEmail", + "repeatPassword", + "pleaseChooseAtLeastChars", + "notAnImage", + "remove", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "addToSpace", + "all", + "allChats", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "appLock", + "askSSSSSign", + "autoplayImages", + "sendTypingNotifications", + "sendOnEnter", + "botMessages", + "bubbleSize", + "cantOpenUri", + "changedTheDisplaynameTo", + "changedTheHistoryVisibility", + "changedTheHistoryVisibilityTo", + "changedTheJoinRules", + "changedTheJoinRulesTo", + "changeTheme", + "changeYourAvatar", + "channelCorruptedDecryptError", + "yourChatBackupHasBeenSetUp", + "chatHasBeenAddedToThisSpace", + "chats", + "classes", + "clearArchive", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_ban", + "commandHint_clearcache", + "commandHint_create", + "commandHint_discardsession", + "commandHint_dm", + "commandHint_html", + "commandHint_invite", + "commandHint_join", + "commandHint_kick", + "commandHint_leave", + "commandHint_me", + "commandHint_myroomavatar", + "commandHint_myroomnick", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandHint_unban", + "commandInvalid", + "commandMissing", + "configureChat", + "containsDisplayName", + "containsUserName", + "contentHasBeenReported", + "copiedToClipboard", + "copyToClipboard", + "createGroup", + "createNewSpace", + "darkTheme", + "defaultPermissionLevel", + "directChats", + "allRooms", + "discover", + "edit", + "editBlockedServers", + "chatPermissions", + "editChatPermissions", + "editRoomAliases", + "editRoomAvatar", + "emoteExists", + "emoteInvalid", + "emoteKeyboardNoRecents", + "emotePacks", + "emoteShortcode", + "emoteWarnNeedToPick", + "emptyChat", + "enableEmotesGlobally", + "enableEncryption", + "enableEncryptionWarning", + "encrypted", + "encryption", + "encryptionNotEnabled", + "endedTheCall", + "enterAGroupName", + "enterAnEmailAddress", + "enterASpacepName", + "homeserver", + "enterYourHomeserver", + "errorObtainingLocation", + "extremeOffensive", + "fileName", + "fluffychat", + "fontSize", + "forward", + "fromJoining", + "fromTheInvitation", + "goToTheNewRoom", + "group", + "chatDescription", + "chatDescriptionHasBeenChanged", + "groupIsPublic", + "groupDescription", + "groupDescriptionHasBeenChanged", + "groups", + "groupWith", + "guestsAreForbidden", + "guestsCanJoin", + "hasWithdrawnTheInvitationFor", + "help", + "hideRedactedEvents", + "hideUnknownEvents", + "howOffensiveIsThisContent", + "id", + "identity", + "ignore", + "ignoredUsers", + "ignoreListDescription", + "ignoreUsername", + "iHaveClickedOnLink", + "incorrectPassphraseOrKey", + "inoffensive", + "inviteContact", + "inviteContactToGroupQuestion", + "inviteContactToGroup", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "invited", + "redactMessageDescription", + "optionalRedactReason", + "invitedUser", + "invitedUsersOnly", + "inviteForMe", + "inviteText", + "isTyping", + "joinedTheChat", + "joinRoom", + "kicked", + "kickedAndBanned", + "kickFromChat", + "lastActiveAgo", + "lastSeenLongTimeAgo", + "leave", + "leftTheChat", + "license", + "lightTheme", + "loadCountMoreParticipants", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "loadingPleaseWait", + "loadMore", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "login", + "logInTo", + "loginWithOneClick", + "logout", + "makeSureTheIdentifierIsValid", + "memberChanges", + "mention", + "messages", + "messagesStyle", + "messageWillBeRemovedWarning", + "moderator", + "muteChat", + "needPantalaimonWarning", + "newChat", + "newMessageInFluffyChat", + "newVerificationRequest", + "no", + "noConnectionToTheServer", + "noEmotesFound", + "noGoogleServicesWarning", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "none", + "noPasswordRecoveryDescription", + "noPermission", + "noRoomsFound", + "notifications", + "notificationsEnabledForThisAccount", + "numUsersTyping", + "obtainingLocation", + "offensive", + "offline", + "ok", + "online", + "onlineKeyBackupEnabled", + "oopsPushError", + "oopsSomethingWentWrong", + "openAppToReadMessages", + "openCamera", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "optionalGroupName", + "or", + "participant", + "passphraseOrKey", + "password", + "passwordForgotten", + "passwordHasBeenChanged", + "passwordRecovery", + "people", + "pickImage", + "pin", + "play", + "pleaseChoose", + "pleaseChooseAPasscode", + "pleaseChooseAUsername", + "pleaseClickOnLink", + "pleaseEnter4Digits", + "pleaseEnterAMatrixIdentifier", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPassword", + "pleaseEnterYourPin", + "pleaseEnterYourUsername", + "privacy", + "publicRooms", + "pushRules", + "reason", + "recording", + "redactedBy", + "directChat", + "redactedByBecause", + "redactedAnEvent", + "redactMessage", + "register", + "reject", + "rejectedTheInvitation", + "rejoin", + "removeAllOtherDevices", + "removedBy", + "removeDevice", + "unbanFromChat", + "removeYourAvatar", + "renderRichContent", + "replaceRoomWithNewerVersion", + "reply", + "reportMessage", + "requestPermission", + "roomHasBeenUpgraded", + "roomVersion", + "saveFile", + "search", + "security", + "recoveryKey", + "recoveryKeyLost", + "seenByUser", + "seenByUserAndCountOthers", + "seenByUserAndUser", + "send", + "sendAMessage", + "sendAsText", + "sendAudio", + "sendFile", + "sendImage", + "sendMessages", + "sendOriginal", + "sendSticker", + "sendVideo", + "sentAFile", + "sentAnAudio", + "sentAPicture", + "sentASticker", + "sentAVideo", + "sentCallInformations", + "separateChatTypes", + "setAsCanonicalAlias", + "setCustomEmotes", + "setChatDescription", + "setInvitationLink", + "setPermissionsLevel", + "setStatus", + "settings", + "share", + "sharedTheLocation", + "shareLocation", + "showDirectChatsInSpaces", + "signUp", + "singlesignon", + "skip", + "sourceCode", + "spaceIsPublic", + "spaceName", + "startedACall", + "startFirstChat", + "status", + "statusExampleMessage", + "submit", + "synchronizingPleaseWait", + "systemTheme", + "theyDontMatch", + "theyMatch", + "title", + "toggleFavorite", + "toggleMuted", + "toggleUnread", + "tooManyRequestsWarning", + "tryToSendAgain", + "unavailable", + "unbannedUser", + "unblockDevice", + "unknownDevice", + "unknownEncryptionAlgorithm", + "unknownEvent", + "unmuteChat", + "unpin", + "unreadChats", + "userAndOthersAreTyping", + "userAndUserAreTyping", + "userIsTyping", + "userLeftTheChat", + "username", + "userSentUnknownEvent", + "unverified", + "verify", + "verifyStart", + "verifySuccess", + "verifyTitle", + "videoCall", + "visibilityOfTheChatHistory", + "visibleForAllParticipants", + "visibleForEveryone", + "voiceMessage", + "waitingPartnerAcceptRequest", + "waitingPartnerEmoji", + "waitingPartnerNumbers", + "wallpaper", + "warning", + "weSentYouAnEmail", + "whoCanPerformWhichAction", + "whoIsAllowedToJoinThisGroup", + "whyDoYouWantToReportThis", + "wipeChatBackup", + "withTheseAddressesRecoveryDescription", + "writeAMessage", + "yes", + "you", + "youAreInvitedToThisChat", + "youAreNoLongerParticipatingInThisChat", + "youCannotInviteYourself", + "youHaveBeenBannedFromThisChat", + "yourPublicKey", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "zh": [ + "accountInformation", + "addNewFriend", + "alreadyHaveAnAccount", + "classes", + "hasKnocked", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ], + + "zh_Hant": [ + "notAnImage", + "importNow", + "importEmojis", + "importFromZipFile", + "importZipFile", + "exportEmotePack", + "replace", + "savedEmotePack", + "updateAvailable", + "updateNow", + "accountInformation", + "confirmMatrixId", + "supposedMxid", + "addNewFriend", + "alreadyHaveAnAccount", + "commandHint_googly", + "commandHint_cuddle", + "commandHint_hug", + "googlyEyesContent", + "cuddleContent", + "hugContent", + "sendTypingNotifications", + "bubbleSize", + "classes", + "commandHint_markasdm", + "commandHint_markasgroup", + "commandHint_html", + "commandHint_me", + "commandHint_op", + "commandHint_plain", + "commandHint_react", + "commandHint_send", + "commandInvalid", + "commandMissing", + "createGroup", + "createNewSpace", + "allRooms", + "discover", + "chatPermissions", + "emoteKeyboardNoRecents", + "enterASpacepName", + "homeserver", + "errorObtainingLocation", + "chatDescription", + "chatDescriptionHasBeenChanged", + "inviteContactToGroupQuestion", + "noChatDescriptionYet", + "anyoneCanKnock", + "noOneCanJoin", + "tryAgain", + "invalidServerName", + "redactMessageDescription", + "optionalRedactReason", + "dehydrate", + "dehydrateWarning", + "dehydrateTor", + "dehydrateTorLong", + "hydrateTor", + "hydrateTorLong", + "hydrate", + "locationDisabledNotice", + "locationPermissionDeniedNotice", + "loginWithOneClick", + "messagesStyle", + "noMatrixServer", + "shareInviteLink", + "scanQrCode", + "obtainingLocation", + "oopsPushError", + "openVideoCamera", + "oneClientLoggedOut", + "addAccount", + "editBundlesForAccount", + "addToBundle", + "removeFromBundle", + "bundleName", + "enableMultiAccounts", + "openInMaps", + "link", + "serverRequiresEmail", + "or", + "pleaseChoose", + "pleaseEnterRecoveryKey", + "pleaseEnterYourPin", + "redactedBy", + "directChat", + "redactedByBecause", + "register", + "removeYourAvatar", + "saveFile", + "recoveryKey", + "recoveryKeyLost", + "sendAsText", + "sendSticker", + "separateChatTypes", + "setAsCanonicalAlias", + "setChatDescription", + "shareLocation", + "showDirectChatsInSpaces", + "singlesignon", + "spaceIsPublic", + "spaceName", + "startFirstChat", + "synchronizingPleaseWait", + "unverified", + "messageInfo", + "time", + "messageType", + "sender", + "openGallery", + "removeFromSpace", + "addToSpaceDescription", + "start", + "pleaseEnterRecoveryKeyDescription", + "addToStory", + "publish", + "whoCanSeeMyStories", + "unsubscribeStories", + "thisUserHasNotPostedAnythingYet", + "yourStory", + "replyHasBeenSent", + "videoWithSize", + "storyFrom", + "whoCanSeeMyStoriesDesc", + "whatIsGoingOn", + "addDescription", + "storyPrivacyWarning", + "iUnderstand", + "openChat", + "markAsRead", + "reportUser", + "dismiss", + "matrixWidgets", + "reactedWith", + "pinMessage", + "confirmEventUnpin", + "emojis", + "placeCall", + "voiceCall", + "unsupportedAndroidVersion", + "unsupportedAndroidVersionLong", + "videoCallsBetaWarning", + "experimentalVideoCalls", + "emailOrUsername", + "indexedDbErrorTitle", + "indexedDbErrorLong", + "switchToAccount", + "nextAccount", + "previousAccount", + "editWidgets", + "addWidget", + "widgetVideo", + "widgetEtherpad", + "widgetJitsi", + "widgetCustom", + "widgetName", + "widgetUrlError", + "widgetNameError", + "errorAddingWidget", + "youRejectedTheInvitation", + "youJoinedTheChat", + "youAcceptedTheInvitation", + "youBannedUser", + "youHaveWithdrawnTheInvitationFor", + "youInvitedBy", + "youInvitedUser", + "youKicked", + "youKickedAndBanned", + "youUnbannedUser", + "hasKnocked", + "noEmailWarning", + "stories", + "users", + "unlockOldMessages", + "storeInSecureStorageDescription", + "saveKeyManuallyDescription", + "storeInAndroidKeystore", + "storeInAppleKeyChain", + "storeSecurlyOnThisDevice", + "countFiles", + "user", + "custom", + "foregroundServiceRunning", + "screenSharingTitle", + "screenSharingDetail", + "callingPermissions", + "callingAccount", + "callingAccountDetails", + "appearOnTop", + "appearOnTopDetails", + "otherCallingPermissions", + "whyIsThisMessageEncrypted", + "noKeyForThisMessage", + "newGroup", + "newSpace", + "enterSpace", + "enterRoom", + "allSpaces", + "numChats", + "hideUnimportantStateEvents", + "doNotShowAgain", + "wasDirectChatDisplayName", + "newSpaceDescription", + "encryptThisChat", + "endToEndEncryption", + "disableEncryptionWarning", + "sorryThatsNotPossible", + "deviceKeys", + "letsStart", + "enterInviteLinkOrMatrixId", + "reopenChat", + "noBackupWarning", + "noOtherDevicesFound", + "fileIsTooBigForServer", + "fileHasBeenSavedAt", + "jumpToLastReadMessage", + "readUpToHere", + "jump", + "openLinkInBrowser", + "reportErrorDescription", + "report", + "signInWithPassword", + "continueWith", + "pleaseTryAgainLaterOrChooseDifferentServer", + "signInWith", + "profileNotFound", + "setTheme", + "setColorTheme", + "invite", + "requests", + "inviteGroupChat", + "invitePrivateChat", + "invalidInput", + "wrongPinEntered", + "pleaseEnterANumber", + "archiveRoomDescription", + "roomUpgradeDescription", + "allCorrect", + "newWayAllGood", + "othersAreBetter", + "holdForInfo", + "greenFeedback", + "yellowFeedback", + "redFeedback", + "customInputFeedbackChoice", + "itInstructionsTitle", + "itInstructionsBody", + "toggleLanguages", + "classWelcomeChat", + "deleteSpace", + "deleteGroup", + "areYouSureDeleteClass", + "areYouSureDeleteGroup", + "cannotBeReversed", + "enterDeletedClassName", + "incorrectClassName", + "oneday", + "oneweek", + "onemonth", + "sixmonth", + "oneyear", + "gaTooltip", + "taTooltip", + "unTooltip", + "interactiveTranslatorSliderHeader", + "interactiveGrammarSliderHeader", + "interactiveTranslatorNotAllowed", + "interactiveTranslatorAllowed", + "interactiveTranslatorRequired", + "interactiveTranslatorNotAllowedDesc", + "interactiveTranslatorAllowedDesc", + "interactiveTranslatorRequiredDesc", + "notYetSet", + "multiLingualClass", + "classAnalytics", + "allClasses", + "myLearning", + "allChatsAndClasses", + "timeOfLastMessage", + "totalMessages", + "waTooltip", + "changeDateRange", + "numberOfStudents", + "classDescription", + "classDescriptionDesc", + "requestToEnroll", + "requestAnExchange", + "findLanguageExchange", + "classAnalyticsDesc", + "addStudents", + "copyClassLink", + "copyClassLinkDesc", + "copyClassCode", + "inviteStudentByUserName", + "classSettings", + "classSettingsDesc", + "selectClassRoomDominantLanguage", + "selectTargetLanguage", + "whatIsYourClassLanguageLevel", + "studentPermissions", + "interactiveTranslator", + "oneToOneChatsWithinClass", + "oneToOneChatsWithinClassDesc", + "createGroupChats", + "createGroupChatsDesc", + "shareVideo", + "shareVideoDesc", + "sharePhotos", + "sharePhotosDesc", + "shareFiles", + "shareFilesDesc", + "shareLocationDesc", + "selectLanguageLevel", + "noIdenticalLanguages", + "iWantALanguagePartnerFrom", + "worldWide", + "noResults", + "searchBy", + "iWantAConversationPartner", + "iWantALanguagePartnerWhoSpeaks", + "iWantALanguagePartnerWhoIsLearning", + "yourBirthdayPlease", + "invalidDob", + "enterYourDob", + "getStarted", + "mustBe13", + "yourBirthdayPleaseShort", + "errorPleaseRefresh", + "joinWithClassCode", + "joinWithClassCodeDesc", + "joinWithClassCodeHint", + "unableToFindClass", + "languageLevelPreA1", + "languageLevelA1", + "languageLevelA2", + "languageLevelB1", + "languageLevelB2", + "languageLevelC1", + "languageLevelC2", + "changeTheNameOfTheClass", + "changeTheNameOfTheChat", + "welcomeToYourNewClass", + "welcomeToClass", + "welcomeToPangea18Plus", + "welcomeToPangeaMinor", + "findALanguagePartner", + "setToPublicSettingsTitle", + "setToPublicSettingsDesc", + "accountSettings", + "unableToFindClassCode", + "askPangeaBot", + "sorryNoResults", + "ignoreInThisText", + "helpMeTranslate", + "needsItShortMessage", + "needsIGCShortMessage", + "needsItMessage", + "needsIgcMessage", + "tokenTranslationTitle", + "spanTranslationDesc", + "spanTranslationTitle", + "l1SpanAndGrammarTitle", + "l1SpanAndGrammarDesc", + "otherTitle", + "otherDesc", + "countryInformation", + "myLanguages", + "targetLanguage", + "sourceLanguage", + "languagesISpeak", + "updateLanguage", + "whatLanguageYouWantToLearn", + "whatIsYourBaseLanguage", + "saveChanges", + "publicProfileTitle", + "publicProfileDesc", + "error502504Title", + "error502504Desc", + "error404Title", + "error404Desc", + "errorDisableIT", + "errorDisableIGC", + "errorDisableLanguageAssistance", + "errorDisableITUserDesc", + "errorDisableIGCUserDesc", + "errorDisableLanguageAssistanceUserDesc", + "errorDisableITClassDesc", + "errorDisableIGCClassDesc", + "errorDisableLanguageAssistanceClassDesc", + "itIsDisabled", + "igcIsDisabled", + "goToLearningSettings", + "error405Title", + "error405Desc", + "loginOrSignup", + "iAgreeToThe", + "termsAndConditions", + "andCertifyIAmAtLeast13YearsOfAge", + "findAClass", + "toggleIT", + "toggleIGC", + "toggleToolSettingsDescription", + "connectedToStaging", + "learningSettings", + "classNameRequired", + "sendVoiceNotes", + "sendVoiceNotesDesc", + "chatTopic", + "chatTopicDesc", + "inviteStudentByUserNameDesc", + "classRoster", + "almostPerfect", + "prettyGood", + "letMeThink", + "clickMessageTitle", + "clickMessageBody", + "understandingMessagesTitle", + "addToClass", + "understandingMessagesBody", + "allDone", + "vocab", + "low", + "medium", + "high", + "unknownProficiency", + "changeView", + "clearAll", + "generateVocabulary", + "generatePrompts", + "subscribe", + "getAccess", + "subscriptionDesc", + "subscriptionManagement", + "currentSubscription", + "changeSubscription", + "cancelSubscription", + "selectYourPlan", + "subsciptionPlatformTooltip", + "subscriptionManagementUnavailable", + "paymentMethod", + "paymentHistory", + "emptyChatDownloadWarning", + "appUpdateAvailable", + "update", + "updateDesc", + "maybeLater", + "mainMenu", + "toggleImmersionMode", + "toggleImmersionModeDesc", + "itToggleDescription", + "igcToggleDescription", + "sendOnEnterDescription", + "alreadyInClass", + "pleaseLoginFirst", + "originalMessage", + "sentMessage", + "useType", + "notAvailable", + "taAndGaTooltip", + "definitionsToolName", + "messageTranslationsToolName", + "definitionsToolDescription", + "translationsToolDescrption", + "welcomeBack", + "classExchanges", + "createNewClass", + "newExchange", + "kickAllStudents", + "kickAllStudentsConfirmation", + "inviteAllStudents", + "inviteAllStudentsConfirmation", + "inviteStudentsFromOtherClasses", + "inviteUsersFromPangea", + "allExchanges", + "redeemPromoCode", + "enterPromoCode", + "downloadTxtFile", + "downloadCSVFile", + "promotionalSubscriptionDesc", + "originalSubscriptionPlatform", + "oneWeekTrial", + "creatingSpacePleaseWait", + "downloadXLSXFile", + "abDisplayName", + "aaDisplayName", + "afDisplayName", + "akDisplayName", + "sqDisplayName", + "amDisplayName", + "arDisplayName", + "anDisplayName", + "hyDisplayName", + "asDisplayName", + "avDisplayName", + "aeDisplayName", + "ayDisplayName", + "azDisplayName", + "bmDisplayName", + "baDisplayName", + "euDisplayName", + "beDisplayName", + "bnDisplayName", + "bhDisplayName", + "biDisplayName", + "bsDisplayName", + "brDisplayName", + "bgDisplayName", + "myDisplayName", + "caDisplayName", + "chDisplayName", + "ceDisplayName", + "nyDisplayName", + "zhDisplayName", + "cvDisplayName", + "kwDisplayName", + "coDisplayName", + "crDisplayName", + "hrDisplayName", + "csDisplayName", + "daDisplayName", + "dvDisplayName", + "nlDisplayName", + "enDisplayName", + "eoDisplayName", + "etDisplayName", + "eeDisplayName", + "foDisplayName", + "fjDisplayName", + "fiDisplayName", + "frDisplayName", + "ffDisplayName", + "glDisplayName", + "kaDisplayName", + "deDisplayName", + "elDisplayName", + "gnDisplayName", + "guDisplayName", + "htDisplayName", + "haDisplayName", + "heDisplayName", + "hzDisplayName", + "hiDisplayName", + "hoDisplayName", + "huDisplayName", + "iaDisplayName", + "idDisplayName", + "ieDisplayName", + "gaDisplayName", + "igDisplayName", + "ikDisplayName", + "ioDisplayName", + "isDisplayName", + "itDisplayName", + "iuDisplayName", + "jaDisplayName", + "jvDisplayName", + "klDisplayName", + "knDisplayName", + "krDisplayName", + "ksDisplayName", + "kkDisplayName", + "kmDisplayName", + "kiDisplayName", + "rwDisplayName", + "kyDisplayName", + "kvDisplayName", + "kgDisplayName", + "koDisplayName", + "kuDisplayName", + "kjDisplayName", + "laDisplayName", + "lbDisplayName", + "lgDisplayName", + "liDisplayName", + "lnDisplayName", + "loDisplayName", + "ltDisplayName", + "luDisplayName", + "lvDisplayName", + "gvDisplayName", + "mkDisplayName", + "mgDisplayName", + "msDisplayName", + "mlDisplayName", + "mtDisplayName", + "miDisplayName", + "mrDisplayName", + "mhDisplayName", + "mnDisplayName", + "naDisplayName", + "nvDisplayName", + "nbDisplayName", + "ndDisplayName", + "neDisplayName", + "ngDisplayName", + "nnDisplayName", + "noDisplayName", + "iiDisplayName", + "nrDisplayName", + "ocDisplayName", + "ojDisplayName", + "cuDisplayName", + "omDisplayName", + "orDisplayName", + "osDisplayName", + "paDisplayName", + "piDisplayName", + "faDisplayName", + "plDisplayName", + "psDisplayName", + "ptDisplayName", + "quDisplayName", + "rmDisplayName", + "rnDisplayName", + "roDisplayName", + "ruDisplayName", + "saDisplayName", + "scDisplayName", + "sdDisplayName", + "seDisplayName", + "smDisplayName", + "sgDisplayName", + "srDisplayName", + "gdDisplayName", + "snDisplayName", + "siDisplayName", + "skDisplayName", + "slDisplayName", + "soDisplayName", + "stDisplayName", + "esDisplayName", + "suDisplayName", + "swDisplayName", + "ssDisplayName", + "svDisplayName", + "taDisplayName", + "teDisplayName", + "tgDisplayName", + "thDisplayName", + "tiDisplayName", + "boDisplayName", + "tkDisplayName", + "tlDisplayName", + "tnDisplayName", + "toDisplayName", + "trDisplayName", + "tsDisplayName", + "ttDisplayName", + "twDisplayName", + "tyDisplayName", + "ugDisplayName", + "ukDisplayName", + "urDisplayName", + "uzDisplayName", + "veDisplayName", + "viDisplayName", + "voDisplayName", + "waDisplayName", + "cyDisplayName", + "woDisplayName", + "fyDisplayName", + "xhDisplayName", + "yiDisplayName", + "yoDisplayName", + "zaDisplayName", + "unkDisplayName", + "zuDisplayName", + "hawDisplayName", + "hmnDisplayName", + "multiDisplayName", + "cebDisplayName", + "dzDisplayName", + "iwDisplayName", + "jwDisplayName", + "moDisplayName", + "shDisplayName", + "wwCountryDisplayName", + "afCountryDisplayName", + "axCountryDisplayName", + "alCountryDisplayName", + "dzCountryDisplayName", + "asCountryDisplayName", + "adCountryDisplayName", + "aoCountryDisplayName", + "aiCountryDisplayName", + "agCountryDisplayName", + "arCountryDisplayName", + "amCountryDisplayName", + "awCountryDisplayName", + "acCountryDisplayName", + "auCountryDisplayName", + "atCountryDisplayName", + "azCountryDisplayName", + "bsCountryDisplayName", + "bhCountryDisplayName", + "bdCountryDisplayName", + "bbCountryDisplayName", + "byCountryDisplayName", + "beCountryDisplayName", + "bzCountryDisplayName", + "bjCountryDisplayName", + "bmCountryDisplayName", + "btCountryDisplayName", + "boCountryDisplayName", + "baCountryDisplayName", + "bwCountryDisplayName", + "brCountryDisplayName", + "ioCountryDisplayName", + "vgCountryDisplayName", + "bnCountryDisplayName", + "bgCountryDisplayName", + "bfCountryDisplayName", + "biCountryDisplayName", + "khCountryDisplayName", + "cmCountryDisplayName", + "caCountryDisplayName", + "cvCountryDisplayName", + "bqCountryDisplayName", + "kyCountryDisplayName", + "cfCountryDisplayName", + "tdCountryDisplayName", + "clCountryDisplayName", + "cnCountryDisplayName", + "cxCountryDisplayName", + "ccCountryDisplayName", + "coCountryDisplayName", + "kmCountryDisplayName", + "cdCountryDisplayName", + "cgCountryDisplayName", + "ckCountryDisplayName", + "crCountryDisplayName", + "ciCountryDisplayName", + "hrCountryDisplayName", + "cuCountryDisplayName", + "cwCountryDisplayName", + "cyCountryDisplayName", + "czCountryDisplayName", + "dkCountryDisplayName", + "djCountryDisplayName", + "dmCountryDisplayName", + "doCountryDisplayName", + "tlCountryDisplayName", + "ecCountryDisplayName", + "egCountryDisplayName", + "svCountryDisplayName", + "gqCountryDisplayName", + "erCountryDisplayName", + "eeCountryDisplayName", + "szCountryDisplayName", + "etCountryDisplayName", + "fkCountryDisplayName", + "foCountryDisplayName", + "fjCountryDisplayName", + "fiCountryDisplayName", + "frCountryDisplayName", + "gfCountryDisplayName", + "pfCountryDisplayName", + "gaCountryDisplayName", + "gmCountryDisplayName", + "geCountryDisplayName", + "deCountryDisplayName", + "ghCountryDisplayName", + "giCountryDisplayName", + "grCountryDisplayName", + "glCountryDisplayName", + "gdCountryDisplayName", + "gpCountryDisplayName", + "guCountryDisplayName", + "gtCountryDisplayName", + "ggCountryDisplayName", + "gnCountryDisplayName", + "gwCountryDisplayName", + "gyCountryDisplayName", + "htCountryDisplayName", + "hmCountryDisplayName", + "hnCountryDisplayName", + "hkCountryDisplayName", + "huCountryDisplayName", + "isCountryDisplayName", + "inCountryDisplayName", + "idCountryDisplayName", + "irCountryDisplayName", + "iqCountryDisplayName", + "ieCountryDisplayName", + "imCountryDisplayName", + "ilCountryDisplayName", + "itCountryDisplayName", + "jmCountryDisplayName", + "jpCountryDisplayName", + "jeCountryDisplayName", + "joCountryDisplayName", + "kzCountryDisplayName", + "keCountryDisplayName", + "kiCountryDisplayName", + "xkCountryDisplayName", + "kwCountryDisplayName", + "kgCountryDisplayName", + "laCountryDisplayName", + "lvCountryDisplayName", + "lbCountryDisplayName", + "lsCountryDisplayName", + "lrCountryDisplayName", + "lyCountryDisplayName", + "liCountryDisplayName", + "ltCountryDisplayName", + "luCountryDisplayName", + "moCountryDisplayName", + "mkCountryDisplayName", + "mgCountryDisplayName", + "mwCountryDisplayName", + "myCountryDisplayName", + "mvCountryDisplayName", + "mlCountryDisplayName", + "mtCountryDisplayName", + "mhCountryDisplayName", + "mqCountryDisplayName", + "mrCountryDisplayName", + "muCountryDisplayName", + "ytCountryDisplayName", + "mxCountryDisplayName", + "fmCountryDisplayName", + "mdCountryDisplayName", + "mcCountryDisplayName", + "mnCountryDisplayName", + "meCountryDisplayName", + "msCountryDisplayName", + "maCountryDisplayName", + "mzCountryDisplayName", + "mmCountryDisplayName", + "naCountryDisplayName", + "nrCountryDisplayName", + "npCountryDisplayName", + "nlCountryDisplayName", + "ncCountryDisplayName", + "nzCountryDisplayName", + "niCountryDisplayName", + "neCountryDisplayName", + "ngCountryDisplayName", + "nuCountryDisplayName", + "nfCountryDisplayName", + "kpCountryDisplayName", + "mpCountryDisplayName", + "noCountryDisplayName", + "omCountryDisplayName", + "pkCountryDisplayName", + "pwCountryDisplayName", + "psCountryDisplayName", + "paCountryDisplayName", + "pgCountryDisplayName", + "pyCountryDisplayName", + "peCountryDisplayName", + "phCountryDisplayName", + "plCountryDisplayName", + "ptCountryDisplayName", + "prCountryDisplayName", + "qaCountryDisplayName", + "reCountryDisplayName", + "roCountryDisplayName", + "ruCountryDisplayName", + "rwCountryDisplayName", + "blCountryDisplayName", + "shCountryDisplayName", + "knCountryDisplayName", + "lcCountryDisplayName", + "mfCountryDisplayName", + "pmCountryDisplayName", + "vcCountryDisplayName", + "wsCountryDisplayName", + "smCountryDisplayName", + "stCountryDisplayName", + "saCountryDisplayName", + "snCountryDisplayName", + "rsCountryDisplayName", + "scCountryDisplayName", + "slCountryDisplayName", + "sgCountryDisplayName", + "sxCountryDisplayName", + "skCountryDisplayName", + "siCountryDisplayName", + "sbCountryDisplayName", + "soCountryDisplayName", + "zaCountryDisplayName", + "gsCountryDisplayName", + "krCountryDisplayName", + "ssCountryDisplayName", + "esCountryDisplayName", + "lkCountryDisplayName", + "sdCountryDisplayName", + "srCountryDisplayName", + "sjCountryDisplayName", + "seCountryDisplayName", + "chCountryDisplayName", + "syCountryDisplayName", + "twCountryDisplayName", + "tjCountryDisplayName", + "tzCountryDisplayName", + "thCountryDisplayName", + "tgCountryDisplayName", + "tkCountryDisplayName", + "toCountryDisplayName", + "ttCountryDisplayName", + "tnCountryDisplayName", + "trCountryDisplayName", + "tmCountryDisplayName", + "tcCountryDisplayName", + "tvCountryDisplayName", + "viCountryDisplayName", + "ugCountryDisplayName", + "uaCountryDisplayName", + "aeCountryDisplayName", + "gbCountryDisplayName", + "usCountryDisplayName", + "uyCountryDisplayName", + "uzCountryDisplayName", + "vuCountryDisplayName", + "vaCountryDisplayName", + "veCountryDisplayName", + "vnCountryDisplayName", + "wfCountryDisplayName", + "ehCountryDisplayName", + "yeCountryDisplayName", + "zmCountryDisplayName", + "zwCountryDisplayName", + "pay", + "allPrivateChats", + "unknownPrivateChat", + "copyClassCodeDesc", + "addToClassDesc", + "addToClassOrExchange", + "addToClassOrExchangeDesc", + "invitedToClassOrExchange", + "decline", + "declinedInvitation", + "acceptedInvitation", + "youreInvited", + "studentPermissionsDesc", + "noEligibleSpaces", + "youAddedToSpace", + "youRemovedFromSpace", + "invitedToChat", + "monthlySubscription", + "yearlySubscription", + "defaultSubscription", + "freeTrial", + "grammarAnalytics", + "total", + "noDataFound", + "promoSubscriptionExpirationDesc", + "emptyChatNameWarning", + "emptyClassNameWarning", + "emptyExchangeNameWarning", + "blurMeansTranslateTitle", + "blurMeansTranslateBody", + "someErrorTitle", + "someErrorBody", + "bestCorrectionFeedback", + "distractorFeedback", + "bestAnswerFeedback", + "definitionDefaultPrompt", + "practiceDefaultPrompt", + "correctionDefaultPrompt", + "itStartDefaultPrompt", + "languageLevelWarning", + "lockedChatWarning", + "lockSpace", + "lockChat", + "archiveSpace", + "suggestTo", + "suggestChatDesc", + "suggestExchangeDesc", + "acceptSelection", + "acceptSelectionAnyway", + "makingActivity", + "why", + "definition", + "exampleSentence", + "addToClassTitle", + "reportToTeacher", + "reportMessageTitle", + "reportMessageBody", + "noTeachersFound", + "pushNotificationsNotAvailable", + "learnMore", + "banUserDescription", + "unbanUserDescription", + "kickUserDescription", + "makeAdminDescription" + ] +} diff --git a/pangea_packages/fcm_shared_isolate/CHANGELOG.md b/pangea_packages/fcm_shared_isolate/CHANGELOG.md new file mode 100644 index 000000000..e53776993 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- Initial release \ No newline at end of file diff --git a/pangea_packages/fcm_shared_isolate/LICENSE b/pangea_packages/fcm_shared_isolate/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/pangea_packages/fcm_shared_isolate/README.md b/pangea_packages/fcm_shared_isolate/README.md new file mode 100644 index 000000000..73f89d4db --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/README.md @@ -0,0 +1,122 @@ +# fcm_shared_isolate + +Firebase Messaging Plugin for Flutter supporting shared isolate + +## Installing the library +After adding the library to your `pubspec.yaml` do the following things: + +1. Modify the main activity on the android side of your app to look like the following + (typically in `android/app/src/main/kotlin/your/app/id/MainActivity.kt`): + +```kotlin +package your.app.id + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.WindowManager + +class MainActivity : FlutterActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + }; + + override fun provideFlutterEngine(context: Context): FlutterEngine? { + return provideEngine(this) + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + // do nothing, because the engine was been configured in provideEngine + } + + companion object { + var engine: FlutterEngine? = null + fun provideEngine(context: Context): FlutterEngine { + var eng = engine ?: FlutterEngine(context, emptyArray(), true, false) + engine = eng + return eng + } + } +``` + +2. Add an `FcmPushService` (typically in `android/app/src/main/kotlin/your/app/id/FcmPushService.kt`) + +```kotlin +package your.app.id + +import com.famedly.fcm_shared_isolate.FcmSharedIsolateService + +import your.app.id.MainActivity + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.view.FlutterMain +import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.WindowManager + +class FcmPushService : FcmSharedIsolateService() { + override fun getEngine(): FlutterEngine { + return provideEngine(getApplicationContext()) + } + + companion object { + fun provideEngine(context: Context): FlutterEngine { + var engine = MainActivity.engine + if (engine == null) { + engine = MainActivity.provideEngine(context) + engine.getLocalizationPlugin().sendLocalesToFlutter( + context.getResources().getConfiguration()) + engine.getDartExecutor().executeDartEntrypoint( + DartEntrypoint.createDefault()) + } + return engine + } + } +} + +``` + +3. Add the intent filters to your `AndroidManifest.xml` (typically in `android/app/src/main/AndroidManifest.xml`): + +```xml + + + + + +``` + +Note that the `.FcmPushService` has to match the class name defined in the file above + +## Usage + +```dart +// Create the instance +final fcm = FcmSharedIsolate(); + +// Only for iOS you need to request permissions: +if (Platform.isIOS) { + await fcm.requestPermission(); +} + +// Get the push token: +await fcm.getToken(); + +// Set the listeners +fcm.setListeners( + onMessage: onMessage, + onNewToken: onNewToken, +); + +Future onMessage(Map message) async { + print('Got a new message from firebase cloud messaging: $message'); +} +``` \ No newline at end of file diff --git a/pangea_packages/fcm_shared_isolate/analysis_options.yaml b/pangea_packages/fcm_shared_isolate/analysis_options.yaml new file mode 100644 index 000000000..0996667b3 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:pedantic/analysis_options.yaml + +linter: + rules: + - camel_case_types + - avoid_print + - constant_identifier_names + - prefer_final_locals + - prefer_final_in_for_each diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/executionHistory/executionHistory.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/executionHistory/executionHistory.lock new file mode 100644 index 000000000..ec3fe34fd Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/executionHistory/executionHistory.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileChanges/last-build.bin b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileChanges/last-build.bin differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileHashes/fileHashes.bin b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileHashes/fileHashes.bin new file mode 100644 index 000000000..5dfd940b6 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileHashes/fileHashes.bin differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileHashes/fileHashes.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileHashes/fileHashes.lock new file mode 100644 index 000000000..7cb52b3f8 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/fileHashes/fileHashes.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/gc.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/6.7.1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/fileChanges/last-build.bin b/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/fileChanges/last-build.bin differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/fileHashes/fileHashes.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/fileHashes/fileHashes.lock new file mode 100644 index 000000000..6eb41fd25 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/fileHashes/fileHashes.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/gc.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/6.8.3/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/checksums/checksums.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/checksums/checksums.lock new file mode 100644 index 000000000..9b9a81fae Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/checksums/checksums.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/dependencies-accessors/dependencies-accessors.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/dependencies-accessors/dependencies-accessors.lock new file mode 100644 index 000000000..166e91e36 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/dependencies-accessors/dependencies-accessors.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/dependencies-accessors/gc.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/dependencies-accessors/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/fileChanges/last-build.bin b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/fileChanges/last-build.bin new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/fileChanges/last-build.bin differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/fileHashes/fileHashes.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/fileHashes/fileHashes.lock new file mode 100644 index 000000000..d6084472c Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/fileHashes/fileHashes.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/gc.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/7.3.3/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 000000000..487a88833 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/buildOutputCleanup/cache.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 000000000..889a37ac8 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Tue Jan 17 18:47:16 PKT 2023 +gradle.version=6.7.1 diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/checksums.lock b/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/checksums.lock new file mode 100644 index 000000000..b99989c33 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/checksums.lock differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/md5-checksums.bin b/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/md5-checksums.bin new file mode 100644 index 000000000..6f9cbd72b Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/md5-checksums.bin differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/sha1-checksums.bin b/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/sha1-checksums.bin new file mode 100644 index 000000000..9391a9b33 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/android/.gradle/checksums/sha1-checksums.bin differ diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/configuration-cache/gc.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/configuration-cache/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/pangea_packages/fcm_shared_isolate/android/.gradle/vcs-1/gc.properties b/pangea_packages/fcm_shared_isolate/android/.gradle/vcs-1/gc.properties new file mode 100644 index 000000000..e69de29bb diff --git a/pangea_packages/fcm_shared_isolate/android/build.gradle b/pangea_packages/fcm_shared_isolate/android/build.gradle new file mode 100644 index 000000000..59f00010c --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/build.gradle @@ -0,0 +1,62 @@ +group 'com.famedly.fcm_shared_isolate' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.4.32' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +def firebaseCoreProject = findProject(':firebase_core') +if (firebaseCoreProject == null) { + + throw GradleException('Could not find the firebase_core FlutterFire plugin, have you added it as a dependency in your pubspec?') +} else if (!firebaseCoreProject.properties['FirebaseSDKVersion']) { + throw GradleException('A newer version of the firebase_core FlutterFire plugin is required, please update your firebase_core pubspec dependency.') +} + +def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) { + if (!rootProject.ext.has('FlutterFire')) return firebaseCoreProject.properties[name] + if (!rootProject.ext.get('FlutterFire')[name]) return firebaseCoreProject.properties[name] + return rootProject.ext.get('FlutterFire').get(name) +} + +android { + compileSdkVersion 30 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } + dependencies { + api firebaseCoreProject + implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'com.google.firebase:firebase-messaging-ktx:23.0.2' +} diff --git a/pangea_packages/fcm_shared_isolate/android/gradle.properties b/pangea_packages/fcm_shared_isolate/android/gradle.properties new file mode 100644 index 000000000..94adc3a3f --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/pangea_packages/fcm_shared_isolate/android/gradle/wrapper/gradle-wrapper.properties b/pangea_packages/fcm_shared_isolate/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9fe8d05db --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/pangea_packages/fcm_shared_isolate/android/local.properties b/pangea_packages/fcm_shared_isolate/android/local.properties new file mode 100644 index 000000000..5ca26ae66 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Tue Jan 17 02:51:21 PKT 2023 +sdk.dir=/Users/lalaiqbalkhokhar/Library/Android/sdk diff --git a/pangea_packages/fcm_shared_isolate/android/settings.gradle b/pangea_packages/fcm_shared_isolate/android/settings.gradle new file mode 100644 index 000000000..12cfbc0b0 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'fcm_shared_isolate' diff --git a/pangea_packages/fcm_shared_isolate/android/src/main/AndroidManifest.xml b/pangea_packages/fcm_shared_isolate/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9ec46a345 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/pangea_packages/fcm_shared_isolate/android/src/main/kotlin/com/famedly/fcm_shared_isolate/FcmSharedIsolatePlugin.kt b/pangea_packages/fcm_shared_isolate/android/src/main/kotlin/com/famedly/fcm_shared_isolate/FcmSharedIsolatePlugin.kt new file mode 100644 index 000000000..fe0f85af3 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/src/main/kotlin/com/famedly/fcm_shared_isolate/FcmSharedIsolatePlugin.kt @@ -0,0 +1,48 @@ + +package com.famedly.fcm_shared_isolate + +import androidx.annotation.NonNull +import com.google.firebase.messaging.FirebaseMessaging +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class FcmSharedIsolatePlugin : FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + + private val fcm = FirebaseMessaging.getInstance() + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "fcm_shared_isolate") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (fcm == null) { + result.error("fcm_unavailable", null, null) + return + } + + if (call.method == "getToken") { + val getToken = FirebaseMessaging.getInstance().getToken() + getToken.addOnSuccessListener { result.success(it) } + getToken.addOnFailureListener { result.error("unknown", null, null) } + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + fun message(@NonNull data: Map) { + channel.invokeMethod("message", data) + } + + fun token(@NonNull str: String) { + channel.invokeMethod("token", str) + } +} diff --git a/pangea_packages/fcm_shared_isolate/android/src/main/kotlin/com/famedly/fcm_shared_isolate/FcmSharedIsolateService.kt b/pangea_packages/fcm_shared_isolate/android/src/main/kotlin/com/famedly/fcm_shared_isolate/FcmSharedIsolateService.kt new file mode 100644 index 000000000..7446d6939 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/android/src/main/kotlin/com/famedly/fcm_shared_isolate/FcmSharedIsolateService.kt @@ -0,0 +1,34 @@ +package com.famedly.fcm_shared_isolate + +import android.os.Handler +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import io.flutter.embedding.engine.FlutterEngine + +abstract class FcmSharedIsolateService : FirebaseMessagingService() { + abstract fun getEngine(): FlutterEngine + + private val handler = Handler() + + private fun getPlugin(): FcmSharedIsolatePlugin { + val registry = getEngine().getPlugins() + var plugin = registry.get(FcmSharedIsolatePlugin::class.java) as? FcmSharedIsolatePlugin + if (plugin == null) { + plugin = FcmSharedIsolatePlugin() + registry.add(plugin) + } + return plugin + } + + override fun onMessageReceived(message: RemoteMessage) { + handler.post { + getPlugin().message(message.getData()) + } + } + + override fun onNewToken(token: String) { + handler.post { + getPlugin().token(token) + } + } +} diff --git a/pangea_packages/fcm_shared_isolate/build/test_cache/build/c075001b96339384a97db4862b8ab8db.cache.dill.track.dill b/pangea_packages/fcm_shared_isolate/build/test_cache/build/c075001b96339384a97db4862b8ab8db.cache.dill.track.dill new file mode 100644 index 000000000..4c868dc8b Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/build/test_cache/build/c075001b96339384a97db4862b8ab8db.cache.dill.track.dill differ diff --git a/pangea_packages/fcm_shared_isolate/build/unit_test_assets/AssetManifest.json b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/AssetManifest.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/AssetManifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pangea_packages/fcm_shared_isolate/build/unit_test_assets/FontManifest.json b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/FontManifest.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/FontManifest.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/pangea_packages/fcm_shared_isolate/build/unit_test_assets/NOTICES.Z b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/NOTICES.Z new file mode 100644 index 000000000..2b6801c84 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/NOTICES.Z differ diff --git a/pangea_packages/fcm_shared_isolate/build/unit_test_assets/shaders/ink_sparkle.frag b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/shaders/ink_sparkle.frag new file mode 100644 index 000000000..ead18b083 Binary files /dev/null and b/pangea_packages/fcm_shared_isolate/build/unit_test_assets/shaders/ink_sparkle.frag differ diff --git a/pangea_packages/fcm_shared_isolate/ios/Classes/FcmSharedIsolatePlugin.swift b/pangea_packages/fcm_shared_isolate/ios/Classes/FcmSharedIsolatePlugin.swift new file mode 100644 index 000000000..50a89f127 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/ios/Classes/FcmSharedIsolatePlugin.swift @@ -0,0 +1,92 @@ +import Flutter +import FirebaseCore +import FirebaseMessaging + +@objc public class FcmSharedIsolatePlugin: NSObject, FlutterPlugin, MessagingDelegate { + internal init(channel: FlutterMethodChannel) { + self.channel = channel + } + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "fcm_shared_isolate", binaryMessenger: registrar.messenger()) + let instance = FcmSharedIsolatePlugin(channel: channel) + registrar.addApplicationDelegate(instance) + registrar.addMethodCallDelegate(instance, channel: channel) + } + + let channel: FlutterMethodChannel + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getToken": + Messaging.messaging().delegate = self + Messaging.messaging().token { token, error in + if let error = error { + result(FlutterError(code: "unknown", message: nil, details: error.localizedDescription)) + } else if let token = token { + print("Some ok") + result(String(token)) + } + } + case "requestPermission": + let arguments = call.arguments as! NSDictionary; + if #available(iOS 10.0, *) { + var authOptions: UNAuthorizationOptions = [] + if arguments["sound"] as! Bool { + authOptions.insert(.sound) + } + if arguments["alert"] as! Bool { + authOptions.insert(.alert) + } + if arguments["badge"] as! Bool { + authOptions.insert(.badge) + } + if arguments["provisional"] as! Bool { + if #available(iOS 12.0, *) { + authOptions.insert(.provisional) + } + } + + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: { [] granted, error in + if let error = error { + result(FlutterError(code: "perm", message: nil, details: error.localizedDescription)) + return + } + + result(granted) + } + ) + + UIApplication.shared.registerForRemoteNotifications() + } else { + var notificationTypes: UIUserNotificationType = [] + if arguments["sound"] as! Bool { + notificationTypes.insert(.sound) + } + if arguments["alert"] as! Bool { + notificationTypes.insert(.alert) + } + if arguments["badge"] as! Bool { + notificationTypes.insert(.badge) + } + + let settings = UIUserNotificationSettings(types: notificationTypes, categories: nil) + UIApplication.shared.registerUserNotificationSettings(settings) + + UIApplication.shared.registerForRemoteNotifications() + + result(true) + } + default: + assertionFailure(call.method) + result(FlutterMethodNotImplemented) + } + } + + public func messaging(_ messaging: Messaging, didReceiveRegistrationToken token: String?) { + + channel.invokeMethod("token", arguments: [token]) + } +} diff --git a/pangea_packages/fcm_shared_isolate/ios/fcm_shared_isolate.podspec b/pangea_packages/fcm_shared_isolate/ios/fcm_shared_isolate.podspec new file mode 100644 index 000000000..9e2fb06ed --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/ios/fcm_shared_isolate.podspec @@ -0,0 +1,18 @@ +Pod::Spec.new do |s| + s.name = 'fcm_shared_isolate' + s.version = '0.0.1' + s.summary = 'fixme' + s.homepage = 'https://gitlab.com/famedly/libraries/fcm_shared_isolate' + s.license = { :file => '../LICENSE' } + s.author = { 'Famedly' => 'info@famedly.de' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.dependency 'FirebaseCore' + s.dependency 'FirebaseMessaging' + s.swift_version = '5.2' + s.ios.deployment_target = '11.0' + s.static_framework = true + +end diff --git a/pangea_packages/fcm_shared_isolate/lib/fcm_shared_isolate.dart b/pangea_packages/fcm_shared_isolate/lib/fcm_shared_isolate.dart new file mode 100644 index 000000000..843716626 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/lib/fcm_shared_isolate.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class FcmSharedIsolate { + final _channel = MethodChannel('fcm_shared_isolate'); + final _msg = >[]; + void Function(Map)? _onMessage; + void Function(String)? _onNewToken; + + FcmSharedIsolate() { + _channel.setMethodCallHandler(handle); + } + + Future handle(MethodCall call) async { + if (call.method == 'message') { + final Map data = call.arguments; + final onMessage = _onMessage; + if (onMessage != null) { + onMessage(data); + } else { + _msg.add(data); + } + } else if (call.method == 'token') { + final String newToken = call.arguments; + _onNewToken?.call(newToken); + } + return null; + } + + Future getToken() async { + // if (Platform.isAndroid) { + // await Firebase.initializeApp(); + // return (await FirebaseMessaging.instance.getToken())!; + // } + return await _channel.invokeMethod('getToken'); + } + + void setListeners({ + void Function(Map)? onMessage, + void Function(String)? onNewToken, + }) { + _onMessage = onMessage; + _onNewToken = onNewToken; + if (onMessage != null) { + _msg.forEach(onMessage); + _msg.clear(); + } + } + + Future requestPermission( + {bool sound = true, + bool alert = true, + bool badge = true, + bool provisional = false}) async { + if (kIsWeb || !Platform.isIOS) { + return true; + } + + final bool result = await _channel.invokeMethod('requestPermission', { + 'sound': sound, + 'alert': alert, + 'badge': badge, + 'provisional': provisional, + }); + return result; + } +} diff --git a/pangea_packages/fcm_shared_isolate/pubspec.lock b/pangea_packages/fcm_shared_isolate/pubspec.lock new file mode 100644 index 000000000..41db938bc --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/pubspec.lock @@ -0,0 +1,242 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "3ff770dfff04a67b0863dff205a0936784de1b87a5e99b11c693fc10e66a9ce3" + url: "https://pub.dev" + source: hosted + version: "1.0.12" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: c129209ba55f3d4272c89fb4a4994c15bea77fb6de63a82d45fb6bc5c94e4355 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5fab93f5b354648efa62e7cc829c90efb68c8796eecf87e0888cae2d5f3accd4" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "18b35ce111b0a4266abf723c825bcf9d4e2519d13638cc7f06f2a8dd960c75bc" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: dc010a6436333029fba858415fe65887c3fe44d8f6e6ea162bb8d3dd764fbcb6 + url: "https://pub.dev" + source: hosted + version: "14.2.1" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: abda2d766486096eb1c568c7b20aef46180596c8b0708190b929133ff03e0a8d + url: "https://pub.dev" + source: hosted + version: "4.2.10" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "7a0ce957bd2210e8636325152234728874dad039f1c7271ba1be5c752fdc5888" + url: "https://pub.dev" + source: hosted + version: "3.2.11" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + matcher: + dependency: transitive + description: + name: matcher + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" + source: hosted + version: "0.12.15" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + pedantic: + dependency: "direct dev" + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" + source: hosted + version: "2.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=3.0.0-0 <4.0.0" + flutter: ">=1.20.0" diff --git a/pangea_packages/fcm_shared_isolate/pubspec.yaml b/pangea_packages/fcm_shared_isolate/pubspec.yaml new file mode 100644 index 000000000..d43abe16c --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/pubspec.yaml @@ -0,0 +1,30 @@ +name: fcm_shared_isolate +description: Firebase Messaging Plugin for Flutter supporting shared isolate +homepage: https://famedly.com +version: 0.1.0 +repository: https://gitlab.com/famedly/libraries/fcm_shared_isolate +issue_tracker: https://gitlab.com/famedly/libraries/fcm_shared_isolate/issues + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + firebase_core: ^2.4.1 + firebase_messaging: ^14.2.1 + flutter: + sdk: flutter + +dev_dependencies: + pedantic: ^1.11.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: com.famedly.fcm_shared_isolate + pluginClass: FcmSharedIsolatePlugin + ios: + pluginClass: FcmSharedIsolatePlugin diff --git a/pangea_packages/fcm_shared_isolate/test/fcm_shared_isolate_test.dart b/pangea_packages/fcm_shared_isolate/test/fcm_shared_isolate_test.dart new file mode 100644 index 000000000..ca85293d8 --- /dev/null +++ b/pangea_packages/fcm_shared_isolate/test/fcm_shared_isolate_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const channel = MethodChannel('fcm_shared_isolate'); + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('create', () async {}); +} diff --git a/pubspec.lock b/pubspec.lock index 7155d6332..b1cfd5f7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "61.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "7bcb5c5d62b3907fb4a269c0f0843df46760d38e12829a715f2ff1fb492f19ef" + url: "https://pub.dev" + source: hosted + version: "1.3.10" adaptive_dialog: dependency: "direct main" description: @@ -177,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.2" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b74247fad72c171381dbe700ca17da24deac637ab6d43c343b42867acb95c991 + url: "https://pub.dev" + source: hosted + version: "3.0.6" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" console: dependency: transitive description: @@ -193,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + country_picker: + dependency: "direct main" + description: + name: country_picker + sha256: e440350c2f6397955499d5a647cc90599ba3048651c6858370483f55c346b525 + url: "https://pub.dev" + source: hosted + version: "2.0.21" cross_file: dependency: transitive description: @@ -217,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.3" + csv: + dependency: "direct main" + description: + name: csv + sha256: "63ed2871dd6471193dffc52c0e6c76fb86269c00244d244297abbb355c84a86e" + url: "https://pub.dev" + source: hosted + version: "5.1.1" cupertino_icons: dependency: "direct main" description: @@ -345,6 +385,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.4" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -353,6 +401,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fcm_shared_isolate: + dependency: "direct main" + description: + path: "pangea_packages/fcm_shared_isolate" + relative: true + source: path + version: "0.1.0" ffi: dependency: transitive description: @@ -409,6 +464,86 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: fd94117b8160022a57af063dcd856fae2d9a29b6e3bbfcb078e77b87ccecbd3f + url: "https://pub.dev" + source: hosted + version: "10.6.2" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: "42bb0ffc4087dbbfdc7e89514c1b86e55fbbdebd42fdb59efda05f8dd2606a62" + url: "https://pub.dev" + source: hosted + version: "3.7.4" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: ad82db058df608974900ee90afac88fa1cc1d2079bfb62f780d1ad7df6505161 + url: "https://pub.dev" + source: hosted + version: "0.5.5+4" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "37299e4907391d7fac8c7ea059bb3292768cc07b72b6c6c777675cc58da2ef4d" + url: "https://pub.dev" + source: hosted + version: "2.20.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0631a2ec971dbc540275e2fa00c3a8a2676f0a7adbc3c197d6fba569db689d97" + url: "https://pub.dev" + source: hosted + version: "2.8.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: d7b6f9c394f8575598fa94e67220cdd2097a0bc28ce20a3ef2db2da94ede5b47 + url: "https://pub.dev" + source: hosted + version: "14.7.2" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "825356880eb94bf16ea7ef6a71384d2c32cc88143b54ecffed8b3987f7178854" + url: "https://pub.dev" + source: hosted + version: "4.5.11" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "9c82e55c4b470970c77a99c90733adff6be7d5a67251007727b2a98d4f04e0cd" + url: "https://pub.dev" + source: hosted + version: "3.5.11" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: e97c5b850ad056e9b3a85d3afeb44c239a83aa994a90723940dac82234f2efaf + url: "https://pub.dev" + source: hosted + version: "0.61.0" flutter: dependency: "direct main" description: flutter @@ -422,6 +557,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + flutter_app_lock: + dependency: "direct main" + description: + name: flutter_app_lock + sha256: "98890a2a2bc507b2f85165515189750e134921f8f4022ec10bd223033633a3ba" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_blurhash: dependency: "direct main" description: @@ -438,6 +581,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_driver: dependency: transitive description: flutter @@ -681,7 +832,7 @@ packages: source: hosted version: "2.0.0" flutter_svg: - dependency: transitive + dependency: "direct main" description: name: flutter_svg sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" @@ -730,6 +881,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.40" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -783,6 +942,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.6" + get: + dependency: transitive + description: + name: get + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" + source: hosted + version: "4.6.5" get_it: dependency: transitive description: @@ -791,6 +958,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.6.0" + get_storage: + dependency: "direct main" + description: + name: get_storage + sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2" + url: "https://pub.dev" + source: hosted + version: "2.1.1" glob: dependency: transitive description: @@ -803,10 +978,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2aa884667eeda3a1c461f31e72af1f77984ab0f29450d8fb12ec1f7bc53eea14" + sha256: e156bc1b2088eb5ece9351bccd48c3e1719a4858eacbd44e59162e98a68205d1 url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "12.0.1" highlighter: dependency: transitive description: @@ -943,6 +1118,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.0" + in_app_purchase: + dependency: "direct main" + description: + name: in_app_purchase + sha256: bdda02b5b11b56d5e29c7f0c57c433db3452b0c8ce1c37cbfcf1de52946efd9f + url: "https://pub.dev" + source: hosted + version: "3.1.11" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + sha256: "3eeff5e51306e1916dda31eb5647b52da8e71ba81ffd9e4d840ee89986d4bf22" + url: "https://pub.dev" + source: hosted + version: "0.3.0+14" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + sha256: "5168afbc54f406f741252b66d41872c1193a0066a6edcb587176290b92e2d537" + url: "https://pub.dev" + source: hosted + version: "1.3.6" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + sha256: "88afd256c7605d431f0ce29d0161f9554851f90ecb92ceb9e18196c4e7858d52" + url: "https://pub.dev" + source: hosted + version: "0.3.6+7" integration_test: dependency: "direct dev" description: flutter @@ -964,6 +1171,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" + jiffy: + dependency: transitive + description: + name: jiffy + sha256: cc1d4b75016a9156c29b5d61f0c9176c3e0fb0580cc5a0e0422b5d2cab3fbfff + url: "https://pub.dev" + source: hosted + version: "6.2.1" js: dependency: transitive description: @@ -1004,6 +1219,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.8" + jwt_decode: + dependency: "direct main" + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" keyboard_shortcuts: dependency: "direct main" description: @@ -1013,6 +1236,14 @@ packages: url: "https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git" source: git version: "0.1.4" + language_tool: + dependency: "direct main" + description: + name: language_tool + sha256: "90ceb6f0a0b57fb3a5b88be82ffd676c90639cd06d622d25f76add30d5a2acd6" + url: "https://pub.dev" + source: hosted + version: "2.1.1" latlong2: dependency: "direct main" description: @@ -1173,6 +1404,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + new_version_plus: + dependency: "direct main" + description: + name: new_version_plus + sha256: "136cd6368ef96eae5ee3efb59f284c6ff61a06c3c08fb6a49f451ac5fa635e1d" + url: "https://pub.dev" + source: hosted + version: "0.0.10" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" olm: dependency: transitive description: @@ -1181,6 +1428,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20 + url: "https://pub.dev" + source: hosted + version: "3.3.2" package_config: dependency: transitive description: @@ -1421,6 +1676,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + purchases_flutter: + dependency: "direct main" + description: + name: purchases_flutter + sha256: "3e1444a87a11b82322ec2bd0f1af3eb0d3431717f1a93cbd4f823e192b5c891f" + url: "https://pub.dev" + source: hosted + version: "5.8.0" qr: dependency: transitive description: @@ -1557,6 +1820,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.5" + sentry: + dependency: transitive + description: + name: sentry + sha256: "80cc1b38c7219c8072c8a79f124838bd668e0bc840e8a98c0fbf9f55a432997d" + url: "https://pub.dev" + source: hosted + version: "7.11.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: "01dffb216873d1d31436bc50585fb222d4e7d8bcefcf84ccc8f145dac546c1d5" + url: "https://pub.dev" + source: hosted + version: "7.11.0" share_plus: dependency: "direct main" description: @@ -1706,6 +1985,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: "9f0a4593f7642b2f106e329734d0e5fc746baf8d0a59495eec586cd0d9ba7d02" + url: "https://pub.dev" + source: hosted + version: "22.2.12" + syncfusion_flutter_xlsio: + dependency: "direct main" + description: + name: syncfusion_flutter_xlsio + sha256: "66b009fce91e10cfa5d9b3cdf2c4aa3fdf7430dab159626f4c67297638da2caf" + url: "https://pub.dev" + source: hosted + version: "22.2.12" + syncfusion_officecore: + dependency: transitive + description: + name: syncfusion_officecore + sha256: "66d0a0faba40f043bba4ef102474213d3145ffeca2b3f2351a98c0cc10079c27" + url: "https://pub.dev" + source: hosted + version: "22.2.12" synchronized: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c4f3f9fe..029263b5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,7 @@ name: fluffychat -description: Chat with your friends. +description: Learn a language while texting your friends. publish_to: none -# On version bump also increase the build number for F-Droid -version: 1.14.5+3520 +version: 1.11.2+3446 environment: sdk: ">=3.0.0 <4.0.0" @@ -16,6 +15,9 @@ dependencies: callkeep: ^0.3.2 chewie: ^1.3.6 collection: ^1.16.0 + connectivity_plus: ^3.0.2 + country_picker: ^2.0.20 + csv: ^5.0.2 cupertino_icons: any desktop_drop: ^0.4.0 desktop_lifecycle: ^0.1.0 @@ -27,11 +29,19 @@ dependencies: emojis: ^0.9.9 #fcm_shared_isolate: ^0.1.0 file_picker: ^5.3.0 + fl_chart: ^0.61.0 + firebase_analytics: ^10.2.1 + firebase_core: ^2.10.0 + firebase_messaging: ^14.4.1 flutter: sdk: flutter flutter_app_badger: ^1.5.0 + flutter_app_lock: ^3.0.0 flutter_blurhash: ^0.7.0 flutter_cache_manager: ^3.3.0 + flutter_dotenv: ^5.0.2 + fcm_shared_isolate: + path: pangea_packages/fcm_shared_isolate flutter_foreground_task: ^6.0.0+1 flutter_highlighter: ^0.1.1 flutter_html: ^3.0.0-beta.2 @@ -46,39 +56,49 @@ dependencies: flutter_openssl_crypto: ^0.1.0 flutter_ringtone_player: ^3.1.1 flutter_secure_storage: ^8.0.0 + flutter_svg: ^2.0.0+1 flutter_typeahead: ^4.3.2 flutter_web_auth_2: ^2.1.1 flutter_webrtc: ^0.9.37 future_loading_dialog: ^0.2.3 geolocator: ^7.6.2 - go_router: ^10.0.0 + get_storage: ^2.1.1 + go_router: ^12.0.1 hive: ^2.2.3 hive_flutter: ^1.1.0 http: ^0.13.4 image_picker: ^1.0.0 + in_app_purchase: ^3.1.5 intl: any just_audio: ^0.9.30 + jwt_decode: ^0.3.1 keyboard_shortcuts: ^0.1.4 + language_tool: ^2.1.1 latlong2: ^0.8.1 linkify: ^5.0.0 matrix: ^0.22.3 matrix_homeserver_recommendations: ^0.3.0 native_imaging: ^0.1.0 + new_version_plus: ^0.0.10 + open_file: ^3.3.2 package_info_plus: ^4.0.0 pasteboard: ^0.2.0 path_provider: ^2.0.9 permission_handler: ^10.0.0 provider: ^6.0.2 punycode: ^1.0.0 + purchases_flutter: ^5.6.0 qr_code_scanner: ^1.0.0 qr_flutter: ^4.0.0 receive_sharing_intent: ^1.4.5 record: ^4.4.4 scroll_to_index: ^3.0.1 + sentry_flutter: ^7.4.0 share_plus: ^7.0.0 shared_preferences: ^2.2.0 # Pinned because https://github.com/flutter/flutter/issues/118401 slugify: ^2.0.0 swipe_to_action: ^0.2.0 + syncfusion_flutter_xlsio: ^22.2.9 tor_detector_web: ^1.1.0 uni_links: ^0.5.1 unifiedpush: ^5.0.0 @@ -111,7 +131,11 @@ flutter: generate: true uses-material-design: true assets: + - .env + - .env.prod - assets/ + - assets/pangea/ + - assets/pangea/bot_faces/ - assets/sounds/ - assets/js/ - assets/js/package/ @@ -130,12 +154,21 @@ flutter: - family: NotoEmoji fonts: - asset: fonts/NotoEmoji/NotoColorEmoji.ttf + # Pangea + - family: Inconsolata + fonts: + - asset: fonts/Inconsolata/Inconsolata-Regular.ttf + - asset: fonts/Inconsolata/Inconsolata-Light.ttf + style: italic + - asset: fonts/Inconsolata/Inconsolata-Bold.ttf + weight: 700 + # Pangea msix_config: display_name: FluffyChat publisher_display_name: FluffyChat publisher: CN=FluffyChat, O=Head of bad integration tests, L=Matrix, S=Internet, C=EU - identity_name: chat.fluffy.fluffychat + identity_name: com.talktolearn.chat logo_path: assets\logo.png capabilities: internetClient, location, microphone, webcam protocol_activation: https diff --git a/scripts/.credentials b/scripts/.credentials new file mode 100644 index 000000000..2adef4ff8 --- /dev/null +++ b/scripts/.credentials @@ -0,0 +1 @@ +DEEPL_AUTH_KEY=3e0060e0-a0c5-008f-ca16-fd92d80bec5d \ No newline at end of file diff --git a/scripts/build-android-apk.sh b/scripts/build-android-apk.sh new file mode 100644 index 000000000..4b1b641be --- /dev/null +++ b/scripts/build-android-apk.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +flutter pub get +flutter build apk --release +mkdir -p build/android +cp build/app/outputs/apk/release/app-release.apk build/android/ diff --git a/scripts/build-android-debug.sh b/scripts/build-android-debug.sh new file mode 100644 index 000000000..5c2744a4c --- /dev/null +++ b/scripts/build-android-debug.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +flutter build apk --debug diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh old mode 100755 new mode 100644 diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100644 index 000000000..e1df0e603 --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,5 @@ +#!/bin/sh -ve +flutter config --enable-linux-desktop +flutter clean +flutter pub get +flutter build linux --release -v diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh old mode 100755 new mode 100644 diff --git a/scripts/build-olm-windows.sh b/scripts/build-olm-windows.sh old mode 100755 new mode 100644 diff --git a/scripts/build-web.sh b/scripts/build-web.sh new file mode 100644 index 000000000..e4c85b254 --- /dev/null +++ b/scripts/build-web.sh @@ -0,0 +1,5 @@ +#!/bin/sh -ve +flutter config --enable-web +flutter clean +flutter pub get +flutter build web --release --verbose --source-maps --dart-define=SENTRY_RELEASE=$CI_COMMIT_SHA diff --git a/scripts/code_analyze.sh b/scripts/code_analyze.sh new file mode 100644 index 000000000..c0249a7c5 --- /dev/null +++ b/scripts/code_analyze.sh @@ -0,0 +1,8 @@ +#!/bin/sh -ve +flutter pub get +flutter pub run import_sorter:main --no-comments --exit-if-changed +flutter format lib/ test/ --set-exit-if-changed +git apply ./scripts/enable-android-google-services.patch +flutter pub get +flutter analyze +flutter pub run dart_code_metrics:metrics lib -r gitlab > code-quality-report.json || true \ No newline at end of file diff --git a/scripts/create_fdroid_repos.sh b/scripts/create_fdroid_repos.sh old mode 100755 new mode 100644 diff --git a/scripts/download-web-stable.sh b/scripts/download-web-stable.sh new file mode 100644 index 000000000..8467d1bf0 --- /dev/null +++ b/scripts/download-web-stable.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +GITLAB_PROJECT_ID="16112282" + +PIPELINE="$(curl https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/pipelines\?scope=tags\&status=success\&order_by=updated_at | jq '.[].id' | head -n1)" +JOB="$(curl https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/pipelines/${PIPELINE}/jobs | jq -r '.[] | select(.name == "build_web").id')" + +wget --output-document web.zip https://gitlab.com/api/v4/projects/${GITLAB_PROJECT_ID}/jobs/${JOB}/artifacts + +unzip web.zip + +mv build/web stable diff --git a/scripts/generate_command_hints_glue.sh b/scripts/generate_command_hints_glue.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-check-release-build.sh b/scripts/integration-check-release-build.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-create-environment-variables.sh b/scripts/integration-create-environment-variables.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-prepare-alpine.sh b/scripts/integration-prepare-alpine.sh new file mode 100644 index 000000000..f4a57b6d5 --- /dev/null +++ b/scripts/integration-prepare-alpine.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +apk update && apk add docker drill grep \ No newline at end of file diff --git a/scripts/integration-prepare-homeserver.sh b/scripts/integration-prepare-homeserver.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-prepare-host.sh b/scripts/integration-prepare-host.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-server-conduit.sh b/scripts/integration-server-conduit.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-server-dendrite.sh b/scripts/integration-server-dendrite.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-server-synapse.sh b/scripts/integration-server-synapse.sh old mode 100755 new mode 100644 diff --git a/scripts/integration-start-avd.sh b/scripts/integration-start-avd.sh old mode 100755 new mode 100644 diff --git a/scripts/open-mr.sh b/scripts/open-mr.sh new file mode 100644 index 000000000..4af2c7128 --- /dev/null +++ b/scripts/open-mr.sh @@ -0,0 +1,37 @@ +#!/bin/bash -ve + +# source: https://about.gitlab.com/blog/2017/09/05/how-to-automatically-create-a-new-mr-on-gitlab-with-gitlab-ci/ + +# Extract the host where the server is running, and add the URL to the APIs +[[ $HOST =~ ^https?://[^/]+ ]] && HOST="${BASH_REMATCH[0]}/api/v4/projects/" + +# Look which is the default branch +TARGET_BRANCH=`curl --silent "${HOST}${CI_PROJECT_ID}" --header "PRIVATE-TOKEN:${PRIVATE_TOKEN}" | python3 -c "import sys, json; print(json.load(sys.stdin)['default_branch'])"`; + +# The description of our new MR, we want to remove the branch after the MR has +# been closed +BODY="{ + \"id\": ${CI_PROJECT_ID}, + \"source_branch\": \"${UPDATE_BRANCH}\", + \"target_branch\": \"${TARGET_BRANCH}\", + \"remove_source_branch\": true, + \"title\": \"chore: automated dependency update\" +}"; + +# Require a list of all the merge request and take a look if there is already +# one with the same source branch +LISTMR=`curl --silent "${HOST}${CI_PROJECT_ID}/merge_requests?state=opened" --header "PRIVATE-TOKEN:${PRIVATE_TOKEN}"`; +COUNTBRANCHES=`echo ${LISTMR} | grep -o "\"source_branch\":\"${UPDATE_BRANCH}\"" | wc -l`; + +# No MR found, let's create a new one +if [ ${COUNTBRANCHES} -eq "0" ]; then + curl -X POST "${HOST}${CI_PROJECT_ID}/merge_requests" \ + --header "PRIVATE-TOKEN:${PRIVATE_TOKEN}" \ + --header "Content-Type: application/json" \ + --data "${BODY}"; + + echo "Opened a new dependency update MR." + exit; +fi + +echo "No new merge request opened."; diff --git a/scripts/prepare-android-release.sh b/scripts/prepare-android-release.sh old mode 100755 new mode 100644 diff --git a/scripts/prepare-fdroid.sh b/scripts/prepare-fdroid.sh old mode 100755 new mode 100644 diff --git a/scripts/prepare-macos.sh b/scripts/prepare-macos.sh old mode 100755 new mode 100644 diff --git a/scripts/prepare-web.sh b/scripts/prepare-web.sh old mode 100755 new mode 100644 diff --git a/scripts/release-ios-testflight.sh b/scripts/release-ios-testflight.sh old mode 100755 new mode 100644 diff --git a/scripts/release-playstore-beta.sh b/scripts/release-playstore-beta.sh new file mode 100644 index 000000000..7d4dcd4b0 --- /dev/null +++ b/scripts/release-playstore-beta.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +flutter pub get +flutter build appbundle --target-platform android-arm,android-arm64,android-x64 +mkdir -p build/android +cp build/app/outputs/bundle/release/app-release.aab build/android/ +cd android +bundle install +bundle update fastlane +bundle exec fastlane deploy_internal_test +cd .. diff --git a/scripts/release-playstore.sh b/scripts/release-playstore.sh new file mode 100644 index 000000000..f78d38cb5 --- /dev/null +++ b/scripts/release-playstore.sh @@ -0,0 +1,9 @@ +#!/bin/sh -ve +RELEASE_TYPE=$(echo $CI_COMMIT_TAG | grep -oE "[a-z]+") +cd android +if [ "$RELEASE_TYPE" = "rc" ]; then + bundle exec fastlane deploy_candidate +else + bundle exec fastlane deploy_release +fi +cd .. diff --git a/scripts/temp.49243.json b/scripts/temp.49243.json new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/translate.sh b/scripts/translate.sh new file mode 100644 index 000000000..425d95352 --- /dev/null +++ b/scripts/translate.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# 1. Define DeepL credentials +DEEPL_AUTH_KEY=$(awk -F "=" '/DEEPL_AUTH_KEY/ {print $2}' ./.credentials) +DEEPL_API_URL="https://api.deepl.com/v2/translate" +TARGET_LANG="es" + +# 2. Extract missing translation keys +MISSING_KEYS=$(jq -r '.es[]' ../needed-translations.txt) + +# 3. Get English copy for missing keys and translate them +for key in $MISSING_KEYS; do + EN_COPY=$(jq -r ".[\"$key\"]" ../assets/l10n/intl_en.arb) + + # 4. Call DeepL for the translations + TRANSLATED_TEXT=$(curl -s -X POST "${DEEPL_API_URL}" \ + -H "Authorization: DeepL-Auth-Key ${DEEPL_AUTH_KEY}" \ + -d "text=${EN_COPY}" \ + -d "target_lang=${TARGET_LANG}" | jq -r '.translations[0].text') + + # 5. Save them to the Spanish translation file + jq ".[\"$key\"] = \"$TRANSLATED_TEXT\"" ../assets/l10n/intl_es.arb > temp.json && mv temp.json ../assets/l10n/intl_$TARGET_LANG.arb + echo "Translated $key: $TRANSLATED_TEXT" +done + +echo "Translations saved to ../assets/l10n/intl_$TARGET_LANG.arb" \ No newline at end of file diff --git a/scripts/update-dependencies.sh b/scripts/update-dependencies.sh old mode 100755 new mode 100644 diff --git a/scripts/upload-sentry.sh b/scripts/upload-sentry.sh new file mode 100644 index 000000000..569d1aad5 --- /dev/null +++ b/scripts/upload-sentry.sh @@ -0,0 +1,22 @@ +#!/bin/sh -ve + +# Build a release version of the app for a platform and upload symbols +OUTPUT_FOLDER_WEB=./build/web/ +SENTRY_RELEASE=$CI_COMMIT_SHA +SENTRY_PROJECT="client" +SENTRY_ORG="pangea-chat" + +echo "[run] Uploading sourcemaps for $SENTRY_RELEASE" +sentry-cli releases new $SENTRY_RELEASE +sentry-cli releases set-commits $CI_COMMIT_SHA --auto +sentry-cli releases files $SENTRY_RELEASE upload-sourcemaps . \ + --ext dart \ + --rewrite + +(cd $OUTPUT_FOLDER_WEB +sentry-cli releases files $SENTRY_RELEASE upload-sourcemaps . \ + --ext map \ + --ext js \ + --rewrite) + +sentry-cli releases finalize $SENTRY_RELEASE diff --git a/snap/gui/fluffychat.desktop b/snap/gui/fluffychat.desktop deleted file mode 100755 index 886148753..000000000 --- a/snap/gui/fluffychat.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=FluffyChat -GenericName=Matrix Client -Comment=Chat with your friends -Exec=fluffychat -Icon=${SNAP}/meta/gui/fluffychat.png -Terminal=false -Type=Application -Categories=Network;Chat;InstantMessaging; diff --git a/snap/gui/fluffychat.png b/snap/gui/fluffychat.png deleted file mode 100644 index e00764939..000000000 Binary files a/snap/gui/fluffychat.png and /dev/null differ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml deleted file mode 100644 index 45399c3a2..000000000 --- a/snap/snapcraft.yaml +++ /dev/null @@ -1,111 +0,0 @@ -name: fluffychat -title: FluffyChat -base: core22 -version: git -license: AGPL-3.0 -summary: The cutest messenger in the Matrix network -description: | - FluffyChat is an open source, nonprofit and cute matrix messenger app. The app is easy to use but secure and decentralized. - - - ## 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 - - Hides complexity of Matrix IDs behind simple QR codes - - Custom emotes and stickers - - Video calls via sharing links to Jitsi - - Spaces - - Compatible with Element, Nheko, NeoChat and all other Matrix apps - - End to end encryption - - Emoji verification & cross signing - - And much more... - - - ## FluffyChat comes with a dream - - Imagine a world where everyone can choose the messenger they like and is still able to chat with all of their friends. - - A world where there are no companies spying on you when you send selfies to friends and lovers. - - And a world where apps are made for fluffyness and not for profit. ♥ - - Join the community: https://matrix.to/#/#fluffychat:matrix.org - Website: http://fluffychat.im - Microblog: https://mastodon.art/@krille - -grade: stable -confinement: strict - -architectures: - - build-on: amd64 - - build-on: arm64 - -parts: - olm: - plugin: cmake - cmake-parameters: - - -DCMAKE_INSTALL_PREFIX=/usr - source: https://gitlab.matrix.org/matrix-org/olm.git - source-type: git - source-tag: '3.2.14' - build-packages: - - g++ - - zenity-integration: - plugin: nil - stage-snaps: - - zenity-integration - - fluffychat: - plugin: flutter - source: . - override-build: | - # Workaround for Flutter build error: - rm -rf build - craftctl default - build-packages: - - libjsoncpp-dev - - curl - stage-packages: - - libsecret-1-dev - - libjsoncpp-dev - -slots: - dbus-svc: - interface: dbus - bus: session - name: chat.fluffy.fluffychat - -apps: - fluffychat: - command: fluffychat - extensions: [gnome] - plugs: - - audio-playback - - desktop - - desktop-legacy - - home - - network - - network-manager - - network-manager-observe - - opengl - - removable-media - - browser-support - - password-manager-service - slots: - - dbus-svc - # Workaround for: - # https://github.com/flutter-webrtc/flutter-webrtc/issues/1212#issuecomment-1611899344 - environment: - XDG_DATA_HOME: $SNAP_USER_DATA - XDG_DATA_DIRS: $SNAP/usr/share - GDK_GL: gles - LD_LIBRARY_PATH: "$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET" \ No newline at end of file diff --git a/test/choreographer_test.dart b/test/choreographer_test.dart new file mode 100644 index 000000000..e69de29bb diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/web/index.html b/web/index.html index 7ebfd6128..f63fe3bae 100644 --- a/web/index.html +++ b/web/index.html @@ -24,13 +24,20 @@ - + + + + - FluffyChat + + + Pangea Chat + + @@ -45,9 +52,13 @@ - + +
+
+ +
+
+
+
diff --git a/web/splash/splash.js b/web/splash/splash.js new file mode 100644 index 000000000..898c454fb --- /dev/null +++ b/web/splash/splash.js @@ -0,0 +1,7 @@ +function removeSplashFromWeb() { + const elem = document.getElementById("splash"); + if (elem) { + elem.remove(); + } + document.body.style.background = "transparent"; +} diff --git a/web/splash/style.css b/web/splash/style.css index 455b79370..edfcec032 100644 --- a/web/splash/style.css +++ b/web/splash/style.css @@ -1,6 +1,7 @@ -body, html { - margin:0; - height:100%; +body, +html { + margin: 0; + height: 100%; background: #ffffff; background-image: url("img/light-background.png"); background-size: 100% 100%; @@ -16,22 +17,26 @@ body, html { } .contain { - display:block; - width:100%; height:100%; + display: block; + width: 100%; + height: 100%; object-fit: contain; } .stretch { - display:block; - width:100%; height:100%; + display: block; + width: 100%; + height: 100%; } .cover { - display:block; - width:100%; height:100%; + display: block; + width: 100%; + height: 100%; object-fit: cover; } +/* #Pangea @media (prefers-color-scheme: dark) { body { margin:0; @@ -41,3 +46,45 @@ body, html { background-size: 100% 100%; } } +*/ + +@media (prefers-color-scheme: dark) { + body { + margin: 0; + height: 100%; + background: #000000; + background-size: 100% 100%; + } +} + +.image { + width: 100%; +} + +.custom-loader { + --d: 88px; + width: 3px; + height: 3px; + position: absolute; + top: 85px; + left: 85px; + border-radius: 100%; + color: #8C5EE8; + box-shadow: + calc(1*var(--d)) calc(0*var(--d)) 0 0, + calc(0.707*var(--d)) calc(0.707*var(--d)) 0 2px, + calc(0*var(--d)) calc(1*var(--d)) 0 4px, + calc(-0.707*var(--d)) calc(0.707*var(--d)) 0 6px, + calc(-1*var(--d)) calc(0*var(--d)) 0 8px, + calc(-0.707*var(--d)) calc(-0.707*var(--d))0 10px, + calc(0*var(--d)) calc(-1*var(--d)) 0 12px; + animation: s7 1s infinite steps(8); +} + +@keyframes s7 { + 100% { + transform: rotate(1turn) + } +} + +/* Pangea# */ \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c56ef092b..3f7add542 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,21 +6,26 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); DesktopLifecyclePluginRegisterWithRegistrar( @@ -31,6 +36,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("EmojiPickerFlutterPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( @@ -41,6 +48,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + SentryFlutterPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SentryFlutterPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 27cc5bc0d..f0c310aec 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,16 +3,19 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus desktop_drop desktop_lifecycle dynamic_color emoji_picker_flutter file_selector_windows + firebase_core flutter_secure_storage_windows flutter_webrtc pasteboard permission_handler_windows record_windows + sentry_flutter share_plus url_launcher_windows window_to_front