Merge branch 'main' into 5244-grammar-practice-ui-updates

This commit is contained in:
Ava Shilling 2026-01-21 14:14:22 -05:00
commit be9ef801a9
94 changed files with 1183 additions and 745 deletions

View file

@ -1,6 +1,6 @@
{
"@@locale": "ar",
"@@last_modified": "2026-01-20 12:31:24.671375",
"@@last_modified": "2026-01-21 13:54:18.388293",
"about": "حول",
"@about": {
"type": "String",
@ -11107,5 +11107,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "صوت بوت بانجيا",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "تم إرسال طلبك إلى إدارة الدورة! سيتم السماح لك بالدخول إذا وافقوا.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1911,7 +1911,7 @@
"playWithAI": "Пакуль гуляйце з ШІ",
"courseStartDesc": "Pangea Bot гатовы да працы ў любы час!\n\n...але навучанне лепш з сябрамі!",
"@@locale": "be",
"@@last_modified": "2026-01-20 12:31:06.570011",
"@@last_modified": "2026-01-21 13:54:07.402936",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11989,5 +11989,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Голас Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Ваш запыт быў адпраўлены адміністрацыі курса! Вы будзеце дапушчаны, калі яны зацвердзяць.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:49.464406",
"@@last_modified": "2026-01-21 13:54:31.328626",
"about": "সম্পর্কে",
"@about": {
"type": "String",
@ -11994,5 +11994,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "প্যাঙ্গিয়া বটের কণ্ঠ",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "আপনার অনুরোধ কোর্স প্রশাসকের কাছে পাঠানো হয়েছে! তারা অনুমোদন করলে আপনাকে প্রবেশ করতে দেওয়া হবে।",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4279,7 +4279,7 @@
"joinPublicTrip": "མི་ཚེས་ལ་ལོག་འབད།",
"startOwnTrip": "ངེད་རང་གི་ལོག་ལ་སྦྱོར་བཅོས།",
"@@locale": "bo",
"@@last_modified": "2026-01-20 12:31:44.969872",
"@@last_modified": "2026-01-21 13:54:28.767499",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -10644,5 +10644,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot voz",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Yor requst has been sent to course admin! Yu'll be let in if dey approve.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:09.126351",
"@@last_modified": "2026-01-21 13:54:08.647836",
"about": "Quant a",
"@about": {
"type": "String",
@ -10914,5 +10914,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Veu del bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "La teva sol·licitud s'ha enviat a l'administrador del curs! Et deixaran entrar si ho aproven.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "cs",
"@@last_modified": "2026-01-20 12:31:00.323945",
"@@last_modified": "2026-01-21 13:54:04.800051",
"about": "O aplikaci",
"@about": {
"type": "String",
@ -11497,5 +11497,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Hlas Pangea Bota",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Vaše žádost byla odeslána administrátorovi kurzu! Budete vpuštěni, pokud ji schválí.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1930,7 +1930,7 @@
"playWithAI": "Leg med AI for nu",
"courseStartDesc": "Pangea Bot er klar til at starte når som helst!\n\n...men læring er bedre med venner!",
"@@locale": "da",
"@@last_modified": "2026-01-20 12:30:10.761209",
"@@last_modified": "2026-01-21 13:53:34.885532",
"@aboutHomeserver": {
"type": "String",
"placeholders": {
@ -11951,5 +11951,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot stemme",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Din anmodning er sendt til kursusadministratoren! Du vil blive lukket ind, hvis de godkender.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "de",
"@@last_modified": "2026-01-20 12:30:47.434524",
"@@last_modified": "2026-01-21 13:53:56.595777",
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
@ -10897,5 +10897,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot Stimme",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Ihre Anfrage wurde an den Kursadministrator gesendet! Sie werden eingelassen, wenn sie zustimmen.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4456,7 +4456,7 @@
"playWithAI": "Παίξτε με την Τεχνητή Νοημοσύνη προς το παρόν",
"courseStartDesc": "Ο Pangea Bot είναι έτοιμος να ξεκινήσει οποιαδήποτε στιγμή!\n\n...αλλά η μάθηση είναι καλύτερη με φίλους!",
"@@locale": "el",
"@@last_modified": "2026-01-20 12:31:59.503296",
"@@last_modified": "2026-01-21 13:54:37.783088",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11948,5 +11948,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Φωνή Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Το αίτημά σας έχει σταλεί στον διαχειριστή του μαθήματος! Θα σας επιτρέψουν να μπείτε αν το εγκρίνουν.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -5056,5 +5056,7 @@
"constructUseIncGEDesc": "Incorrect grammar error practice",
"fillInBlank": "Fill in the blank with the correct choice",
"learn": "Learn",
"languageUpdated": "Target language updated!"
"languageUpdated": "Target language updated!",
"voiceDropdownTitle": "Pangea Bot voice",
"knockDesc": "Your request has been sent to course admin! You'll be let in if they approve."
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:32:07.490560",
"@@last_modified": "2026-01-21 13:54:42.193114",
"about": "Prio",
"@about": {
"type": "String",
@ -11979,5 +11979,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voĉo de Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Via peto estis sendita al la kursa administranto! Vi estos enirita se ili aprobas.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "es",
"@@last_modified": "2026-01-20 12:29:59.898184",
"@@last_modified": "2026-01-21 13:53:29.857658",
"about": "Acerca de",
"@about": {
"type": "String",
@ -8124,5 +8124,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voz del bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "¡Tu solicitud ha sido enviada al administrador del curso! Te dejarán entrar si la aprueban.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "et",
"@@last_modified": "2026-01-20 12:30:45.162738",
"@@last_modified": "2026-01-21 13:53:55.586614",
"about": "Rakenduse teave",
"@about": {
"type": "String",
@ -11161,5 +11161,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Boti hääl",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Teie taotlus on saadetud kursuse administraatorile! Teid lastakse sisse, kui nad heaks kiidavad.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "eu",
"@@last_modified": "2026-01-20 12:30:40.144354",
"@@last_modified": "2026-01-21 13:53:53.122879",
"about": "Honi buruz",
"@about": {
"type": "String",
@ -10890,5 +10890,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot ahotsa",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Zure eskaera ikastaroaren administratzaileari bidali zaio! Onartzen badute, sartuko zara.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:52.231221",
"@@last_modified": "2026-01-21 13:54:33.096668",
"repeatPassword": "تکرار رمزعبور",
"@repeatPassword": {},
"about": "درباره",
@ -11622,5 +11622,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "صدای ربات پانژیا",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "درخواست شما به مدیر دوره ارسال شده است! اگر آنها تأیید کنند، شما وارد خواهید شد.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4009,7 +4009,7 @@
"playWithAI": "Leiki tekoälyn kanssa nyt",
"courseStartDesc": "Pangea Bot on valmis milloin tahansa!\n\n...mutta oppiminen on parempaa ystävien kanssa!",
"@@locale": "fi",
"@@last_modified": "2026-01-20 12:30:08.099637",
"@@last_modified": "2026-01-21 13:53:33.564588",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11513,5 +11513,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Botin ääni",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Pyyntösi on lähetetty kurssin ylläpitäjälle! Sinut päästetään sisään, jos he hyväksyvät sen.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2787,7 +2787,7 @@
"selectAll": "Piliin lahat",
"deselectAll": "Huwag piliin lahat",
"@@locale": "fil",
"@@last_modified": "2026-01-20 12:31:19.884620",
"@@last_modified": "2026-01-21 13:54:14.817261",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
@ -11866,5 +11866,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Boses ng Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Ang iyong kahilingan ay naipadala sa admin ng kurso! Papayagan ka nilang pumasok kung sila ay mag-aapruba.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "fr",
"@@last_modified": "2026-01-20 12:32:20.398562",
"@@last_modified": "2026-01-21 13:54:48.318824",
"about": "À propos",
"@about": {
"type": "String",
@ -11214,5 +11214,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voix du bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Votre demande a été envoyée à l'administrateur du cours ! Vous serez admis s'ils approuvent.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4517,7 +4517,7 @@
"playWithAI": "Imir le AI faoi láthair",
"courseStartDesc": "Tá Bot Pangea réidh chun dul am ar bith!\n\n...ach is fearr foghlaim le cairde!",
"@@locale": "ga",
"@@last_modified": "2026-01-20 12:32:18.033824",
"@@last_modified": "2026-01-21 13:54:46.978792",
"@customReaction": {
"type": "String",
"placeholders": {}
@ -10888,5 +10888,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "guth Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Tá do hiarratas curtha chuig an riarachán cúrsa! Cuirfear isteach thú má cheadaíonn siad é.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "gl",
"@@last_modified": "2026-01-20 12:30:05.234280",
"@@last_modified": "2026-01-21 13:53:31.619299",
"about": "Acerca de",
"@about": {
"type": "String",
@ -10887,5 +10887,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voz do bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "A túa solicitude foi enviada ao administrador do curso! Serás admitido se a aproban.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:30:31.884050",
"@@last_modified": "2026-01-21 13:53:47.592162",
"about": "אודות",
"@about": {
"type": "String",
@ -11939,5 +11939,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "קול של פנגיאה בוט",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "הבקשה שלך נשלחה למנהל הקורס! תורשה להיכנס אם הם יאשרו.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4483,7 +4483,7 @@
"playWithAI": "अभी के लिए एआई के साथ खेलें",
"courseStartDesc": "पैंजिया बॉट कभी भी जाने के लिए तैयार है!\n\n...लेकिन दोस्तों के साथ सीखना बेहतर है!",
"@@locale": "hi",
"@@last_modified": "2026-01-20 12:32:05.240015",
"@@last_modified": "2026-01-21 13:54:40.850433",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11975,5 +11975,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "पैंगिया बॉट की आवाज़",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "आपका अनुरोध पाठ्यक्रम प्रशासन को भेज दिया गया है! यदि वे स्वीकृत करते हैं, तो आपको अंदर जाने दिया जाएगा।",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "hr",
"@@last_modified": "2026-01-20 12:30:29.823787",
"@@last_modified": "2026-01-21 13:53:46.524141",
"about": "Informacije",
"@about": {
"type": "String",
@ -11262,5 +11262,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot glas",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Vaš zahtjev je poslan administratoru tečaja! Bit ćete primljeni ako odobre.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "hu",
"@@last_modified": "2026-01-20 12:30:13.762355",
"@@last_modified": "2026-01-21 13:53:36.742109",
"about": "Névjegy",
"@about": {
"type": "String",
@ -10891,5 +10891,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot hang",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "A kérésed el lett küldve a kurzus adminisztrátorának! Be fogsz engedni, ha jóváhagyják.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1958,7 +1958,7 @@
"playWithAI": "Joca con le IA pro ora",
"courseStartDesc": "Pangea Bot es preste a comenzar a qualunque momento!\n\n...ma apprender es melior con amicos!",
"@@locale": "ia",
"@@last_modified": "2026-01-20 12:30:35.012898",
"@@last_modified": "2026-01-21 13:53:49.028067",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11968,5 +11968,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voix du bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Tua peticio est missa ad administratorem cursuum! Te admittent si illi approbant.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:30:15.788809",
"@@last_modified": "2026-01-21 13:53:37.899898",
"setAsCanonicalAlias": "Atur sebagai alias utama",
"@setAsCanonicalAlias": {
"type": "String",
@ -10881,5 +10881,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Suara Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Permintaan Anda telah dikirim ke admin kursus! Anda akan diizinkan masuk jika mereka menyetujuinya.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4372,7 +4372,7 @@
"playWithAI": "Joca con AI pro ora",
"courseStartDesc": "Pangea Bot es preste a partir a qualunque momento!\n\n...ma apprender es melior con amicos!",
"@@locale": "ie",
"@@last_modified": "2026-01-20 12:30:26.700297",
"@@last_modified": "2026-01-21 13:53:45.416677",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11864,5 +11864,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot guth",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Tua iarrtas a chaidh a chur gu rianachd a' chùrsa! Thèid thu a leigeil a-steach ma tha iad a' freagairt gu math.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:30:55.791406",
"@@last_modified": "2026-01-21 13:54:01.568848",
"about": "Informazioni",
"@about": {
"type": "String",
@ -10893,5 +10893,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voce del bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "La tua richiesta è stata inviata all'amministratore del corso! Sarai ammesso se approvano.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "ja",
"@@last_modified": "2026-01-20 12:32:02.883097",
"@@last_modified": "2026-01-21 13:54:39.359536",
"about": "このアプリについて",
"@about": {
"type": "String",
@ -11680,5 +11680,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "パンゲアボットの声",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "あなたのリクエストはコース管理者に送信されました! 彼らが承認すれば、入ることができます。",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2594,7 +2594,7 @@
"playWithAI": "ამ დროისთვის ითამაშეთ AI-თან",
"courseStartDesc": "Pangea Bot მზადაა ნებისმიერ დროს გასასვლელად!\n\n...მაგრამ სწავლა უკეთესია მეგობრებთან ერთად!",
"@@locale": "ka",
"@@last_modified": "2026-01-20 12:32:12.611848",
"@@last_modified": "2026-01-21 13:54:44.486907",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11920,5 +11920,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "პანჯეა ბოტის ხმა",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "თქვენი მოთხოვნა გაგზავნილია კურსის ადმინისტრატორთან! თქვენ შეგიშვებენ, თუ ისინი დაამტკიცებენ.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:29:56.840054",
"@@last_modified": "2026-01-21 13:53:28.417406",
"about": "소개",
"@about": {
"type": "String",
@ -10998,5 +10998,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "판게아 봇 음성",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "귀하의 요청이 과정 관리자에게 전송되었습니다! 그들이 승인하면 들어갈 수 있습니다.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -3861,7 +3861,7 @@
"playWithAI": "Žaiskite su dirbtiniu intelektu dabar",
"courseStartDesc": "Pangea botas pasiruošęs bet kada pradėti!\n\n...bet mokymasis yra geresnis su draugais!",
"@@locale": "lt",
"@@last_modified": "2026-01-20 12:31:36.164653",
"@@last_modified": "2026-01-21 13:54:22.729769",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11695,5 +11695,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot balsas",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Jūsų prašymas buvo išsiųstas kurso administratoriui! Būsite įleistas, jei jie patvirtins.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4482,7 +4482,7 @@
"playWithAI": "Tagad spēlējiet ar AI",
"courseStartDesc": "Pangea bots ir gatavs jebkurā laikā!\n\n...bet mācīties ir labāk ar draugiem!",
"@@locale": "lv",
"@@last_modified": "2026-01-20 12:31:22.453166",
"@@last_modified": "2026-01-21 13:54:16.883169",
"analyticsInactiveTitle": "Pieprasījumi neaktīviem lietotājiem nevar tikt nosūtīti",
"analyticsInactiveDesc": "Neaktīvi lietotāji, kuri nav pieteikušies kopš šīs funkcijas ieviešanas, neredzēs jūsu pieprasījumu.\n\nPieprasījuma poga parādīsies, kad viņi atgriezīsies. Jūs varat atkārtoti nosūtīt pieprasījumu vēlāk, noklikšķinot uz pieprasījuma pogas viņu vārdā, kad tā būs pieejama.",
"accessRequestedTitle": "Pieprasījums piekļūt analītikai",
@ -10876,5 +10876,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot balss",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Jūsu pieprasījums ir nosūtīts kursa administratoram! Jūs tiksiet iekšā, ja viņi apstiprinās.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:02.821968",
"@@last_modified": "2026-01-21 13:54:06.232198",
"about": "Om",
"@about": {
"type": "String",
@ -11983,5 +11983,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot-stemme",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Din forespørsel har blitt sendt til kursadministratoren! Du vil bli sluppet inn hvis de godkjenner.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:42.507524",
"@@last_modified": "2026-01-21 13:54:27.649909",
"about": "Over ons",
"@about": {
"type": "String",
@ -10890,5 +10890,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot stem",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Je verzoek is verzonden naar de cursusbeheerder! Je wordt toegelaten als ze goedkeuren.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "pl",
"@@last_modified": "2026-01-20 12:31:54.796841",
"@@last_modified": "2026-01-21 13:54:34.451816",
"about": "O aplikacji",
"@about": {
"type": "String",
@ -10888,5 +10888,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Głos bota Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Twoja prośba została wysłana do administratora kursu! Zostaniesz wpuszczony, jeśli ją zatwierdzą.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:30:42.601068",
"@@last_modified": "2026-01-21 13:53:54.148542",
"copiedToClipboard": "Copiada para a área de transferência",
"@copiedToClipboard": {
"type": "String",
@ -11990,5 +11990,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voz do Bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Sua solicitação foi enviada ao administrador do curso! Você será admitido se eles aprovarem.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:30:37.380939",
"@@last_modified": "2026-01-21 13:53:51.587209",
"about": "Sobre",
"@about": {
"type": "String",
@ -11248,5 +11248,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voz do Bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Sua solicitação foi enviada ao administrador do curso! Você será admitido se eles aprovarem.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -3331,7 +3331,7 @@
"selectAll": "Selecionar tudo",
"deselectAll": "Desmarcar tudo",
"@@locale": "pt_PT",
"@@last_modified": "2026-01-20 12:31:13.609297",
"@@last_modified": "2026-01-21 13:54:11.801274",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11919,5 +11919,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Voz do Bot Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Sua solicitação foi enviada ao administrador do curso! Você será admitido se eles aprovarem.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:30:21.408747",
"@@last_modified": "2026-01-21 13:53:41.456181",
"about": "Despre",
"@about": {
"type": "String",
@ -11625,5 +11625,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Vocea Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Cererea ta a fost trimisă administratorului cursului! Vei fi lăsat să intri dacă ei aprobă.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "ru",
"@@last_modified": "2026-01-20 12:32:09.850624",
"@@last_modified": "2026-01-21 13:54:43.229000",
"about": "О проекте",
"@about": {
"type": "String",
@ -10995,5 +10995,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Голос бота Pangea",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Ваш запрос отправлен администратору курса! Вы будете допущены, если они одобрят.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "sk",
"@@last_modified": "2026-01-20 12:30:24.498564",
"@@last_modified": "2026-01-21 13:53:43.706457",
"about": "O aplikácii",
"@about": {
"type": "String",
@ -11974,5 +11974,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Hlas Pangea Bota",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Vaša žiadosť bola odoslaná administrátorovi kurzu! Budete vpustený, ak ju schvália.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2464,7 +2464,7 @@
"playWithAI": "Za zdaj igrajte z AI-jem",
"courseStartDesc": "Pangea Bot je pripravljen kadarkoli!\n\n...ampak je bolje učiti se s prijatelji!",
"@@locale": "sl",
"@@last_modified": "2026-01-20 12:30:51.399294",
"@@last_modified": "2026-01-21 13:53:58.241980",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11971,5 +11971,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Glas Pangea Bota",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Vaša zahteva je bila poslana skrbniku tečaja! Vstopili boste, če jo odobrijo.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:32:14.799697",
"@@last_modified": "2026-01-21 13:54:45.561732",
"about": "О програму",
"@about": {
"type": "String",
@ -11992,5 +11992,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Glas Pangea Bota",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Vaš zahtev je poslat administratoru kursa! Bićete primljeni ako odobre.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:57.066428",
"@@last_modified": "2026-01-21 13:54:35.931933",
"about": "Om",
"@about": {
"type": "String",
@ -11368,5 +11368,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot röst",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Din begäran har skickats till kursadministratören! Du kommer att släppas in om de godkänner.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:40.562260",
"@@last_modified": "2026-01-21 13:54:25.999605",
"acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது",
"@acceptedTheInvitation": {
"type": "String",
@ -11114,5 +11114,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "பாஙேஆ பாட்டின் குரல்",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "உங்கள் கோரிக்கை பாடம் நிர்வாகிக்கு அனுப்பப்பட்டுள்ளது! அவர்கள் ஒப்புதலளித்தால் நீங்கள் உள்ளே அனுமதிக்கப்படுவீர்கள்.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1920,7 +1920,7 @@
"playWithAI": "ఇప్పుడే AI తో ఆడండి",
"courseStartDesc": "పాంజియా బాట్ ఎప్పుడైనా సిద్ధంగా ఉంటుంది!\n\n...కానీ స్నేహితులతో నేర్చుకోవడం మెరుగైనది!",
"@@locale": "te",
"@@last_modified": "2026-01-20 12:31:32.903548",
"@@last_modified": "2026-01-21 13:54:20.897642",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
@ -11979,5 +11979,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "పాంజియా బాట్ శబ్దం",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "మీ అభ్యర్థన కోర్సు నిర్వాహకుడికి పంపబడింది! వారు ఆమోదిస్తే, మీరు లోపలికి రానున్నారు.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4456,7 +4456,7 @@
"playWithAI": "เล่นกับ AI ชั่วคราว",
"courseStartDesc": "Pangea Bot พร้อมที่จะเริ่มต้นได้ทุกเมื่อ!\n\n...แต่การเรียนรู้ดีกว่ากับเพื่อน!",
"@@locale": "th",
"@@last_modified": "2026-01-20 12:31:11.891533",
"@@last_modified": "2026-01-21 13:54:10.758435",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11948,5 +11948,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "เสียงของ Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "คำขอของคุณได้ถูกส่งไปยังผู้ดูแลหลักสูตรแล้ว! คุณจะได้รับอนุญาตให้เข้าหากพวกเขาอนุมัติ.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "tr",
"@@last_modified": "2026-01-20 12:31:28.469826",
"@@last_modified": "2026-01-21 13:54:19.467039",
"about": "Hakkında",
"@about": {
"type": "String",
@ -11112,5 +11112,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot sesi",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Talebiniz kurs yöneticisine gönderildi! Onaylarlarsa içeri alınacaksınız.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "uk",
"@@last_modified": "2026-01-20 12:30:57.869011",
"@@last_modified": "2026-01-21 13:54:03.403456",
"about": "Про застосунок",
"@about": {
"type": "String",
@ -10884,5 +10884,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Голос Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Ваш запит надіслано адміністратору курсу! Ви будете допущені, якщо вони схвалять.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:38.101874",
"@@last_modified": "2026-01-21 13:54:24.252269",
"about": "Giới thiệu",
"@about": {
"type": "String",
@ -6460,5 +6460,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Giọng nói của Pangea Bot",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "Yêu cầu của bạn đã được gửi đến quản trị viên khóa học! Bạn sẽ được cho vào nếu họ chấp thuận.",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1856,7 +1856,7 @@
"selectAll": "全選",
"deselectAll": "取消全選",
"@@locale": "yue",
"@@last_modified": "2026-01-20 12:30:53.854617",
"@@last_modified": "2026-01-21 13:53:59.885092",
"@ignoreUser": {
"type": "String",
"placeholders": {}
@ -11981,5 +11981,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot 聲音",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "你的請求已經發送給課程管理員!如果他們批准,你將被允許進入。",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "zh",
"@@last_modified": "2026-01-20 12:31:47.017242",
"@@last_modified": "2026-01-21 13:54:29.681537",
"about": "关于",
"@about": {
"type": "String",
@ -10881,5 +10881,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "潘吉亚机器人声音",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "您的请求已发送给课程管理员!如果他们批准,您将被允许进入。",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-20 12:31:16.140892",
"@@last_modified": "2026-01-21 13:54:13.165623",
"about": "關於",
"@about": {
"type": "String",
@ -10888,5 +10888,15 @@
"@languageUpdated": {
"type": "String",
"placeholders": {}
},
"voiceDropdownTitle": "Pangea Bot 語音",
"@voiceDropdownTitle": {
"type": "String",
"placeholders": {}
},
"knockDesc": "您的請求已發送給課程管理員!如果他們批准,您將被允許進入。",
"@knockDesc": {
"type": "String",
"placeholders": {}
}
}

View file

@ -496,6 +496,8 @@ class ChatController extends State<ChatPageWithRoom>
if (botAudioEvent == null) return;
final matrix = Matrix.of(context);
if (matrix.voiceMessageEventId.value != null) return;
matrix.voiceMessageEventId.value = botAudioEvent.eventId;
matrix.audioPlayer?.dispose();
matrix.audioPlayer = AudioPlayer();
@ -2009,6 +2011,7 @@ class ChatController extends State<ChatPageWithRoom>
bool showMessageShimmer(Event event) {
if (event.type != EventTypes.Message) return false;
if (!(event.eventId == buttonEventID)) return false;
if (event.messageType == MessageTypes.Text) {
return !InstructionsEnum.clickTextMessages.isToggledOff;
}

View file

@ -13,8 +13,6 @@ import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_roles_event_widget.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_settings_language_icon.dart';
import 'package:fluffychat/pangea/chat/extensions/custom_room_display_extension.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
@ -477,18 +475,6 @@ class Message extends StatelessWidget {
presenceBackgroundColor: wallpaperMode
? Colors.transparent
: null,
// #Pangea
miniIcon:
user.id == BotName.byEnvironment
? BotSettingsLanguageIcon(
user: user,
)
: null,
presenceOffset:
user.id == BotName.byEnvironment
? const Offset(0, 0)
: null,
// Pangea#
);
},
),

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pangea/user/style_settings_repo.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -157,10 +157,16 @@ class SettingsStyleController extends State<SettingsStyle> {
void changeFontSizeFactor(double d) {
setState(() => AppConfig.fontSizeFactor = d);
Matrix.of(context).store.setString(
SettingKeys.fontSizeFactor,
AppConfig.fontSizeFactor.toString(),
);
// #Pangea
// Matrix.of(context).store.setString(
// SettingKeys.fontSizeFactor,
// AppConfig.fontSizeFactor.toString(),
// );
StyleSettingsRepo.setFontSizeFactor(
Matrix.of(context).client.userID!,
AppConfig.fontSizeFactor,
);
// Pangea#
}
@override

View file

@ -6,8 +6,6 @@ import 'package:shimmer/shimmer.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_settings_language_icon.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
@ -70,14 +68,6 @@ class ActivityParticipantIndicator extends StatelessWidget {
name: userId!.localpart,
size: 60.0,
userId: userId,
miniIcon:
room != null && userId == BotName.byEnvironment
? BotSettingsLanguageIcon(user: user!)
: null,
presenceOffset:
room != null && userId == BotName.byEnvironment
? const Offset(0, 0)
: null,
)
: ClipRRect(
borderRadius: BorderRadius.circular(30),

View file

@ -106,27 +106,17 @@ class _VocabChipsState extends State<_VocabChips> with TokenRenderingMixin {
OverlayUtil.showPositionedCard(
overlayKey: target,
context: context,
cardToShow: StatefulBuilder(
builder: (context, setState) => WordZoomWidget(
token: PangeaTokenText(
content: v.lemma,
length: v.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: v.lemma,
type: ConstructTypeEnum.vocab,
category: v.pos,
),
langCode: widget.langCode,
onClose: () {
MatrixState.pAnyState.closeOverlay(target);
setState(() => _selectedVocab = null);
},
onDismissNewWordOverlay: () {
if (mounted) setState(() {});
},
),
cardToShow: _WordCardWrapper(
v: v,
langCode: widget.langCode,
target: target,
onClose: () {
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => setState(() => _selectedVocab = null),
);
}
},
),
transformTargetId: target,
closePrevOverlay: false,
@ -204,3 +194,52 @@ class _VocabChipsState extends State<_VocabChips> with TokenRenderingMixin {
);
}
}
class _WordCardWrapper extends StatefulWidget {
final Vocab v;
final String langCode;
final String target;
final VoidCallback onClose;
const _WordCardWrapper({
required this.v,
required this.langCode,
required this.target,
required this.onClose,
});
@override
State<_WordCardWrapper> createState() => _WordCardWrapperState();
}
class _WordCardWrapperState extends State<_WordCardWrapper> {
@override
void dispose() {
widget.onClose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WordZoomWidget(
token: PangeaTokenText(
content: widget.v.lemma,
length: widget.v.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: widget.v.lemma,
type: ConstructTypeEnum.vocab,
category: widget.v.pos,
),
langCode: widget.langCode,
onClose: () {
MatrixState.pAnyState.closeOverlay(widget.target);
widget.onClose();
},
onDismissNewWordOverlay: () {
if (mounted) setState(() {});
},
);
}
}

View file

@ -97,7 +97,6 @@ class SpaceAnalyticsSummaryModel {
Set<ConstructIdentifier> blockedConstructs,
int numCompletedActivities,
) {
int totalXP = 0;
int numWordsTyped = 0;
int numChoicesCorrect = 0;
int numChoicesIncorrect = 0;
@ -114,7 +113,9 @@ class SpaceAnalyticsSummaryModel {
mergeTable.addConstructsByUses(e.content.uses, blockedConstructs);
for (final use in e.content.uses) {
totalXP += use.xp;
final id = use.identifier;
if (blockedConstructs.contains(id)) continue;
allUses.add(use);
if (use.useType.summaryEnumType ==
@ -132,8 +133,7 @@ class SpaceAnalyticsSummaryModel {
sentEventIds.add(use.metadata.eventId!);
}
final id = use.identifier;
final existing = id.type == ConstructTypeEnum.vocab
final existing = use.identifier.type == ConstructTypeEnum.vocab
? aggregatedVocab[id]
: aggregatedMorph[id];
@ -189,6 +189,10 @@ class SpaceAnalyticsSummaryModel {
}
}
final totalXP = cleanedVocab.values
.fold<int>(0, (sum, entry) => sum + entry.points) +
cleanedMorph.values.fold<int>(0, (sum, entry) => sum + entry.points);
final level = DerivedAnalyticsDataModel.calculateLevelWithXp(totalXP);
final uniqueVocabCount = cleanedVocab.length;
final uniqueMorphCount = cleanedMorph.length;

View file

@ -67,7 +67,7 @@ class SessionLoader extends AsyncLoader<AnalyticsPracticeSessionModel> {
}
class AnalyticsPractice extends StatefulWidget {
static bool bypassExitConfirmation = false;
static bool bypassExitConfirmation = true;
final ConstructTypeEnum type;
const AnalyticsPractice({
@ -201,18 +201,18 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
String choiceTargetId(String choiceId) =>
'${widget.type.name}-choice-card-${choiceId.replaceAll(' ', '_')}';
void _resetActivityState() {
void _clearState() {
activityState.value = const AsyncState.loading();
activityTarget.value = null;
selectedMorphChoice.value = null;
}
void _resetSessionState() {
enableChoicesNotifier.value = true;
progressNotifier.value = 0.0;
_queue.clear();
_choiceTexts.clear();
_choiceEmojis.clear();
activityState.value = const AsyncState.idle();
AnalyticsPractice.bypassExitConfirmation = true;
}
void updateElapsedTime(int seconds) {
@ -239,8 +239,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
Future<void> _onLanguageUpdate() async {
try {
_resetActivityState();
_resetSessionState();
_clearState();
await _analyticsService
.updateDispatcher.constructUpdateStream.stream.first
.timeout(const Duration(seconds: 10));
@ -257,14 +256,14 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
Future<void> _startSession() async {
await _waitForAnalytics();
await _sessionLoader.load();
if (_sessionLoader.isError) return;
progressNotifier.value = _sessionLoader.value!.progress;
await _continueSession();
}
Future<void> reloadSession() async {
_resetActivityState();
_resetSessionState();
_clearState();
_sessionLoader.reset();
await _startSession();
}
@ -324,8 +323,10 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
final activity = await nextActivityCompleter.completer.future;
activityState.value = AsyncState.loaded(activity);
AnalyticsPractice.bypassExitConfirmation = false;
}
} catch (e) {
AnalyticsPractice.bypassExitConfirmation = true;
activityState.value = AsyncState.error(e);
} finally {
_continuing = false;
@ -349,7 +350,9 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
if (!mounted) return;
activityState.value = AsyncState.loaded(res);
AnalyticsPractice.bypassExitConfirmation = false;
} catch (e) {
AnalyticsPractice.bypassExitConfirmation = true;
if (!mounted) return;
activityState.value = AsyncState.error(e);
return;

View file

@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
@ -15,10 +16,17 @@ import 'package:fluffychat/pangea/practice_activities/message_activity_request.d
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/widgets/matrix.dart';
class InsufficientDataException implements Exception {}
class AnalyticsPracticeSessionRepo {
static Future<AnalyticsPracticeSessionModel> get(
ConstructTypeEnum type,
) async {
if (MatrixState.pangeaController.subscriptionController.isSubscribed ==
false) {
throw UnsubscribedException();
}
final r = Random();
final activityTypes = ActivityTypeEnum.analyticsPracticeTypes(type);
@ -67,6 +75,10 @@ class AnalyticsPracticeSessionRepo {
}
}
if (targets.isEmpty) {
throw InsufficientDataException();
}
final session = AnalyticsPracticeSessionModel(
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
userL2: MatrixState.pangeaController.userController.userL2!.langCode,

View file

@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
@ -84,7 +85,9 @@ class AnalyticsPracticeView extends StatelessWidget {
builder: (context, state, __) {
return switch (state) {
AsyncError<AnalyticsPracticeSessionModel>(:final error) =>
ErrorIndicator(message: error.toString()),
ErrorIndicator(
message: error.toLocalizedString(context),
),
AsyncLoaded<AnalyticsPracticeSessionModel>(:final value) =>
value.isComplete
? CompletedActivitySessionView(state.value, controller)

View file

@ -147,6 +147,7 @@ class LearningProgressIndicators extends StatelessWidget {
.colorScheme
.primary,
),
textScaler: TextScaler.noScaling,
),
if (userL1 != null && userL2 != null)
const Icon(Icons.chevron_right_outlined),
@ -162,6 +163,7 @@ class LearningProgressIndicators extends StatelessWidget {
.colorScheme
.primary,
),
textScaler: TextScaler.noScaling,
),
],
),

View file

@ -125,6 +125,7 @@ class AnimatedFloatingNumberState extends State<AnimatedFloatingNumber>
Text(
widget.number.toString(),
style: indicatorStyle,
textScaler: TextScaler.noScaling,
),
],
);

View file

@ -1,194 +0,0 @@
import 'package:flutter/material.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart';
import 'package:fluffychat/pangea/languages/language_model.dart';
import 'package:fluffychat/pangea/languages/p_language_store.dart';
import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart';
import 'package:fluffychat/pangea/learning_settings/p_language_dropdown.dart';
import 'package:fluffychat/widgets/matrix.dart';
class BotChatSettingsDialog extends StatefulWidget {
final Room room;
const BotChatSettingsDialog({
required this.room,
super.key,
});
@override
BotChatSettingsDialogState createState() => BotChatSettingsDialogState();
}
class BotChatSettingsDialogState extends State<BotChatSettingsDialog> {
LanguageModel? _selectedLang;
LanguageLevelTypeEnum? _selectedLevel;
String? _selectedVoice;
@override
void initState() {
final botSettings = widget.room.botOptions;
final activityPlan = _isActivity ? widget.room.activityPlan : null;
_selectedLevel = activityPlan?.req.cefrLevel ?? botSettings?.languageLevel;
_selectedVoice = botSettings?.targetVoice;
final lang =
activityPlan?.req.targetLanguage ?? botSettings?.targetLanguage;
if (lang != null) {
_selectedLang = PLanguageStore.byLangCode(lang);
}
super.initState();
}
bool get _isActivity => widget.room.isActivitySession;
Future<void> _setLanguage(LanguageModel? lang) async {
setState(() {
_selectedLang = lang;
_selectedVoice = null;
});
final model = widget.room.botOptions ?? BotOptionsModel();
model.targetLanguage = lang?.langCode;
model.targetVoice = null;
try {
await widget.room.setBotOptions(model);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': widget.room.id,
'langCode': lang?.langCode,
},
);
}
}
Future<void> _setLevel(LanguageLevelTypeEnum? level) async {
if (level == null) return;
setState(() => _selectedLevel = level);
final model = widget.room.botOptions ?? BotOptionsModel();
model.languageLevel = level;
try {
await widget.room.setBotOptions(model);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': widget.room.id,
'level': level.name,
},
);
}
}
Future<void> _setVoice(String? voice) async {
setState(() => _selectedVoice = voice);
final model = widget.room.botOptions ?? BotOptionsModel();
model.targetVoice = voice;
try {
await widget.room.setBotOptions(model);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': widget.room.id,
'voice': voice,
},
);
}
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Column(
spacing: 12.0,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.room.isActivitySession)
ListTile(
contentPadding: const EdgeInsets.all(0.0),
minLeadingWidth: 12.0,
leading: Icon(
Icons.info_outline,
size: 12.0,
color: Theme.of(context).disabledColor,
),
title: Text(
L10n.of(context).activitySettingsOverrideWarning,
style: TextStyle(
color: Theme.of(context).disabledColor,
fontSize: 12.0,
),
),
)
else
const SizedBox(),
PLanguageDropdown(
onChange: _setLanguage,
initialLanguage: _selectedLang,
languages:
MatrixState.pangeaController.pLanguageStore.targetOptions,
isL2List: true,
decorationText: L10n.of(context).targetLanguage,
enabled: !widget.room.isActivitySession,
),
LanguageLevelDropdown(
initialLevel: _selectedLevel,
onChanged: _setLevel,
enabled: !widget.room.isActivitySession,
width: 300,
maxHeight: 300,
),
DropdownButtonFormField2<String>(
customButton: _selectedVoice != null
? CustomDropdownTextButton(text: _selectedVoice!)
: null,
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
),
decoration: InputDecoration(
labelText: L10n.of(context).voice,
),
isExpanded: true,
dropdownStyleData: DropdownStyleData(
maxHeight: 250,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14.0),
),
),
items: (_selectedLang?.voices ?? <String>[]).map((voice) {
return DropdownMenuItem(
value: voice,
child: Text(voice),
);
}).toList(),
onChanged: _setVoice,
value: _selectedVoice,
),
const SizedBox(),
],
),
);
}
}

View file

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
class BotSettingsLanguageIcon extends StatelessWidget {
final User user;
const BotSettingsLanguageIcon({
super.key,
required this.user,
});
@override
Widget build(BuildContext context) {
final room = user.room;
String? langCode = room.botOptions?.targetLanguage;
if (room.isActivitySession && room.activityPlan != null) {
langCode = room.activityPlan!.req.targetLanguage;
}
if (langCode == null) {
return const SizedBox();
}
langCode = langCode.split('-').first;
return InkWell(
borderRadius: BorderRadius.circular(32.0),
onTap: room.isRoomAdmin
? () => showMemberActionsPopupMenu(
context: context,
user: user,
room: room,
)
: null,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: Theme.of(context).colorScheme.primaryContainer,
),
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Text(
langCode,
style: TextStyle(
fontSize: 10.0,
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
);
}
}

View file

@ -8,23 +8,23 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart';
class BotOptionsModel {
LanguageLevelTypeEnum languageLevel;
String topic;
List<String> keywords;
bool safetyModeration;
String mode;
String? discussionTopic;
String? discussionKeywords;
bool? discussionTriggerReactionEnabled;
String? discussionTriggerReactionKey;
String? customSystemPrompt;
bool? customTriggerReactionEnabled;
String? customTriggerReactionKey;
String? textAdventureGameMasterInstructions;
String? targetLanguage;
String? targetVoice;
final LanguageLevelTypeEnum languageLevel;
final String topic;
final List<String> keywords;
final bool safetyModeration;
final String mode;
final String? discussionTopic;
final String? discussionKeywords;
final bool? discussionTriggerReactionEnabled;
final String? discussionTriggerReactionKey;
final String? customSystemPrompt;
final bool? customTriggerReactionEnabled;
final String? customTriggerReactionKey;
final String? textAdventureGameMasterInstructions;
final String? targetLanguage;
final String? targetVoice;
BotOptionsModel({
const BotOptionsModel({
////////////////////////////////////////////////////////////////////////////
// General Bot Options
////////////////////////////////////////////////////////////////////////////
@ -133,50 +133,45 @@ class BotOptionsModel {
}
}
//TODO: define enum with all possible values
updateBotOption(String key, dynamic value) {
switch (key) {
case ModelKey.languageLevel:
languageLevel = value;
break;
case ModelKey.safetyModeration:
safetyModeration = value;
break;
case ModelKey.mode:
mode = value;
break;
case ModelKey.discussionTopic:
discussionTopic = value;
break;
case ModelKey.discussionKeywords:
discussionKeywords = value;
break;
case ModelKey.discussionTriggerReactionEnabled:
discussionTriggerReactionEnabled = value;
break;
case ModelKey.discussionTriggerReactionKey:
discussionTriggerReactionKey = value;
break;
case ModelKey.customSystemPrompt:
customSystemPrompt = value;
break;
case ModelKey.customTriggerReactionEnabled:
customTriggerReactionEnabled = value;
break;
case ModelKey.customTriggerReactionKey:
customTriggerReactionKey = value;
break;
case ModelKey.textAdventureGameMasterInstructions:
textAdventureGameMasterInstructions = value;
break;
case ModelKey.targetLanguage:
targetLanguage = value;
break;
case ModelKey.targetVoice:
targetVoice = value;
break;
default:
throw Exception('Invalid key for bot options - $key');
}
BotOptionsModel copyWith({
LanguageLevelTypeEnum? languageLevel,
String? topic,
List<String>? keywords,
bool? safetyModeration,
String? mode,
String? discussionTopic,
String? discussionKeywords,
bool? discussionTriggerReactionEnabled,
String? discussionTriggerReactionKey,
String? customSystemPrompt,
bool? customTriggerReactionEnabled,
String? customTriggerReactionKey,
String? textAdventureGameMasterInstructions,
String? targetLanguage,
String? targetVoice,
}) {
return BotOptionsModel(
languageLevel: languageLevel ?? this.languageLevel,
topic: topic ?? this.topic,
keywords: keywords ?? this.keywords,
safetyModeration: safetyModeration ?? this.safetyModeration,
mode: mode ?? this.mode,
discussionTopic: discussionTopic ?? this.discussionTopic,
discussionKeywords: discussionKeywords ?? this.discussionKeywords,
discussionTriggerReactionEnabled: discussionTriggerReactionEnabled ??
this.discussionTriggerReactionEnabled,
discussionTriggerReactionKey:
discussionTriggerReactionKey ?? this.discussionTriggerReactionKey,
customSystemPrompt: customSystemPrompt ?? this.customSystemPrompt,
customTriggerReactionEnabled:
customTriggerReactionEnabled ?? this.customTriggerReactionEnabled,
customTriggerReactionKey:
customTriggerReactionKey ?? this.customTriggerReactionKey,
textAdventureGameMasterInstructions:
textAdventureGameMasterInstructions ??
this.textAdventureGameMasterInstructions,
targetLanguage: targetLanguage ?? this.targetLanguage,
targetVoice: targetVoice ?? this.targetVoice,
);
}
}

View file

@ -7,23 +7,19 @@ import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/user/user_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
extension BotClientExtension on Client {
bool get hasBotDM => rooms.any((r) => r.isBotDM);
Room? get botDM => rooms.firstWhereOrNull((r) => r.isBotDM);
Room? get botDM => rooms.firstWhereOrNull(
(room) {
if (room.isDirectChat &&
room.directChatMatrixID == BotName.byEnvironment) {
return true;
}
if (room.botOptions?.mode == BotMode.directChat) {
return true;
}
return false;
},
);
// All 2-member rooms with the bot
List<Room> get targetBotChats => rooms.where((r) {
if (r.isBotDM) return true;
if (r.summary.mJoinedMemberCount != 2) return false;
return r.getParticipants().any((u) => u.id == BotName.byEnvironment);
}).toList();
Future<String> startChatWithBot() => startDirectChat(
BotName.byEnvironment,
@ -45,27 +41,31 @@ extension BotClientExtension on Client {
],
);
Future<void> updateBotOptions() async {
if (!isLogged() || botDM == null) return;
Future<void> updateBotOptions(UserSettings userSettings) async {
final rooms = targetBotChats;
if (rooms.isEmpty) return;
final targetLanguage =
MatrixState.pangeaController.userController.userL2?.langCode;
final cefrLevel = MatrixState
.pangeaController.userController.profile.userSettings.cefrLevel;
final updateBotOptions = botDM!.botOptions ?? BotOptionsModel();
final futures = <Future>[];
for (final room in rooms) {
final botOptions = room.botOptions ?? const BotOptionsModel();
final targetLanguage = userSettings.targetLanguage;
final languageLevel = userSettings.cefrLevel;
final voice = userSettings.voice;
if (updateBotOptions.targetLanguage == targetLanguage &&
updateBotOptions.languageLevel == cefrLevel) {
return;
if (botOptions.targetLanguage == targetLanguage &&
botOptions.languageLevel == languageLevel &&
botOptions.targetVoice == voice) {
continue;
}
final updated = botOptions.copyWith(
targetLanguage: targetLanguage,
languageLevel: languageLevel,
targetVoice: voice,
);
futures.add(room.setBotOptions(updated));
}
if (targetLanguage != null &&
updateBotOptions.targetLanguage != targetLanguage) {
updateBotOptions.targetVoice = null;
}
updateBotOptions.targetLanguage = targetLanguage;
updateBotOptions.languageLevel = cefrLevel;
await botDM!.setBotOptions(updateBotOptions);
await Future.wait(futures);
}
}

View file

@ -109,6 +109,7 @@ class ModelKey {
static const String transcription = "transcription";
static const String botTranscription = 'bot_transcription';
static const String voice = "voice";
// bot options
static const String languageLevel = "difficulty";

View file

@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/languages/p_language_store.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
import 'package:fluffychat/pangea/user/pangea_push_rules_extension.dart';
import 'package:fluffychat/pangea/user/style_settings_repo.dart';
import 'package:fluffychat/pangea/user/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../utils/firebase_analytics.dart';
@ -55,7 +56,7 @@ class PangeaController {
TtsController.setAvailableLanguages();
}
void _onLogin(BuildContext context) {
void _onLogin(BuildContext context, String? userID) {
initControllers();
_registerSubscriptions();
@ -64,6 +65,10 @@ class PangeaController {
Provider.of<LocaleProvider>(context, listen: false).setLocale(l1);
});
subscriptionController.reinitialize();
StyleSettingsRepo.fontSizeFactor(userID!).then((factor) {
AppConfig.fontSizeFactor = factor;
});
}
void _onLogout(BuildContext context) {
@ -91,7 +96,7 @@ class PangeaController {
_onLogout(context);
break;
case LoginState.loggedIn:
_onLogin(context);
_onLogin(context, userID);
break;
}
@ -112,8 +117,9 @@ class PangeaController {
userController.languageStream.stream.listen(_onLanguageUpdate);
_settingsSubscription?.cancel();
_settingsSubscription = userController.settingsUpdateStream.stream
.listen((_) => matrixState.client.updateBotOptions());
_settingsSubscription = userController.settingsUpdateStream.stream.listen(
(update) => matrixState.client.updateBotOptions(update.userSettings),
);
_joinSpaceSubscription?.cancel();
_joinSpaceSubscription ??= matrixState.client.onSync.stream
@ -122,9 +128,7 @@ class PangeaController {
}
Future<void> _clearCache({List<String> exclude = const []}) async {
final List<Future<void>> futures = [
matrixState.store.setString(SettingKeys.fontSizeFactor, ''),
];
final List<Future<void>> futures = [];
for (final key in _storageKeys) {
if (exclude.contains(key)) continue;
futures.add(GetStorage(key).erase());
@ -142,7 +146,6 @@ class PangeaController {
);
}
AppConfig.fontSizeFactor = 1.0;
await Future.wait(futures);
}
@ -174,7 +177,7 @@ class PangeaController {
}
_clearCache(exclude: exclude);
matrixState.client.updateBotOptions();
matrixState.client.updateBotOptions(userController.profile.userSettings);
}
static final List<String> _storageKeys = [

View file

@ -57,7 +57,9 @@ class CourseChatsController extends State<CourseChats>
@override
void initState() {
loadHierarchy(reload: true);
loadHierarchy(reload: true).then(
(_) => _joinDefaultChats(),
);
// Listen for changes to the activeSpace's hierarchy,
// and reload the hierarchy when they come through
@ -212,7 +214,6 @@ class CourseChatsController extends State<CourseChats>
try {
await _loadHierarchy(activeSpace: room, reload: reload);
if (mounted) await _joinDefaultChats();
if (mounted) {
final futures = [
loadRoomSummaries(

View file

@ -179,7 +179,7 @@ class SelectedCourseController extends State<SelectedCourse>
await showOkAlertDialog(
context: context,
title: L10n.of(context).youHaveKnocked,
message: L10n.of(context).pleaseWaitUntilInvited,
message: L10n.of(context).knockDesc,
);
return;
}

View file

@ -234,40 +234,43 @@ class SelectedCourseView extends StatelessWidget {
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
Row(
spacing: 12.0,
children: [
const Icon(
Icons.edit,
size: mediumIconSize,
),
Flexible(
child: Text(
L10n.of(context).editCourseLater,
style: const TextStyle(
fontSize: descFontSize,
if (controller.widget.mode !=
SelectedCourseMode.join) ...[
Row(
spacing: 12.0,
children: [
const Icon(
Icons.edit,
size: mediumIconSize,
),
Flexible(
child: Text(
L10n.of(context).editCourseLater,
style: const TextStyle(
fontSize: descFontSize,
),
),
),
),
],
),
Row(
spacing: 12.0,
children: [
const Icon(
Icons.shield,
size: mediumIconSize,
),
Flexible(
child: Text(
L10n.of(context).newCourseAccess,
style: const TextStyle(
fontSize: descFontSize,
],
),
Row(
spacing: 12.0,
children: [
const Icon(
Icons.shield,
size: mediumIconSize,
),
Flexible(
child: Text(
L10n.of(context).newCourseAccess,
style: const TextStyle(
fontSize: descFontSize,
),
),
),
),
],
),
],
),
],
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: ElevatedButton(

View file

@ -294,16 +294,18 @@ class PangeaMessageEvent {
RepresentationEvent? representationByLanguage(
String langCode, {
bool Function(RepresentationEvent)? filter,
}) {
representations.firstWhereOrNull(
(element) =>
element.langCode.split("-")[0] == langCode.split("-")[0] &&
(filter?.call(element) ?? true),
);
return null;
}
}) =>
representations.firstWhereOrNull(
(element) =>
element.langCode.split("-")[0] == langCode.split("-")[0] &&
(filter?.call(element) ?? true),
);
Event? getTextToSpeechLocal(String langCode, String text) {
Event? getTextToSpeechLocal(
String langCode,
String text,
String? voice,
) {
for (final audio in allAudio) {
final dataMap = audio.content.tryGetMap(ModelKey.transcription);
if (dataMap == null || !dataMap.containsKey(ModelKey.tokens)) continue;
@ -313,7 +315,9 @@ class PangeaMessageEvent {
dataMap as dynamic,
);
if (audioData.langCode == langCode && audioData.text == text) {
if (audioData.langCode == langCode &&
audioData.text == text &&
audioData.voice == voice) {
return audio;
}
} catch (e, s) {
@ -368,7 +372,7 @@ class PangeaMessageEvent {
String langCode,
String? voice,
) async {
final local = getTextToSpeechLocal(langCode, messageDisplayText);
final local = getTextToSpeechLocal(langCode, messageDisplayText, voice);
if (local != null) {
final file = await local.getPangeaAudioFile();
if (file != null) return file;
@ -424,7 +428,7 @@ class PangeaMessageEvent {
'waveform': response.waveform,
},
ModelKey.transcription: response
.toPangeaAudioEventData(rep?.text ?? body, langCode)
.toPangeaAudioEventData(rep?.text ?? body, langCode, voice)
.toJson(),
},
).then((eventId) async {

View file

@ -44,7 +44,6 @@ class PSettingsSwitchListTileState
@override
Widget build(BuildContext context) {
return SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
value: currentValue,
title: Text(widget.title),
activeThumbColor: AppConfig.activeToggleColor,

View file

@ -41,7 +41,6 @@ class SettingsLearningController extends State<SettingsLearning> {
PangeaController pangeaController = MatrixState.pangeaController;
late Profile _profile;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
String? languageMatchError;
final ScrollController scrollController = ScrollController();
@ -110,18 +109,16 @@ class SettingsLearningController extends State<SettingsLearning> {
updateToolSetting(ToolSetting.enableTTS, false);
}
if (formKey.currentState!.validate()) {
await showFutureLoadingDialog(
context: context,
future: () async => pangeaController.userController
.updateProfile(
(_) => _profile,
waitForDataInSync: true,
)
.timeout(const Duration(seconds: 15)),
);
Navigator.of(context).pop();
}
await showFutureLoadingDialog(
context: context,
future: () async => pangeaController.userController
.updateProfile(
(_) => _profile,
waitForDataInSync: true,
)
.timeout(const Duration(seconds: 15)),
);
Navigator.of(context).pop();
}
Future<void> resetInstructionTooltips() async {
@ -153,11 +150,12 @@ class SettingsLearningController extends State<SettingsLearning> {
LanguageModel? sourceLanguage,
LanguageModel? targetLanguage,
}) async {
if (sourceLanguage != null) {
if (sourceLanguage != null && sourceLanguage != selectedSourceLanguage) {
_profile.userSettings.sourceLanguage = sourceLanguage.langCode;
}
if (targetLanguage != null) {
if (targetLanguage != null && targetLanguage != selectedTargetLanguage) {
_profile.userSettings.targetLanguage = targetLanguage.langCode;
_profile.userSettings.voice = null;
if (!_profile.toolSettings.enableTTS && isTTSSupported) {
updateToolSetting(ToolSetting.enableTTS, true);
}
@ -181,6 +179,11 @@ class SettingsLearningController extends State<SettingsLearning> {
if (mounted) setState(() {});
}
void setVoice(String? voice) {
_profile.userSettings.voice = voice;
if (mounted) setState(() {});
}
void changeCountry(Country? country) {
_profile.userSettings.country = country?.name;
if (mounted) setState(() {});
@ -343,6 +346,8 @@ class SettingsLearningController extends State<SettingsLearning> {
LanguageLevelTypeEnum get cefrLevel => _profile.userSettings.cefrLevel;
String? get selectedVoice => _profile.userSettings.voice;
Country? get country =>
CountryService().findByName(_profile.userSettings.country);

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
@ -14,6 +13,7 @@ import 'package:fluffychat/pangea/learning_settings/p_language_dropdown.dart';
import 'package:fluffychat/pangea/learning_settings/p_settings_switch_list_tile.dart';
import 'package:fluffychat/pangea/learning_settings/settings_learning.dart';
import 'package:fluffychat/pangea/learning_settings/tool_settings_enum.dart';
import 'package:fluffychat/pangea/learning_settings/voice_dropdown.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -45,21 +45,23 @@ class SettingsLearningView extends StatelessWidget {
)
: null,
),
body: Form(
key: controller.formKey,
child: ListTileTheme(
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
child: MaxWidthBody(
withScrolling: false,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
controller: controller.scrollController,
body: ListTileTheme(
iconColor: Theme.of(context).textTheme.bodyLarge!.color,
child: MaxWidthBody(
withScrolling: false,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
controller: controller.scrollController,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32.0),
child: Column(
spacing: 16.0,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
padding:
const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
spacing: 16.0,
children: [
@ -99,171 +101,112 @@ class SettingsLearningView extends StatelessWidget {
.colorScheme
.surfaceContainerHigh,
),
AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: controller.userL1?.langCodeShort ==
controller.userL2?.langCodeShort
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
if (controller.userL1?.langCodeShort ==
controller.userL2?.langCodeShort)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Row(
spacing: 8.0,
children: [
Icon(
Icons.info_outlined,
color: Theme.of(context)
.colorScheme
.error,
),
Flexible(
child: Text(
L10n.of(context)
.noIdenticalLanguages,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.error,
),
),
child: Row(
spacing: 8.0,
children: [
Icon(
Icons.info_outlined,
color: Theme.of(context)
.colorScheme
.error,
),
Flexible(
child: Text(
L10n.of(context)
.noIdenticalLanguages,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.error,
),
),
),
],
),
)
: const SizedBox.shrink(),
),
CountryPickerDropdown(controller),
),
],
),
),
LanguageLevelDropdown(
initialLevel: controller.cefrLevel,
onChanged: controller.setCefrLevel,
),
VoiceDropdown(
value: controller.selectedVoice,
language: controller.selectedTargetLanguage,
onChanged: controller.setVoice,
),
CountryPickerDropdown(controller),
GenderDropdown(
initialGender: controller.gender,
onChanged: controller.setGender,
),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white54,
),
borderRadius: BorderRadius.circular(8.0),
),
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
ProfileSettingsSwitchListTile.adaptive(
defaultValue:
controller.getToolSetting(
ToolSetting.autoIGC,
),
title: ToolSetting.autoIGC
.toolName(context),
subtitle: ToolSetting.autoIGC
.toolDescription(context),
onChange: (bool value) =>
controller.updateToolSetting(
ToolSetting.autoIGC,
value,
),
enabled: true,
),
ProfileSettingsSwitchListTile.adaptive(
defaultValue:
controller.getToolSetting(
ToolSetting.enableAutocorrect,
),
title: ToolSetting.enableAutocorrect
.toolName(context),
subtitle: ToolSetting
.enableAutocorrect
.toolDescription(context),
onChange: (bool value) {
controller.updateToolSetting(
ToolSetting.enableAutocorrect,
value,
);
if (value) {
controller
.showKeyboardSettingsDialog();
}
},
enabled: true,
),
],
),
),
for (final toolSetting
in ToolSetting.values.where(
(tool) =>
tool.isAvailableSetting &&
tool != ToolSetting.autoIGC &&
tool != ToolSetting.enableAutocorrect,
))
Column(
children: [
ProfileSettingsSwitchListTile.adaptive(
defaultValue: controller
.getToolSetting(toolSetting),
title: toolSetting.toolName(context),
subtitle: toolSetting ==
ToolSetting.enableTTS &&
!controller.isTTSSupported
? null
: toolSetting
.toolDescription(context),
onChange: (bool value) =>
controller.updateToolSetting(
toolSetting,
value,
),
),
],
),
SwitchListTile.adaptive(
value: controller.publicProfile,
onChanged: controller.setPublicProfile,
title: Text(
L10n.of(context).publicProfileTitle,
),
subtitle: Text(
L10n.of(context).publicProfileDesc,
),
activeThumbColor:
AppConfig.activeToggleColor,
contentPadding: EdgeInsets.zero,
),
ResetInstructionsListTile(
controller: controller,
),
],
),
),
...ToolSetting.values
.where(
(tool) => tool.isAvailableSetting,
)
.map(
(toolSetting) => _ProfileSwitchTile(
value:
controller.getToolSetting(toolSetting),
setting: toolSetting,
onChanged: (v) {
controller.updateToolSetting(
toolSetting,
v,
);
if (v &&
toolSetting ==
ToolSetting.enableTTS) {
controller.showKeyboardSettingsDialog();
}
},
),
),
SwitchListTile.adaptive(
value: controller.publicProfile,
onChanged: controller.setPublicProfile,
title: Text(
L10n.of(context).publicProfileTitle,
),
subtitle: Text(
L10n.of(context).publicProfileDesc,
),
activeThumbColor: AppConfig.activeToggleColor,
),
ResetInstructionsListTile(
controller: controller,
),
],
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.haveSettingsBeenChanged
? controller.submit
: null,
child: Text(L10n.of(context).saveChanges),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.haveSettingsBeenChanged
? controller.submit
: null,
child: Text(L10n.of(context).saveChanges),
),
),
],
),
),
],
),
),
),
);
if (!controller.widget.isDialog) return dialogContent;
return FullWidthDialog(
dialogContent: dialogContent,
maxWidth: 600,
@ -273,3 +216,25 @@ class SettingsLearningView extends StatelessWidget {
);
}
}
class _ProfileSwitchTile extends StatelessWidget {
final bool value;
final ToolSetting setting;
final Function(bool) onChanged;
const _ProfileSwitchTile({
required this.value,
required this.setting,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return ProfileSettingsSwitchListTile.adaptive(
defaultValue: value,
title: setting.toolName(context),
subtitle: setting.toolDescription(context),
onChange: onChanged,
);
}
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart';
import 'package:fluffychat/pangea/languages/language_model.dart';
class VoiceDropdown extends StatelessWidget {
final String? value;
final LanguageModel? language;
final Function(String?) onChanged;
const VoiceDropdown({
super.key,
this.value,
this.language,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return DropdownButtonFormField2<String>(
customButton:
value != null ? CustomDropdownTextButton(text: value!) : null,
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
),
decoration: InputDecoration(
labelText: L10n.of(context).voiceDropdownTitle,
),
isExpanded: true,
dropdownStyleData: DropdownStyleData(
maxHeight: 250,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(14.0),
),
),
items: (language?.voices ?? <String>[]).map((voice) {
return DropdownMenuItem(
value: voice,
child: Text(voice),
);
}).toList(),
onChanged: onChanged,
value: value,
);
}
}

View file

@ -10,7 +10,6 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics_requested_dialog.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class AnalyticsRequestIndicator extends StatefulWidget {
@ -60,18 +59,38 @@ class AnalyticsRequestIndicatorState extends State<AnalyticsRequestIndicator> {
);
await Future.wait(futures);
final analyicsRoomIds = analyticsRooms.map((r) => r.id).toSet();
final analyticsRoomIds = analyticsRooms.map((r) => r.id).toSet();
_analyticsRoomSub?.cancel();
_analyticsRoomSub = widget.room.client.onRoomState.stream
.where(
(event) =>
analyicsRoomIds.contains(event.roomId) &&
event.state.type == EventTypes.RoomMember,
)
.rateLimit(const Duration(seconds: 1))
.listen((_) => setState(() {}));
_analyticsRoomSub = widget.room.client.onSync.stream.listen((update) async {
final joined = update.rooms?.join?.entries
.where((e) => analyticsRoomIds.contains(e.key));
if (mounted) setState(() {});
if (joined == null || joined.isEmpty) return;
final Set<String> updatedRoomIds = {};
for (final entry in joined) {
final memberEvents = entry.value.timeline?.events?.where(
(e) => e.type == EventTypes.RoomMember,
);
if (memberEvents != null && memberEvents.isNotEmpty) {
updatedRoomIds.add(entry.key);
}
}
if (updatedRoomIds.isEmpty) return;
for (final roomId in updatedRoomIds) {
final room = widget.room.client.getRoomById(roomId);
if (room == null) continue;
await room.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
);
}
if (mounted) {
setState(() {});
}
});
}
Map<User, List<Room>> get _knockingAdmins {

View file

@ -94,4 +94,8 @@ class AnalyticsRequestsRepo {
final key = _storageKey(userId, language);
await _requestStorage.remove(key);
}
static Future<void> clear() async {
await _requestStorage.erase();
}
}

View file

@ -189,6 +189,7 @@ class SpaceAnalyticsState extends State<SpaceAnalytics> {
Future<void> refresh() async {
if (room == null || !room!.isSpace || selectedLanguage == null) return;
await AnalyticsRequestsRepo.clear();
setState(() {
downloads = Map.fromEntries(

View file

@ -41,11 +41,16 @@ class TextToSpeechResponseModel {
"tts_tokens": List<dynamic>.from(ttsTokens.map((x) => x.toJson())),
};
PangeaAudioEventData toPangeaAudioEventData(String text, String langCode) {
PangeaAudioEventData toPangeaAudioEventData(
String text,
String langCode,
String? voice,
) {
return PangeaAudioEventData(
text: text,
langCode: langCode,
tokens: ttsTokens,
voice: voice,
);
}
}
@ -91,11 +96,13 @@ class PangeaAudioEventData {
final String text;
final String langCode;
final List<TTSToken> tokens;
final String? voice;
PangeaAudioEventData({
required this.text,
required this.langCode,
required this.tokens,
this.voice,
});
factory PangeaAudioEventData.fromJson(dynamic json) => PangeaAudioEventData(
@ -106,6 +113,7 @@ class PangeaAudioEventData {
.map((x) => TTSToken.fromJson(x))
.toList(),
),
voice: json[ModelKey.voice] as String?,
);
Map<String, dynamic> toJson() => {
@ -113,5 +121,6 @@ class PangeaAudioEventData {
ModelKey.langCode: langCode,
ModelKey.tokens:
List<Map<String, dynamic>>.from(tokens.map((x) => x.toJson())),
if (voice != null) ModelKey.voice: voice,
};
}

View file

@ -14,9 +14,6 @@ import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/languages/language_model.dart';
import 'package:fluffychat/pangea/languages/p_language_store.dart';
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
import 'package:fluffychat/pangea/toolbar/layout/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/select_mode_buttons.dart';
@ -500,19 +497,19 @@ class _MessageBubbleTranscription extends StatelessWidget {
onClick: onTokenSelected,
isSelected: isTokenSelected,
),
if (MatrixState
.pangeaController.userController.showTranscription)
PhoneticTranscriptionWidget(
text: transcription.transcript.text,
textLanguage: PLanguageStore.byLangCode(
transcription.langCode,
) ??
LanguageModel.unknown,
style: style,
iconColor: style.color,
onTranscriptionFetched: () =>
controller.contentChangedStream.add(true),
),
// if (MatrixState
// .pangeaController.userController.showTranscription)
// PhoneticTranscriptionWidget(
// text: transcription.transcript.text,
// textLanguage: PLanguageStore.byLangCode(
// transcription.langCode,
// ) ??
// LanguageModel.unknown,
// style: style,
// iconColor: style.color,
// onTranscriptionFetched: () =>
// controller.contentChangedStream.add(true),
// ),
],
),
);

View file

@ -8,10 +8,10 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/text_to_speech/text_to_speech_response_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class MessageAudioCard extends StatefulWidget {
final PangeaMessageEvent messageEvent;
@ -44,7 +44,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
try {
audioFile = await widget.messageEvent.requestTextToSpeech(
widget.messageEvent.messageDisplayLangCode,
widget.messageEvent.room.botOptions?.targetVoice,
MatrixState.pangeaController.userController.voice,
);
debugPrint("audio file is now: $audioFile. setting starts and ends...");
if (mounted) setState(() => _isLoading = false);

View file

@ -7,7 +7,6 @@ import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/pangea/analytics_misc/lemma_emoji_setter_mixin.dart';
import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
@ -58,7 +57,7 @@ class _AudioLoader extends AsyncLoader<(PangeaAudioFile, File?)> {
Future<(PangeaAudioFile, File?)> fetch() async {
final audioBytes = await messageEvent.requestTextToSpeech(
messageEvent.messageDisplayLangCode,
messageEvent.room.botOptions?.targetVoice,
MatrixState.pangeaController.userController.voice,
);
File? audioFile;
@ -91,8 +90,6 @@ class SelectModeController with LemmaEmojiSetter {
ValueNotifier<SelectMode?> selectedMode = ValueNotifier<SelectMode?>(null);
final StreamController contentChangedStream = StreamController.broadcast();
// Sometimes the same token is clicked twice. Setting it to the same value
// won't trigger the notifier, so use the bool for force it to trigger.
ValueNotifier<(PangeaTokenText?, bool)> playTokenNotifier =
@ -105,7 +102,6 @@ class SelectModeController with LemmaEmojiSetter {
_translationLoader.dispose();
_sttTranslationLoader.dispose();
_audioLoader.dispose();
contentChangedStream.close();
}
static List<SelectMode> get _textModes => [

View file

@ -0,0 +1,40 @@
import 'package:get_storage/get_storage.dart';
class _StyleSettings {
final double fontSizeFactor;
const _StyleSettings({
this.fontSizeFactor = 1.0,
});
Map<String, dynamic> toJson() {
return {
'fontSizeFactor': fontSizeFactor,
};
}
factory _StyleSettings.fromJson(Map<String, dynamic> json) {
return _StyleSettings(
fontSizeFactor: (json['fontSizeFactor'] as num?)?.toDouble() ?? 1.0,
);
}
}
class StyleSettingsRepo {
static final GetStorage _storage = GetStorage("style_settings");
static Future<double> fontSizeFactor(String userId) async {
await GetStorage.init("style_settings");
final json =
_storage.read<Map<String, dynamic>>('${userId}_style_settings');
final settings =
json != null ? _StyleSettings.fromJson(json) : const _StyleSettings();
return settings.fontSizeFactor;
}
static Future<void> setFontSizeFactor(String userId, double factor) async {
await GetStorage.init("style_settings");
final settings = _StyleSettings(fontSizeFactor: factor);
await _storage.write('${userId}_style_settings', settings.toJson());
}
}

View file

@ -431,6 +431,8 @@ class UserController {
: langModel;
}
String? get voice => profile.userSettings.voice;
bool get languagesSet =>
userL1Code != null &&
userL2Code != null &&

View file

@ -19,6 +19,7 @@ class UserSettings {
GenderEnum gender;
String? country;
LanguageLevelTypeEnum cefrLevel;
String? voice;
UserSettings({
this.dateOfBirth,
@ -29,6 +30,7 @@ class UserSettings {
this.gender = GenderEnum.unselected,
this.country,
this.cefrLevel = LanguageLevelTypeEnum.a1,
this.voice,
});
factory UserSettings.fromJson(Map<String, dynamic> json) => UserSettings(
@ -52,6 +54,7 @@ class UserSettings {
json[ModelKey.cefrLevel],
)
: LanguageLevelTypeEnum.a1,
voice: json[ModelKey.voice],
);
Map<String, dynamic> toJson() {
@ -64,6 +67,7 @@ class UserSettings {
data[ModelKey.userGender] = gender.string;
data[ModelKey.userCountry] = country;
data[ModelKey.cefrLevel] = cefrLevel.string;
data[ModelKey.voice] = voice;
return data;
}
@ -123,6 +127,7 @@ class UserSettings {
gender: gender,
country: country,
cefrLevel: cefrLevel,
voice: voice,
);
}
@ -138,7 +143,8 @@ class UserSettings {
other.sourceLanguage == sourceLanguage &&
other.gender == gender &&
other.country == country &&
other.cefrLevel == cefrLevel;
other.cefrLevel == cefrLevel &&
other.voice == voice;
}
@override
@ -151,6 +157,7 @@ class UserSettings {
gender.hashCode,
country.hashCode,
cefrLevel.hashCode,
voice.hashCode,
]);
}

View file

@ -8,6 +8,7 @@ import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_repo.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/utils/other_party_can_receive.dart';
import 'uia_request_manager.dart';
@ -34,6 +35,10 @@ extension LocalizedExceptionExtension on Object {
if (this is UnsubscribedException) {
return L10n.of(context).unsubscribedResponseError;
}
if (this is InsufficientDataException) {
return L10n.of(context).notEnoughToPractice;
}
// Pangea#
if (this is FileTooBigMatrixException) {
final exception = this as FileTooBigMatrixException;

View file

@ -64,7 +64,8 @@ extension IsStateExtension on Event {
bool get isVisibleInPangeaGui {
if (!room.showActivityChatUI) {
return type != EventTypes.RoomMember ||
roomMemberChangeType != RoomMemberChangeType.avatar;
(roomMemberChangeType != RoomMemberChangeType.avatar &&
roomMemberChangeType != RoomMemberChangeType.other);
}
return type != EventTypes.RoomMember;

View file

@ -28,7 +28,6 @@ class Avatar extends StatelessWidget {
final double? presenceSize;
final Offset? presenceOffset;
final Widget? miniIcon;
// Pangea#
const Avatar({
@ -48,7 +47,6 @@ class Avatar extends StatelessWidget {
this.userId,
this.presenceSize,
this.presenceOffset,
this.miniIcon,
// Pangea#
super.key,
});
@ -140,13 +138,7 @@ class Avatar extends StatelessWidget {
),
// #Pangea
// if (presenceUserId != null)
if (miniIcon != null)
Positioned(
bottom: presenceOffset?.dy ?? -3,
right: presenceOffset?.dx ?? -3,
child: miniIcon!,
)
else if (presenceUserId != null && size >= 32.0 && showPresence)
if (presenceUserId != null && size >= 32.0 && showPresence)
// Pangea#
PresenceBuilder(
client: client,

View file

@ -26,6 +26,7 @@ import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
import 'package:fluffychat/pangea/languages/locale_provider.dart';
import 'package:fluffychat/pangea/user/style_settings_repo.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -553,9 +554,16 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
}
void initSettings() {
AppConfig.fontSizeFactor =
double.tryParse(store.getString(SettingKeys.fontSizeFactor) ?? '') ??
AppConfig.fontSizeFactor;
// #Pangea
// AppConfig.fontSizeFactor =
// double.tryParse(store.getString(SettingKeys.fontSizeFactor) ?? '') ??
// AppConfig.fontSizeFactor;
if (client.isLogged()) {
StyleSettingsRepo.fontSizeFactor(client.userID!).then((factor) {
AppConfig.fontSizeFactor = factor;
});
}
// Pangea#
AppConfig.renderHtml =
store.getBool(SettingKeys.renderHtml) ?? AppConfig.renderHtml;

View file

@ -6,8 +6,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/bot/widgets/bot_chat_settings_dialog.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
import 'adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
@ -108,15 +106,15 @@ void showMemberActionsPopupMenu({
],
),
),
if (user.id == BotName.byEnvironment && room != null && room.isRoomAdmin)
PopupMenuItem(
enabled: false,
padding: const EdgeInsets.only(
left: 12.0,
right: 12.0,
),
child: BotChatSettingsDialog(room: room),
),
// if (user.id == BotName.byEnvironment && room != null && room.isRoomAdmin)
// PopupMenuItem(
// enabled: false,
// padding: const EdgeInsets.only(
// left: 12.0,
// right: 12.0,
// ),
// child: BotChatSettingsDialog(room: room),
// ),
const PopupMenuDivider(),
// #Pangea
if (user.room.client.userID != user.id)