Merge branch 'main' into 5421-grammar-practice-todos

This commit is contained in:
Ava Shilling 2026-01-29 14:36:00 -05:00
commit aa597b8698
98 changed files with 2811 additions and 800 deletions

View file

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix_api_lite/generated/model.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
@ -48,6 +47,7 @@ import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selectio
import 'package:fluffychat/pangea/common/utils/p_vguard.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/course_creation/course_invite_page.dart';
import 'package:fluffychat/pangea/course_creation/public_course_preview.dart';
import 'package:fluffychat/pangea/course_creation/selected_course_page.dart';
import 'package:fluffychat/pangea/join_codes/join_with_link_page.dart';
import 'package:fluffychat/pangea/learning_settings/settings_learning.dart';
@ -407,15 +407,13 @@ abstract class AppRoutes {
],
),
GoRoute(
path: ':courseid',
path: ':courseroomid',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
SelectedCourse(
state.pathParameters['courseid']!,
SelectedCourseMode.join,
roomChunk: state.extra as PublicRoomsChunk?,
PublicCoursePreview(
roomID: state.pathParameters['courseroomid']!,
),
);
},

View file

@ -1,6 +1,6 @@
{
"@@locale": "ar",
"@@last_modified": "2026-01-27 14:02:59.727958",
"@@last_modified": "2026-01-28 13:26:35.542116",
"about": "حول",
"@about": {
"type": "String",
@ -11151,5 +11151,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "ما اللغة التي تتعلمها؟",
"searchLanguagesHint": "ابحث عن اللغات المستهدفة",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "أسئلة؟ نحن هنا للمساعدة!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "حدث خطأ ما، ونحن نعمل بجد على إصلاحه. تحقق مرة أخرى لاحقًا.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1910,7 +1910,7 @@
"playWithAI": "Пакуль гуляйце з ШІ",
"courseStartDesc": "Pangea Bot гатовы да працы ў любы час!\n\n...але навучанне лепш з сябрамі!",
"@@locale": "be",
"@@last_modified": "2026-01-27 14:02:50.270963",
"@@last_modified": "2026-01-28 13:26:22.828870",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -12033,5 +12033,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Якую мову вы вывучаеце?",
"searchLanguagesHint": "Пошук мэтавых моў",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Пытанні? Мы тут, каб дапамагчы!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Што-то пайшло не так, і мы актыўна працуем над выпраўленнем. Праверце пазней.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:11.965364",
"@@last_modified": "2026-01-28 13:26:47.712647",
"about": "সম্পর্কে",
"@about": {
"type": "String",
@ -12038,5 +12038,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "আপনি কোন ভাষা শিখছেন?",
"searchLanguagesHint": "লক্ষ্য ভাষা অনুসন্ধান করুন",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "প্রশ্ন আছে? আমরা সাহায্য করতে এখানে আছি!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "কিছু ভুল হয়েছে, এবং আমরা এটি ঠিক করতে কঠোর পরিশ্রম করছি। পরে আবার চেক করুন।",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4278,7 +4278,7 @@
"joinPublicTrip": "མི་ཚེས་ལ་ལོག་འབད།",
"startOwnTrip": "ངེད་རང་གི་ལོག་ལ་སྦྱོར་བཅོས།",
"@@locale": "bo",
"@@last_modified": "2026-01-27 14:03:09.952724",
"@@last_modified": "2026-01-28 13:26:44.570789",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -10688,5 +10688,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Kedua bahasa apa yang Anda pelajari?",
"searchLanguagesHint": "Cari bahasa target",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Pytania? Jesteśmy tutaj, aby pomóc!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Nǐng bǐng wǒng, yǐng wǒng bǐng wǒng. Cǐng bǐng yǐng bǐng.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:51.719519",
"@@last_modified": "2026-01-28 13:26:23.872618",
"about": "Quant a",
"@about": {
"type": "String",
@ -10958,5 +10958,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Quina llengua estàs aprenent?",
"searchLanguagesHint": "Cerca llengües objectiu",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Preguntes? Som aquí per ajudar!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Alguna cosa ha anat malament, i estem treballant dur per solucionar-ho. Comprova-ho més tard.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "cs",
"@@last_modified": "2026-01-27 14:02:46.567796",
"@@last_modified": "2026-01-28 13:26:20.564591",
"about": "O aplikaci",
"@about": {
"type": "String",
@ -11541,5 +11541,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Jaký jazyk se učíte?",
"searchLanguagesHint": "Hledejte cílové jazyky",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Otázky? Jsme tu, abychom pomohli!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Něco se pokazilo a my na tom tvrdě pracujeme. Zkontrolujte to prosím později.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1929,7 +1929,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-27 14:02:21.118853",
"@@last_modified": "2026-01-28 13:25:58.575899",
"@aboutHomeserver": {
"type": "String",
"placeholders": {
@ -11995,5 +11995,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Hvilket sprog lærer du?",
"searchLanguagesHint": "Søg efter målsprog",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Spørgsmål? Vi er her for at hjælpe!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Noget gik galt, og vi arbejder hårdt på at løse det. Tjek igen senere.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "de",
"@@last_modified": "2026-01-27 14:02:39.850908",
"@@last_modified": "2026-01-28 13:26:14.577124",
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
@ -10941,5 +10941,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Welche Sprache lernst du?",
"searchLanguagesHint": "Zielsprachen suchen",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Fragen? Wir sind hier, um zu helfen!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Etwas ist schiefgelaufen, und wir arbeiten hart daran, es zu beheben. Überprüfen Sie es später erneut.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4455,7 +4455,7 @@
"playWithAI": "Παίξτε με την Τεχνητή Νοημοσύνη προς το παρόν",
"courseStartDesc": "Ο Pangea Bot είναι έτοιμος να ξεκινήσει οποιαδήποτε στιγμή!\n\n...αλλά η μάθηση είναι καλύτερη με φίλους!",
"@@locale": "el",
"@@last_modified": "2026-01-27 14:03:17.212773",
"@@last_modified": "2026-01-28 13:26:53.057151",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11992,5 +11992,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Ποια γλώσσα μαθαίνετε;",
"searchLanguagesHint": "Αναζητήστε γλώσσες στόχου",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Ερωτήσεις; Είμαστε εδώ για να βοηθήσουμε!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Κάτι πήγε στραβά και εργαζόμαστε σκληρά για να το διορθώσουμε. Έλεγξε ξανά αργότερα.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -5063,5 +5063,9 @@
"publicInviteDescSpace": "Search for users to invite them to this space.",
"useActivityImageAsChatBackground": "Use activity image as chat background",
"chatWithSupport": "Chat with Support",
"newCourseAccess": "By default, courses are publicly searchable and require admin approval to join. You can edit these settings at any time."
"newCourseAccess": "By default, courses are publicly searchable and require admin approval to join. You can edit these settings at any time.",
"courseLoadingError": "Something went wrong, and we're hard at work fixing it. Check again later.",
"onboardingLanguagesTitle": "What language are you learning?",
"searchLanguagesHint": "Search target languages",
"supportSubtitle": "Questions? We're here to help!"
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:21.507072",
"@@last_modified": "2026-01-28 13:26:57.219657",
"about": "Prio",
"@about": {
"type": "String",
@ -12023,5 +12023,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Kian lingvon vi lernas?",
"searchLanguagesHint": "Serĉu celajn lingvojn",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Demandoj? Ni ĉi tie por helpi!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Io malĝuste okazis, kaj ni diligente laboras por ripari ĝin. Kontrolu denove poste.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "es",
"@@last_modified": "2026-01-27 14:02:15.353643",
"@@last_modified": "2026-01-28 13:25:54.826808",
"about": "Acerca de",
"@about": {
"type": "String",
@ -8168,5 +8168,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "¿Qué idioma estás aprendiendo?",
"searchLanguagesHint": "Buscar idiomas objetivo",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "¿Preguntas? ¡Estamos aquí para ayudar!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Algo salió mal y estamos trabajando arduamente para solucionarlo. Revisa de nuevo más tarde.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "et",
"@@last_modified": "2026-01-27 14:02:38.644251",
"@@last_modified": "2026-01-28 13:26:13.431455",
"about": "Rakenduse teave",
"@about": {
"type": "String",
@ -11205,5 +11205,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Millist keelt sa õpid?",
"searchLanguagesHint": "Otsi sihtkeeli",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Küsimused? Me oleme siin, et aidata!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Midagi läks valesti ja me teeme kõvasti tööd, et see parandada. Kontrolli hiljem uuesti.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "eu",
"@@last_modified": "2026-01-27 14:02:35.628448",
"@@last_modified": "2026-01-28 13:26:10.864458",
"about": "Honi buruz",
"@about": {
"type": "String",
@ -10934,5 +10934,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Zer hizkuntza ikasten ari zara?",
"searchLanguagesHint": "Bilatu helburu hizkuntzak",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Galderak? Hemen gaude laguntzeko!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Zerbait oker joan da, eta horren konponketan lan gogorra egiten ari gara. Begiratu berriro geroago.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:13.031797",
"@@last_modified": "2026-01-28 13:26:49.168597",
"repeatPassword": "تکرار رمزعبور",
"@repeatPassword": {},
"about": "درباره",
@ -11666,5 +11666,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "شما در حال یادگیری چه زبانی هستید؟",
"searchLanguagesHint": "زبان‌های هدف را جستجو کنید",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "سوالات؟ ما اینجا هستیم تا کمک کنیم!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "مشکلی پیش آمده و ما در حال تلاش برای رفع آن هستیم. بعداً دوباره بررسی کنید.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4008,7 +4008,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-27 14:02:18.192045",
"@@last_modified": "2026-01-28 13:25:57.438673",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11557,5 +11557,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Mitä kieltä opit?",
"searchLanguagesHint": "Etsi kohdekieliä",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Kysymyksiä? Olemme täällä auttamassa!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Jotain meni pieleen, ja teemme kovasti töitä sen korjaamiseksi. Tarkista myöhemmin uudelleen.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2786,7 +2786,7 @@
"selectAll": "Piliin lahat",
"deselectAll": "Huwag piliin lahat",
"@@locale": "fil",
"@@last_modified": "2026-01-27 14:02:56.958154",
"@@last_modified": "2026-01-28 13:26:32.216257",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
@ -11910,5 +11910,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Anong wika ang iyong pinag-aaralan?",
"searchLanguagesHint": "Maghanap ng mga target na wika",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "May mga tanong? Nandito kami para tumulong!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "May nangyaring mali, at abala kami sa pag-aayos nito. Suriin muli mamaya.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "fr",
"@@last_modified": "2026-01-27 14:03:27.361514",
"@@last_modified": "2026-01-28 13:27:03.910294",
"about": "À propos",
"@about": {
"type": "String",
@ -11258,5 +11258,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Quelle langue apprenez-vous ?",
"searchLanguagesHint": "Recherchez des langues cibles",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Des questions ? Nous sommes là pour vous aider !",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Quelque chose a mal tourné, et nous travaillons dur pour le réparer. Vérifiez à nouveau plus tard.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4516,7 +4516,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-27 14:03:26.279031",
"@@last_modified": "2026-01-28 13:27:02.604512",
"@customReaction": {
"type": "String",
"placeholders": {}
@ -10932,5 +10932,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Cén teanga atá á foghlaim agat?",
"searchLanguagesHint": "Cuardaigh teangacha sprioc",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Ceisteanna? Táimid anseo chun cabhrú!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Tharla rud éigin mícheart, agus táimid ag obair go dian chun é a shocrú. Seiceáil arís níos déanaí.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "gl",
"@@last_modified": "2026-01-27 14:02:17.041137",
"@@last_modified": "2026-01-28 13:25:56.077589",
"about": "Acerca de",
"@about": {
"type": "String",
@ -10931,5 +10931,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Que idioma estás aprendendo?",
"searchLanguagesHint": "Busca idiomas de destino",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "¿Preguntas? Estamos aquí para axudar!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Algo saíu mal e estamos traballando duro para solucionalo. Comproba de novo máis tarde.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:31.662677",
"@@last_modified": "2026-01-28 13:26:06.755080",
"about": "אודות",
"@about": {
"type": "String",
@ -11983,5 +11983,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "איזו שפה אתה לומד?",
"searchLanguagesHint": "חפש שפות יעד",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "שאלות? אנחנו כאן כדי לעזור!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "משהו השתבש, ואנחנו עובדים קשה על תיקון זה. בדוק שוב מאוחר יותר.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4482,7 +4482,7 @@
"playWithAI": "अभी के लिए एआई के साथ खेलें",
"courseStartDesc": "पैंजिया बॉट कभी भी जाने के लिए तैयार है!\n\n...लेकिन दोस्तों के साथ सीखना बेहतर है!",
"@@locale": "hi",
"@@last_modified": "2026-01-27 14:03:20.292018",
"@@last_modified": "2026-01-28 13:26:55.548010",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -12019,5 +12019,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "आप कौन सी भाषा सीख रहे हैं?",
"searchLanguagesHint": "लक्षित भाषाएँ खोजें",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "प्रश्न? हम आपकी मदद के लिए यहाँ हैं!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "कुछ गलत हो गया है, और हम इसे ठीक करने में कड़ी मेहनत कर रहे हैं। बाद में फिर से जांचें।",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "hr",
"@@last_modified": "2026-01-27 14:02:30.204264",
"@@last_modified": "2026-01-28 13:26:05.722028",
"about": "Informacije",
"@about": {
"type": "String",
@ -11306,5 +11306,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Koji jezik učite?",
"searchLanguagesHint": "Pretraži ciljne jezike",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Imate pitanja? Tu smo da pomognemo!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Nešto je pošlo po zlu i marljivo radimo na rješavanju problema. Provjerite ponovo kasnije.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "hu",
"@@last_modified": "2026-01-27 14:02:22.464338",
"@@last_modified": "2026-01-28 13:25:59.737331",
"about": "Névjegy",
"@about": {
"type": "String",
@ -10935,5 +10935,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Milyen nyelvet tanulsz?",
"searchLanguagesHint": "Keresd a célnyelveket",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Kérdése van? Itt vagyunk, hogy segítsünk!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Valami hiba történt, és keményen dolgozunk a javításon. Kérlek, nézd meg később.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1957,7 +1957,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-27 14:02:33.390978",
"@@last_modified": "2026-01-28 13:26:08.333302",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -12012,5 +12012,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Kia lingvo vi lernas?",
"searchLanguagesHint": "Serĉu celajn lingvojn",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Kwestyon? Nou la pou ede!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "N'ayen a fau, e n'ayen a t'awen a t'awen a t'awen. T'awen a t'awen a t'awen.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:23.466673",
"@@last_modified": "2026-01-28 13:26:00.772325",
"setAsCanonicalAlias": "Atur sebagai alias utama",
"@setAsCanonicalAlias": {
"type": "String",
@ -10925,5 +10925,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Bahasa apa yang Anda pelajari?",
"searchLanguagesHint": "Cari bahasa target",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Pertanyaan? Kami di sini untuk membantu!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Ada yang tidak beres, dan kami sedang bekerja keras untuk memperbaikinya. Periksa lagi nanti.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4371,7 +4371,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-27 14:02:28.999815",
"@@last_modified": "2026-01-28 13:26:04.525215",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11908,5 +11908,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Cén teanga atá á foghlaim agat?",
"searchLanguagesHint": "Cuardaigh teangacha sprioc",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Ceisteanna? Táimid anseo chun cabhrú!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Níl aon rud ag dul i gceart, agus táimid ag obair go dian chun é a shocrú. Seiceáil arís níos déanaí.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:43.998266",
"@@last_modified": "2026-01-28 13:26:18.238331",
"about": "Informazioni",
"@about": {
"type": "String",
@ -10937,5 +10937,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Quale lingua stai imparando?",
"searchLanguagesHint": "Cerca lingue target",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Domande? Siamo qui per aiutarti!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Qualcosa è andato storto e stiamo lavorando duramente per risolverlo. Controlla di nuovo più tardi.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "ja",
"@@last_modified": "2026-01-27 14:03:18.964652",
"@@last_modified": "2026-01-28 13:26:54.166864",
"about": "このアプリについて",
"@about": {
"type": "String",
@ -11724,5 +11724,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "どの言語を学んでいますか?",
"searchLanguagesHint": "ターゲット言語を検索",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "質問がありますか?私たちはお手伝いします!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "何かがうまくいかなかったため、私たちは修正作業に取り組んでいます。後で再度確認してください。",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2593,7 +2593,7 @@
"playWithAI": "ამ დროისთვის ითამაშეთ AI-თან",
"courseStartDesc": "Pangea Bot მზადაა ნებისმიერ დროს გასასვლელად!\n\n...მაგრამ სწავლა უკეთესია მეგობრებთან ერთად!",
"@@locale": "ka",
"@@last_modified": "2026-01-27 14:03:23.702338",
"@@last_modified": "2026-01-28 13:27:00.130109",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11964,5 +11964,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "რომელი ენა სწავლობთ?",
"searchLanguagesHint": "ძებნა მიზნობრივი ენების",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "კითხვები? ჩვენ აქ ვართ, რომ დაგეხმაროთ!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "რამე არასწორად მოხდა, და ჩვენ აქტიურად ვმუშაობთ ამის გამოსასწორებლად. შეამოწმეთ მოგვიანებით.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:13.792128",
"@@last_modified": "2026-01-28 13:25:53.869320",
"about": "소개",
"@about": {
"type": "String",
@ -11042,5 +11042,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "어떤 언어를 배우고 있나요?",
"searchLanguagesHint": "목표 언어 검색",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "질문이 있으신가요? 저희가 도와드리겠습니다!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "문제가 발생했으며, 우리는 이를 해결하기 위해 열심히 작업하고 있습니다. 나중에 다시 확인해 주세요.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -3860,7 +3860,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-27 14:03:05.189233",
"@@last_modified": "2026-01-28 13:26:39.413825",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11739,5 +11739,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Kokią kalbą mokotės?",
"searchLanguagesHint": "Ieškoti tikslo kalbų",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Klausimai? Mes čia, kad padėtume!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Kažkas nepavyko, ir mes sunkiai dirbame, kad tai išspręstume. Patikrinkite vėliau.",
"@courseLoadingError": {
"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-27 14:02:58.530525",
"@@last_modified": "2026-01-28 13:26:33.690801",
"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",
@ -10920,5 +10920,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Kuru valodu tu mācies?",
"searchLanguagesHint": "Meklēt mērķa valodas",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Jautājumi? Mēs esam šeit, lai palīdzētu!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Kaut kas nogāja greizi, un mēs smagi strādājam, lai to labotu. Pārbaudiet vēlāk.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:48.767207",
"@@last_modified": "2026-01-28 13:26:21.535919",
"about": "Om",
"@about": {
"type": "String",
@ -12027,5 +12027,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Hvilket språk lærer du?",
"searchLanguagesHint": "Søk etter målspråk",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Spørsmål? Vi er her for å hjelpe!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Noe gikk galt, og vi jobber hardt med å fikse det. Sjekk igjen senere.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:08.964846",
"@@last_modified": "2026-01-28 13:26:43.074504",
"about": "Over ons",
"@about": {
"type": "String",
@ -10934,5 +10934,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Welke taal ben je aan het leren?",
"searchLanguagesHint": "Zoek doeltalen",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Vragen? We zijn hier om te helpen!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Er is iets misgegaan en we zijn hard aan het werk om het op te lossen. Kijk later nog eens.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "pl",
"@@last_modified": "2026-01-27 14:03:14.463461",
"@@last_modified": "2026-01-28 13:26:50.545730",
"about": "O aplikacji",
"@about": {
"type": "String",
@ -10932,5 +10932,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Jakiego języka się uczysz?",
"searchLanguagesHint": "Szukaj języków docelowych",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Pytania? Jesteśmy tutaj, aby pomóc!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Coś poszło nie tak, a my ciężko pracujemy nad naprawą. Sprawdź ponownie później.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:37.702228",
"@@last_modified": "2026-01-28 13:26:11.994676",
"copiedToClipboard": "Copiada para a área de transferência",
"@copiedToClipboard": {
"type": "String",
@ -12034,5 +12034,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Qual idioma você está aprendendo?",
"searchLanguagesHint": "Pesquise idiomas-alvo",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Dúvidas? Estamos aqui para ajudar!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Algo deu errado, e estamos trabalhando arduamente para corrigir isso. Verifique novamente mais tarde.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:34.316481",
"@@last_modified": "2026-01-28 13:26:09.473589",
"about": "Sobre",
"@about": {
"type": "String",
@ -11292,5 +11292,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Qual idioma você está aprendendo?",
"searchLanguagesHint": "Pesquise idiomas-alvo",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Dúvidas? Estamos aqui para ajudar!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Algo deu errado, e estamos trabalhando duro para consertar. Verifique novamente mais tarde.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -3330,7 +3330,7 @@
"selectAll": "Selecionar tudo",
"deselectAll": "Desmarcar tudo",
"@@locale": "pt_PT",
"@@last_modified": "2026-01-27 14:02:54.292550",
"@@last_modified": "2026-01-28 13:26:26.329625",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11963,5 +11963,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Qual idioma você está aprendendo?",
"searchLanguagesHint": "Pesquise idiomas-alvo",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Dúvidas? Estamos aqui para ajudar!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Algo deu errado, e estamos trabalhando arduamente para corrigir isso. Verifique novamente mais tarde.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:25.277330",
"@@last_modified": "2026-01-28 13:26:01.958335",
"about": "Despre",
"@about": {
"type": "String",
@ -11669,5 +11669,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Ce limbă înveți?",
"searchLanguagesHint": "Caută limbi țintă",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Întrebări? Suntem aici să ajutăm!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Ceva a mers prost și lucrăm din greu pentru a remedia problema. Verifică din nou mai târziu.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "ru",
"@@last_modified": "2026-01-27 14:03:22.446889",
"@@last_modified": "2026-01-28 13:26:58.889837",
"about": "О проекте",
"@about": {
"type": "String",
@ -11042,5 +11042,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Какой язык вы изучаете?",
"searchLanguagesHint": "Поиск целевых языков",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Вопросы? Мы здесь, чтобы помочь!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Что-то пошло не так, и мы усердно работаем над исправлением. Проверьте позже.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "sk",
"@@last_modified": "2026-01-27 14:02:27.134602",
"@@last_modified": "2026-01-28 13:26:03.157059",
"about": "O aplikácii",
"@about": {
"type": "String",
@ -12018,5 +12018,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Aký jazyk sa učíte?",
"searchLanguagesHint": "Hľadajte cieľové jazyky",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Otázky? Sme tu, aby sme pomohli!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Niečo sa pokazilo a my na tom tvrdo pracujeme, aby sme to opravili. Skontrolujte to neskôr.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2463,7 +2463,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-27 14:02:41.198883",
"@@last_modified": "2026-01-28 13:26:15.797667",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -12015,5 +12015,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Katero jezika se učiš?",
"searchLanguagesHint": "Išči ciljne jezike",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Vprašanja? Tu smo, da pomagamo!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Nekaj je šlo narobe in trdo delamo na tem, da to popravimo. Preverite znova kasneje.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:24.807506",
"@@last_modified": "2026-01-28 13:27:01.338972",
"about": "О програму",
"@about": {
"type": "String",
@ -12036,5 +12036,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Koji jezik učite?",
"searchLanguagesHint": "Pretraži ciljne jezike",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Pitanja? Tu smo da pomognemo!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Nešto je pošlo po zlu, i mi marljivo radimo na rešenju. Proverite ponovo kasnije.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:15.575050",
"@@last_modified": "2026-01-28 13:26:51.814505",
"about": "Om",
"@about": {
"type": "String",
@ -11412,5 +11412,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Vilket språk lär du dig?",
"searchLanguagesHint": "Sök efter målspråk",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Frågor? Vi är här för att hjälpa till!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Något gick fel, och vi arbetar hårt för att åtgärda det. Kolla igen senare.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:07.633079",
"@@last_modified": "2026-01-28 13:26:42.100203",
"acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது",
"@acceptedTheInvitation": {
"type": "String",
@ -11158,5 +11158,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "நீங்கள் எது மொழி கற்றுக்கொள்கிறீர்கள்?",
"searchLanguagesHint": "இலக்கு மொழிகளை தேடுங்கள்",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "கேள்விகள்? நாங்கள் உதவ இங்கே இருக்கிறோம்!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "எதோ தவறு ஏற்பட்டது, அதை சரிசெய்ய நாங்கள் கடுமையாக வேலை செய்கிறோம். பின்னர் மீண்டும் சரிபார்க்கவும்.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1919,7 +1919,7 @@
"playWithAI": "ఇప్పుడే AI తో ఆడండి",
"courseStartDesc": "పాంజియా బాట్ ఎప్పుడైనా సిద్ధంగా ఉంటుంది!\n\n...కానీ స్నేహితులతో నేర్చుకోవడం మెరుగైనది!",
"@@locale": "te",
"@@last_modified": "2026-01-27 14:03:02.156703",
"@@last_modified": "2026-01-28 13:26:38.102435",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
@ -12023,5 +12023,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "మీరు ఏ భాష నేర్చుకుంటున్నారు?",
"searchLanguagesHint": "లక్ష్య భాషలను శోధించండి",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "ప్రశ్నలు? మేము మీకు సహాయం చేయడానికి ఇక్కడ ఉన్నాము!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "ఏదో తప్పు జరిగింది, మరియు మేము దీన్ని సరిదిద్దడానికి కష్టపడుతున్నాము. తర్వాత మళ్లీ తనిఖీ చేయండి.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -4455,7 +4455,7 @@
"playWithAI": "เล่นกับ AI ชั่วคราว",
"courseStartDesc": "Pangea Bot พร้อมที่จะเริ่มต้นได้ทุกเมื่อ!\n\n...แต่การเรียนรู้ดีกว่ากับเพื่อน!",
"@@locale": "th",
"@@last_modified": "2026-01-27 14:02:53.265962",
"@@last_modified": "2026-01-28 13:26:25.241346",
"@alwaysUse24HourFormat": {
"type": "String",
"placeholders": {}
@ -11992,5 +11992,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "คุณกำลังเรียนภาษาอะไรอยู่?",
"searchLanguagesHint": "ค้นหาภาษาที่ต้องการ",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "มีคำถามไหม? เราพร้อมที่จะช่วยเหลือ!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "มีบางอย่างผิดพลาด และเรากำลังทำงานอย่างหนักเพื่อแก้ไข ตรวจสอบอีกครั้งในภายหลัง.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "tr",
"@@last_modified": "2026-01-27 14:03:00.927147",
"@@last_modified": "2026-01-28 13:26:36.845268",
"about": "Hakkında",
"@about": {
"type": "String",
@ -11156,5 +11156,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Hangi dili öğreniyorsunuz?",
"searchLanguagesHint": "Hedef dilleri arayın",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Sorular mı? Yardımcı olmaya buradayız!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Bir şeyler yanlış gitti ve biz bunu düzeltmek için yoğun bir şekilde çalışıyoruz. Lütfen daha sonra tekrar kontrol edin.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "uk",
"@@last_modified": "2026-01-27 14:02:45.151936",
"@@last_modified": "2026-01-28 13:26:19.427065",
"about": "Про застосунок",
"@about": {
"type": "String",
@ -10928,5 +10928,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Яку мову ви вивчаєте?",
"searchLanguagesHint": "Шукати цільові мови",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Питання? Ми тут, щоб допомогти!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Щось пішло не так, і ми наполегливо працюємо над виправленням. Перевірте пізніше.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:03:06.468185",
"@@last_modified": "2026-01-28 13:26:40.659203",
"about": "Giới thiệu",
"@about": {
"type": "String",
@ -6504,5 +6504,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "Bạn đang học ngôn ngữ nào?",
"searchLanguagesHint": "Tìm kiếm ngôn ngữ mục tiêu",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "Câu hỏi? Chúng tôi ở đây để giúp đỡ!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "Đã xảy ra sự cố, và chúng tôi đang nỗ lực khắc phục. Vui lòng kiểm tra lại sau.",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1855,7 +1855,7 @@
"selectAll": "全選",
"deselectAll": "取消全選",
"@@locale": "yue",
"@@last_modified": "2026-01-27 14:02:42.486712",
"@@last_modified": "2026-01-28 13:26:16.825978",
"@ignoreUser": {
"type": "String",
"placeholders": {}
@ -12025,5 +12025,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "你正在學習什麼語言?",
"searchLanguagesHint": "搜尋目標語言",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "有問題嗎?我們在這裡幫助你!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "發生了一些問題,我們正在努力修復。稍後再檢查。",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,6 +1,6 @@
{
"@@locale": "zh",
"@@last_modified": "2026-01-27 14:03:10.872296",
"@@last_modified": "2026-01-28 13:26:45.843538",
"about": "关于",
"@about": {
"type": "String",
@ -10925,5 +10925,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "你正在学习什么语言?",
"searchLanguagesHint": "搜索目标语言",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "有问题吗?我们在这里帮助您!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "出现了一些问题,我们正在努力修复。请稍后再检查。",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2026-01-27 14:02:55.518251",
"@@last_modified": "2026-01-28 13:26:27.558260",
"about": "關於",
"@about": {
"type": "String",
@ -10932,5 +10932,25 @@
"@newCourseAccess": {
"type": "String",
"placeholders": {}
},
"onboardingLanguagesTitle": "你正在學習什麼語言?",
"searchLanguagesHint": "搜尋目標語言",
"@onboardingLanguagesTitle": {
"type": "String",
"placeholders": {}
},
"@searchLanguagesHint": {
"type": "String",
"placeholders": {}
},
"supportSubtitle": "有問題嗎?我們在這裡幫助您!",
"@supportSubtitle": {
"type": "String",
"placeholders": {}
},
"courseLoadingError": "發生了一些問題,我們正在努力修復。稍後再檢查。",
"@courseLoadingError": {
"type": "String",
"placeholders": {}
}
}

View file

@ -2015,18 +2015,6 @@ class ChatController extends State<ChatPageWithRoom>
bool get _isToolbarOpen =>
MatrixState.pAnyState.isOverlayOpen(RegExp(r'^message_toolbar_overlay$'));
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;
}
if (event.messageType == MessageTypes.Audio) {
return !InstructionsEnum.clickAudioMessages.isToggledOff;
}
return false;
}
void showToolbar(
Event event, {
PangeaMessageEvent? pangeaMessageEvent,
@ -2059,14 +2047,8 @@ class ChatController extends State<ChatPageWithRoom>
);
// you've clicked a message so lets turn this off
InstructionsEnum.clickMessage.setToggledOff(true);
if (event.messageType == MessageTypes.Text &&
!InstructionsEnum.clickTextMessages.isToggledOff) {
InstructionsEnum.clickTextMessages.setToggledOff(true);
}
if (event.messageType == MessageTypes.Audio &&
!InstructionsEnum.clickAudioMessages.isToggledOff) {
InstructionsEnum.clickAudioMessages.setToggledOff(true);
if (!InstructionsEnum.clickMessage.isToggledOff) {
InstructionsEnum.clickMessage.setToggledOff(true);
}
if (!kIsWeb) {

View file

@ -11,8 +11,10 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/toolbar/layout/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/token_practice_button.dart';
@ -446,6 +448,12 @@ class HtmlMessage extends StatelessWidget {
: false;
final isNew = token != null && newTokens.contains(token.text);
final isFirstNewToken = isNew &&
controller.buttonEventID == event.eventId &&
newTokens.first == token.text;
final showShimmer =
!InstructionsEnum.shimmerNewToken.isToggledOff && isFirstNewToken;
final tokenWidth = renderer.tokenTextWidthForContainer(
node.text,
Theme.of(context).colorScheme.primary.withAlpha(200),
@ -500,19 +508,25 @@ class HtmlMessage extends StatelessWidget {
: null,
child: HoverBuilder(
builder: (context, hovered) {
return UnderlineText(
text: node.text.trim(),
style: existingStyle,
linkStyle: linkStyle,
textDirection: pangeaMessageEvent?.textDirection,
underlineColor: TokenRenderingUtil.underlineColor(
underlineColor,
selected: selected,
highlighted: highlighted,
isNew: isNew,
practiceMode: readingAssistanceMode ==
ReadingAssistanceMode.practiceMode,
hovered: hovered,
return ShimmerBackground(
enabled: showShimmer,
borderRadius: BorderRadius.circular(4.0),
child: UnderlineText(
text: node.text.trim(),
style: existingStyle,
linkStyle: linkStyle,
textDirection:
pangeaMessageEvent?.textDirection,
underlineColor:
TokenRenderingUtil.underlineColor(
underlineColor,
selected: selected,
highlighted: highlighted,
isNew: isNew,
practiceMode: readingAssistanceMode ==
ReadingAssistanceMode.practiceMode,
hovered: hovered,
),
),
);
},

View file

@ -17,7 +17,6 @@ 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';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
@ -603,228 +602,223 @@ class Message extends StatelessWidget {
child: ValueListenableBuilder(
valueListenable: controller
.depressMessageButton,
// #Pangea
child: ShimmerBackground(
enabled: controller
.showMessageShimmer(
event,
child: Container(
decoration: BoxDecoration(
color: noBubble
? Colors.transparent
: color,
borderRadius:
borderRadius,
),
// Pangea#
child: Container(
decoration:
BoxDecoration(
color: noBubble
? Colors
.transparent
: color,
borderRadius:
borderRadius,
),
clipBehavior:
Clip.antiAlias,
// #Pangea
child:
CompositedTransformTarget(
link: MatrixState
clipBehavior:
Clip.antiAlias,
// #Pangea
child:
CompositedTransformTarget(
link: MatrixState
.pAnyState
.layerLinkAndKey(
event.eventId,
)
.link,
// child: BubbleBackground(
// colors: colors,
// ignore: noBubble || !ownMessage,
// scrollController: scrollController,
// Pangea#
child: Container(
// #Pangea
key: MatrixState
.pAnyState
.layerLinkAndKey(
event.eventId,
)
.link,
// child: BubbleBackground(
// colors: colors,
// ignore: noBubble || !ownMessage,
// scrollController: scrollController,
.key,
// Pangea#
child: Container(
// #Pangea
key: MatrixState
.pAnyState
.layerLinkAndKey(
event.eventId,
)
.key,
// Pangea#
decoration:
BoxDecoration(
borderRadius:
BorderRadius
.circular(
AppConfig
.borderRadius,
),
decoration:
BoxDecoration(
borderRadius:
BorderRadius
.circular(
AppConfig
.borderRadius,
),
constraints:
const BoxConstraints(
maxWidth: FluffyThemes
.columnWidth *
1.5,
),
child: Column(
mainAxisSize:
MainAxisSize
.min,
crossAxisAlignment:
CrossAxisAlignment
.start,
children: <Widget>[
if ({
RelationshipTypes
.reply,
RelationshipTypes
.thread,
}.contains(
event
.relationshipType,
))
FutureBuilder<
Event?>(
future: event
.getReplyEvent(
timeline,
),
builder: (
BuildContext
context,
snapshot,
) {
final replyEvent = snapshot
.hasData
? snapshot
.data!
: Event(
eventId: event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...',
},
// #Pangea
// senderId: event
// .senderId,
senderId: "",
// Pangea#
type: 'm.room.message',
room: event.room,
status: EventStatus.sent,
originServerTs: DateTime.now(),
);
return Padding(
padding:
const EdgeInsets.only(
left:
16,
right:
16,
top:
8,
),
),
constraints:
const BoxConstraints(
maxWidth: FluffyThemes
.columnWidth *
1.5,
),
child: Column(
mainAxisSize:
MainAxisSize
.min,
crossAxisAlignment:
CrossAxisAlignment
.start,
children: <Widget>[
if ({
RelationshipTypes
.reply,
RelationshipTypes
.thread,
}.contains(
event
.relationshipType,
))
FutureBuilder<
Event?>(
future: event
.getReplyEvent(
timeline,
),
builder: (
BuildContext
context,
snapshot,
) {
final replyEvent = snapshot
.hasData
? snapshot
.data!
: Event(
eventId:
event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...',
},
// #Pangea
// senderId: event
// .senderId,
senderId:
"",
// Pangea#
type:
'm.room.message',
room:
event.room,
status:
EventStatus.sent,
originServerTs:
DateTime.now(),
);
return Padding(
padding:
const EdgeInsets
.only(
left:
16,
right:
16,
top: 8,
),
child:
Material(
color: Colors
.transparent,
borderRadius:
ReplyContent.borderRadius,
child:
Material(
color:
Colors.transparent,
InkWell(
borderRadius:
ReplyContent.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
),
child:
InkWell(
borderRadius:
ReplyContent.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
),
AbsorbPointer(
child:
AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
),
),
);
},
),
MessageContent(
displayEvent,
textColor:
textColor,
linkColor:
linkColor,
onInfoTab:
onInfoTab,
borderRadius:
borderRadius,
timeline:
timeline,
selected:
selected,
// #Pangea
pangeaMessageEvent:
pangeaMessageEvent,
controller:
controller,
nextEvent:
nextEvent,
prevEvent:
previousEvent,
// Pangea#
),
);
},
),
if (event
.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
))
Padding(
padding:
const EdgeInsets
.only(
bottom:
8.0,
left:
16.0,
right:
16.0,
),
child: Row(
mainAxisSize:
MainAxisSize
.min,
spacing:
4.0,
children: [
Icon(
Icons
.edit_outlined,
MessageContent(
displayEvent,
textColor:
textColor,
linkColor:
linkColor,
onInfoTab:
onInfoTab,
borderRadius:
borderRadius,
timeline:
timeline,
selected:
selected,
// #Pangea
pangeaMessageEvent:
pangeaMessageEvent,
controller:
controller,
nextEvent:
nextEvent,
prevEvent:
previousEvent,
// Pangea#
),
if (event
.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
))
Padding(
padding:
const EdgeInsets
.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisSize:
MainAxisSize
.min,
spacing:
4.0,
children: [
Icon(
Icons
.edit_outlined,
color: textColor
.withAlpha(
164,
),
size:
14,
),
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
),
style:
TextStyle(
color:
textColor.withAlpha(
164,
),
size:
14,
fontSize:
11,
),
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
),
style:
TextStyle(
color:
textColor.withAlpha(
164,
),
fontSize:
11,
),
),
],
),
),
],
),
],
),
),
],
),
),
),

View file

@ -380,6 +380,7 @@ class ChatListViewBody extends StatelessWidget {
.setToggledOff(true),
),
title: Text(L10n.of(context).chatWithSupport),
subtitle: Text(L10n.of(context).supportSubtitle),
onTap: () async {
await showFutureLoadingDialog(
context: context,

View file

@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.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/pangea/user/user_search_extension.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/platform_infos.dart';
@ -52,8 +53,11 @@ class NewPrivateChatController extends State<NewPrivateChat> {
}
Future<List<Profile>> _searchUser(String searchTerm) async {
final result =
await Matrix.of(context).client.searchUserDirectory(searchTerm);
// #Pangea
// final result =
// await Matrix.of(context).client.searchUserDirectory(searchTerm);
final result = await Matrix.of(context).client.searchUser(searchTerm);
// Pangea#
final profiles = result.results;
if (searchTerm.isValidMatrixId &&

View file

@ -19,12 +19,26 @@ class SpaceCodeOnboardingView extends StatelessWidget {
Widget build(BuildContext context) {
return PangeaLoginScaffold(
customAppBar: AppBar(
leading: BackButton(
onPressed: () => pLogoutAction(
context,
bypassWarning: true,
title: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BackButton(
onPressed: () => pLogoutAction(
context,
bypassWarning: true,
),
),
const SizedBox(
width: 40.0,
),
],
),
),
automaticallyImplyLeading: false,
),
showAppName: false,
mainAssetUrl: controller.profile?.avatarUrl,

View file

@ -353,7 +353,8 @@ extension ActivityRoomExtension on Room {
bool get isActivitySession =>
(roomType?.startsWith(PangeaRoomTypes.activitySession) == true ||
activityPlan != null) &&
activityPlan?.isDeprecatedModel == false;
activityPlan?.isDeprecatedModel == false &&
activityPlan?.activityId != null;
String? get activityId {
if (!isActivitySession) return null;

View file

@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/languages/p_language_store.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityFinishedStatusMessage extends StatelessWidget {
@ -28,10 +29,17 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
Future<void> _archiveToAnalytics() async {
try {
final activityPlan = controller.room.activityPlan;
if (activityPlan == null) {
throw Exception("No activity plan found for room");
}
final lang = activityPlan.req.targetLanguage.split("-").first;
final langModel = PLanguageStore.byLangCode(lang)!;
await controller.room.archiveActivity();
await MatrixState
.pangeaController.matrixState.analyticsDataService.updateService
.sendActivityAnalytics(controller.room.id);
.sendActivityAnalytics(controller.room.id, langModel);
} catch (e, s) {
ErrorHandler.logError(
e: e,

View file

@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activ
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/bot_join_error_dialog.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_request.dart';
@ -191,7 +192,7 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
final availableRoles = activity!.roles;
final assignedRoles = activityRoom?.assignedRoles ??
roomSummaries?[widget.roomId]?.activityRoles.roles ??
roomSummaries?[widget.roomId]?.activityRoles?.roles ??
{};
final unassignedIds = availableRoles.keys
.where((id) => !assignedRoles.containsKey(id))
@ -293,8 +294,17 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
);
}
await Future.wait(futures);
} catch (e) {
} catch (e, s) {
error = e;
ErrorHandler.logError(
e: e,
s: s,
data: {
"activityId": widget.activityId,
"roomId": widget.roomId,
"parentId": widget.parentId,
},
);
} finally {
if (mounted) {
setState(() => loading = false);

View file

@ -518,7 +518,7 @@ class _ActivityStatuses extends StatelessWidget {
// room (like the bot). Otherwise, show only joined users with roles
Map<String, ActivityRoleModel> activityRoles =
status == ActivitySummaryStatus.completed
? e.value.activityRoles.roles
? (e.value.activityRoles?.roles ?? {})
: e.value.joinedUsersWithRoles;
// If the user is in the activity room and it's not completed, use the room's
@ -530,7 +530,7 @@ class _ActivityStatuses extends StatelessWidget {
return ListTile(
title: OpenRolesIndicator(
roles: activityPlan.roles.values
roles: (activityPlan?.roles.values ?? [])
.sorted((a, b) => a.id.compareTo(b.id))
.toList(),
assignedRoles: activityRoles.values.toList(),

View file

@ -128,12 +128,14 @@ class AnalyticsUpdateService {
await future;
}
Future<void> sendActivityAnalytics(String roomId) async {
final analyticsRoom = await _getAnalyticsRoom();
Future<void> sendActivityAnalytics(String roomId, LanguageModel lang) async {
final analyticsRoom = await _getAnalyticsRoom(l2Override: lang);
if (analyticsRoom == null) return;
await analyticsRoom.addActivityRoomId(roomId);
dataService.updateDispatcher.sendActivityAnalyticsUpdate(roomId);
if (lang.langCodeShort == _l2?.langCodeShort) {
dataService.updateDispatcher.sendActivityAnalyticsUpdate(roomId);
}
}
Future<void> blockConstruct(ConstructIdentifier constructId) async {

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
@ -50,42 +49,110 @@ class ExampleMessageUtil {
String? form,
PangeaMessageEvent messageEvent,
) {
PangeaToken? token;
String? text;
List<PangeaToken>? tokens;
int targetTokenIndex = -1;
if (messageEvent.isAudioMessage) {
final stt = messageEvent.getSpeechToTextLocal();
if (stt == null) return null;
final tokens = stt.transcript.sttTokens.map((t) => t.token).toList();
token = tokens.firstWhereOrNull(
(token) => token.text.content == form,
);
tokens = stt.transcript.sttTokens.map((t) => t.token).toList();
targetTokenIndex = tokens.indexWhere((t) => t.text.content == form);
text = stt.transcript.text;
} else {
final tokens = messageEvent.messageDisplayRepresentation?.tokens;
tokens = messageEvent.messageDisplayRepresentation?.tokens;
if (tokens == null || tokens.isEmpty) return null;
token = tokens.firstWhereOrNull(
(token) => token.text.content == form,
);
targetTokenIndex = tokens.indexWhere((t) => t.text.content == form);
text = messageEvent.messageDisplayText;
}
if (token == null) return null;
if (targetTokenIndex == -1) {
return null;
}
final before = text.characters.take(token.text.offset).toString();
final after = text.characters
.skip(token.text.offset + token.text.content.characters.length)
final targetToken = tokens[targetTokenIndex];
const maxContextChars = 100;
final targetStart = targetToken.text.offset;
final targetEnd = targetStart + targetToken.text.content.characters.length;
final totalChars = text.characters.length;
final beforeAvailable = targetStart;
final afterAvailable = totalChars - targetEnd;
// ---------- Dynamic budget split ----------
int beforeBudget = maxContextChars ~/ 2;
int afterBudget = maxContextChars - beforeBudget;
if (beforeAvailable < beforeBudget) {
afterBudget += beforeBudget - beforeAvailable;
beforeBudget = beforeAvailable;
} else if (afterAvailable < afterBudget) {
beforeBudget += afterBudget - afterAvailable;
afterBudget = afterAvailable;
}
// ---------- BEFORE ----------
int beforeStartOffset = 0;
bool trimmedBefore = false;
if (beforeAvailable > beforeBudget) {
final desiredStart = targetStart - beforeBudget;
for (int i = 0; i < targetTokenIndex; i++) {
final token = tokens[i];
final tokenEnd =
token.text.offset + token.text.content.characters.length;
if (tokenEnd > desiredStart) {
beforeStartOffset = token.text.offset;
trimmedBefore = true;
break;
}
}
}
final before = text.characters
.skip(beforeStartOffset)
.take(targetStart - beforeStartOffset)
.toString();
// ---------- AFTER ----------
int afterEndOffset = totalChars;
bool trimmedAfter = false;
if (afterAvailable > afterBudget) {
final desiredEnd = targetEnd + afterBudget;
for (int i = targetTokenIndex + 1; i < tokens.length; i++) {
final token = tokens[i];
if (token.text.offset >= desiredEnd) {
afterEndOffset = token.text.offset;
trimmedAfter = true;
break;
}
}
}
final after = text.characters
.skip(targetEnd)
.take(afterEndOffset - targetEnd)
.toString()
.trimRight();
return [
if (trimmedBefore) const TextSpan(text: ''),
TextSpan(text: before),
TextSpan(
text: token.text.content,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
text: targetToken.text.content,
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: after),
if (trimmedAfter) const TextSpan(text: ''),
];
}
}

View file

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
@ -25,6 +23,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_activity_model.da
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';
class AnalyticsPracticeView extends StatelessWidget {
final AnalyticsPracticeState controller;
@ -113,110 +112,78 @@ class _AnalyticsActivityView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
final isColumnMode = FluffyThemes.isColumnMode(context);
TextStyle? titleStyle = isColumnMode
? Theme.of(context).textTheme.titleLarge
: Theme.of(context).textTheme.titleMedium;
titleStyle = titleStyle?.copyWith(fontWeight: FontWeight.bold);
return ListView(
children: [
//per-activity instructions, add switch statement once there are more types
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.selectMeaning,
padding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 24.0,
vertical: 8.0,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ValueListenableBuilder(
valueListenable: controller.activityTarget,
builder: (context, target, __) => target != null
? Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
SizedBox(
height: 75.0,
child: ValueListenableBuilder(
valueListenable: controller.activityTarget,
builder: (context, target, __) => target != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
target.promptText(context),
textAlign: TextAlign.center,
style: titleStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (controller.widget.type == ConstructTypeEnum.vocab)
PhoneticTranscriptionWidget(
text:
target.target.tokens.first.vocabConstructID.lemma,
textLanguage: MatrixState
.pangeaController.userController.userL2!,
style: const TextStyle(fontSize: 14.0),
),
child: Column(
spacing: 12.0,
children: [
Text(
target.promptText(context),
textAlign: TextAlign.center,
style: FluffyThemes.isColumnMode(context)
? Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontWeight: FontWeight.bold,
)
: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (controller.widget.type ==
ConstructTypeEnum.vocab)
PhoneticTranscriptionWidget(
text: target
.target.tokens.first.vocabConstructID.lemma,
textLanguage: MatrixState
.pangeaController.userController.userL2!,
style: const TextStyle(fontSize: 14.0),
),
],
),
)
: const SizedBox.shrink(),
),
Flexible(
fit: FlexFit.loose,
child: SingleChildScrollView(
child: _AnalyticsPracticeCenterContent(
controller: controller,
),
),
),
Expanded(
child: _ActivityChoicesWidget(controller),
),
//reserve space for grammar category morph meaning to avoid shifting, but only in those questions
AnimatedBuilder(
animation: Listenable.merge([
controller.activityState,
controller.selectedMorphChoice,
]),
builder: (context, _) {
final activityState = controller.activityState.value;
final selectedChoice = controller.selectedMorphChoice.value;
final isGrammarCategory = activityState
is AsyncLoaded<MultipleChoicePracticeActivityModel> &&
activityState.value.activityType ==
ActivityTypeEnum.grammarCategory;
if (!isGrammarCategory) {
return const SizedBox.shrink();
}
return SizedBox(
height: 125.0,
child: selectedChoice == null
? const SizedBox.shrink()
: SingleChildScrollView(
child: MorphMeaningWidget(
feature: selectedChoice.feature,
tag: selectedChoice.tag,
blankErrorFeedback: true,
),
),
);
},
),
],
],
)
: const SizedBox.shrink(),
),
),
const SizedBox(height: 16.0),
Center(
child: _AnalyticsPracticeCenterContent(controller: controller),
),
const SizedBox(height: 16.0),
_ActivityChoicesWidget(controller),
const SizedBox(height: 16.0),
ListenableBuilder(
listenable: Listenable.merge([
controller.activityState,
controller.selectedMorphChoice,
]),
builder: (context, _) {
final activityState = controller.activityState.value;
final selectedChoice = controller.selectedMorphChoice.value;
if (activityState
is! AsyncLoaded<MultipleChoicePracticeActivityModel> ||
selectedChoice == null) {
return const SizedBox.shrink();
}
return MorphMeaningWidget(
feature: selectedChoice.feature,
tag: selectedChoice.tag,
blankErrorFeedback: true,
);
},
),
],
);
}
@ -235,29 +202,39 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget {
valueListenable: controller.activityTarget,
builder: (context, target, __) => switch (target?.target.activityType) {
null => const SizedBox(),
ActivityTypeEnum.grammarError => ValueListenableBuilder(
valueListenable: controller.activityState,
builder: (context, state, __) => switch (state) {
AsyncLoaded(
value: final GrammarErrorPracticeActivityModel activity
) =>
Column(
mainAxisSize: MainAxisSize.min,
children: [
_ErrorBlankWidget(
key: ValueKey(
'${activity.eventID}_${activity.errorOffset}_${activity.errorLength}',
),
activity: activity,
ActivityTypeEnum.grammarError => SizedBox(
height: 160.0,
child: SingleChildScrollView(
child: ValueListenableBuilder(
valueListenable: controller.activityState,
builder: (context, state, __) => switch (state) {
AsyncLoaded(
value: final GrammarErrorPracticeActivityModel activity
) =>
Column(
mainAxisSize: MainAxisSize.min,
children: [
_ErrorBlankWidget(
key: ValueKey(
'${activity.eventID}_${activity.errorOffset}_${activity.errorLength}',
),
activity: activity,
),
const SizedBox(height: 12),
],
),
const SizedBox(height: 12),
],
),
_ => const SizedBox(),
},
_ => const SizedBox(),
},
),
),
),
_ => _ExampleMessageWidget(
controller.getExampleMessage(target!.target),
_ => SizedBox(
height: 100.0,
child: Center(
child: _ExampleMessageWidget(
controller.getExampleMessage(target!.target),
),
),
),
},
);
@ -333,6 +310,51 @@ class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
final errorOffset = widget.activity.errorOffset;
final errorLength = widget.activity.errorLength;
const maxContextChars = 50;
final chars = text.characters;
final totalLength = chars.length;
// ---------- BEFORE ----------
int beforeStart = 0;
bool trimmedBefore = false;
if (errorOffset > maxContextChars) {
int desiredStart = errorOffset - maxContextChars;
// Snap left to nearest whitespace to avoid cutting words
while (desiredStart > 0 && chars.elementAt(desiredStart) != ' ') {
desiredStart--;
}
beforeStart = desiredStart;
trimmedBefore = true;
}
final before =
chars.skip(beforeStart).take(errorOffset - beforeStart).toString();
// ---------- AFTER ----------
int afterEnd = totalLength;
bool trimmedAfter = false;
final errorEnd = errorOffset + errorLength;
final afterChars = totalLength - errorEnd;
if (afterChars > maxContextChars) {
int desiredEnd = errorEnd + maxContextChars;
// Snap right to nearest whitespace
while (desiredEnd < totalLength && chars.elementAt(desiredEnd) != ' ') {
desiredEnd++;
}
afterEnd = desiredEnd;
trimmedAfter = true;
}
final after = chars.skip(errorEnd).take(afterEnd - errorEnd).toString();
return Column(
children: [
Container(
@ -357,10 +379,8 @@ class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
children: [
if (errorOffset > 0)
TextSpan(
text: text.characters.take(errorOffset).toString(),
),
if (trimmedBefore) const TextSpan(text: ''),
if (before.isNotEmpty) TextSpan(text: before),
WidgetSpan(
child: Container(
height: 4.0,
@ -371,12 +391,8 @@ class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> {
),
),
),
if (errorOffset + errorLength < text.length)
TextSpan(
text: text.characters
.skip(errorOffset + errorLength)
.toString(),
),
if (after.isNotEmpty) TextSpan(text: after),
if (trimmedAfter) const TextSpan(text: ''),
],
),
),
@ -464,43 +480,29 @@ class _ActivityChoicesWidget extends StatelessWidget {
],
),
AsyncLoaded<MultipleChoicePracticeActivityModel>(:final value) =>
LayoutBuilder(
builder: (context, constraints) {
ValueListenableBuilder(
valueListenable: controller.enableChoicesNotifier,
builder: (context, enabled, __) {
final choices = controller.filteredChoices(value);
final constrainedHeight =
constraints.maxHeight.clamp(0.0, 400.0);
final cardHeight = (constrainedHeight / (choices.length + 1))
.clamp(50.0, 80.0);
return ValueListenableBuilder(
valueListenable: controller.enableChoicesNotifier,
builder: (context, enabled, __) => Column(
children: [
Expanded(
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: choices
.map(
(choice) => _ChoiceCard(
activity: value,
targetId: controller
.choiceTargetId(choice.choiceId),
choiceId: choice.choiceId,
onPressed: () => controller.onSelectChoice(
choice.choiceId,
),
cardHeight: cardHeight,
choiceText: choice.choiceText,
choiceEmoji: choice.choiceEmoji,
enabled: enabled,
),
)
.toList(),
return Column(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: choices
.map(
(choice) => _ChoiceCard(
activity: value,
targetId: controller.choiceTargetId(choice.choiceId),
choiceId: choice.choiceId,
onPressed: () => controller.onSelectChoice(
choice.choiceId,
),
cardHeight: 60.0,
choiceText: choice.choiceText,
choiceEmoji: choice.choiceEmoji,
enabled: enabled,
),
),
],
),
)
.toList(),
);
},
),

View file

@ -149,8 +149,6 @@ class _CardContainer extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
height: height,
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
decoration: BoxDecoration(
color: baseColor,

View file

@ -3,6 +3,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.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/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
extension BotRoomExtension on Room {
@ -23,11 +24,40 @@ extension BotRoomExtension on Room {
return BotOptionsModel.fromJson(stateEvent.content);
}
Future<void> setBotOptions(BotOptionsModel options) =>
client.setRoomStateWithKey(
id,
PangeaEventTypes.botOptions,
'',
options.toJson(),
);
Future<void> setBotOptions(BotOptionsModel options) async {
const maxRetries = 3;
Duration retryDelay = const Duration(seconds: 5);
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (attempt > 1) {
await Future.delayed(retryDelay);
retryDelay *= 2;
}
await client.setRoomStateWithKey(
id,
PangeaEventTypes.botOptions,
'',
options.toJson(),
);
return;
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': id,
'options': options.toJson(),
'attempt': attempt,
},
);
if (attempt == maxRetries) {
rethrow;
}
}
}
}
}

View file

@ -9,10 +9,10 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/pages/pangea_invitation_selection_view.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/user/user_search_extension.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -337,20 +337,11 @@ class PangeaInvitationSelectionController
setState(() => foundProfiles = []);
}
String pangeaSearchText = text;
if (!pangeaSearchText.startsWith("@")) {
pangeaSearchText = "@$pangeaSearchText";
}
if (!pangeaSearchText.contains(":")) {
pangeaSearchText = "$pangeaSearchText:${Environment.homeServer}";
}
setState(() => loading = true);
final matrix = Matrix.of(context);
SearchUserDirectoryResponse response;
try {
response =
await matrix.client.searchUserDirectory(pangeaSearchText, limit: 100);
response = await matrix.client.searchUser(text, limit: 100);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text((e).toLocalizedString(context))),

View file

@ -1,11 +1,13 @@
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart';
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/common/utils/error_handler.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';
@ -15,10 +17,15 @@ extension BotClientExtension on Client {
Room? get botDM => rooms.firstWhereOrNull((r) => r.isBotDM);
// 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);
List<Room> get _targetBotChats => rooms.where((r) {
return
// bot settings exist
r.botOptions != null &&
// there is no activity plan
r.activityPlan == null &&
// it's just the bot and one other user in the room
r.summary.mJoinedMemberCount == 2 &&
r.getParticipants().any((u) => u.id == BotName.byEnvironment);
}).toList();
Future<String> startChatWithBot() => startDirectChat(
@ -42,30 +49,41 @@ extension BotClientExtension on Client {
);
Future<void> updateBotOptions(UserSettings userSettings) async {
final rooms = targetBotChats;
if (rooms.isEmpty) return;
final targetBotRooms = [..._targetBotChats];
if (targetBotRooms.isEmpty) return;
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;
try {
final futures = <Future>[];
for (final targetBotRoom in targetBotRooms) {
final botOptions = targetBotRoom.botOptions ?? const BotOptionsModel();
final targetLanguage = userSettings.targetLanguage;
final languageLevel = userSettings.cefrLevel;
final voice = userSettings.voice;
if (botOptions.targetLanguage == targetLanguage &&
botOptions.languageLevel == languageLevel &&
botOptions.targetVoice == voice) {
continue;
if (botOptions.targetLanguage == targetLanguage &&
botOptions.languageLevel == languageLevel &&
botOptions.targetVoice == voice) {
continue;
}
final updated = botOptions.copyWith(
targetLanguage: targetLanguage,
languageLevel: languageLevel,
targetVoice: voice,
);
futures.add(targetBotRoom.setBotOptions(updated));
}
final updated = botOptions.copyWith(
targetLanguage: targetLanguage,
languageLevel: languageLevel,
targetVoice: voice,
await Future.wait(futures);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'userSettings': userSettings.toJson(),
'targetBotRooms': targetBotRooms.map((r) => r.id).toList(),
},
);
futures.add(room.setBotOptions(updated));
}
await Future.wait(futures);
}
}

View file

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:http/http.dart' hide Client;
import 'package:matrix/matrix.dart';
import 'package:matrix/matrix_api_lite/generated/api.dart';
@ -7,6 +8,8 @@ import 'package:matrix/matrix_api_lite/generated/api.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_event.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
extension RoomSummaryExtension on Api {
@ -52,27 +55,40 @@ class RoomSummariesResponse {
});
return RoomSummariesResponse(summaries: summaries);
}
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
summaries.forEach((key, value) {
json[key] = value.toJson();
});
return json;
}
}
class RoomSummaryResponse {
final ActivityPlanModel activityPlan;
final ActivityRolesModel activityRoles;
final ActivityPlanModel? activityPlan;
final ActivityRolesModel? activityRoles;
final ActivitySummaryModel? activitySummary;
final CoursePlanEvent? coursePlan;
final JoinRules? joinRule;
final Map<String, int>? powerLevels;
final Map<String, String> membershipSummary;
final String? displayName;
final String? avatarUrl;
RoomSummaryResponse({
required this.activityPlan,
required this.activityRoles,
required this.membershipSummary,
this.activityPlan,
this.activityRoles,
this.activitySummary,
this.coursePlan,
this.joinRule,
this.powerLevels,
this.displayName,
this.avatarUrl,
});
List<String> get adminUserIDs {
if (powerLevels == null) return [];
return powerLevels!.entries
.where((entry) => entry.value >= 100)
.map((entry) => entry.key)
.toList();
}
Membership? getMembershipForUserId(String userId) {
final membershipString = membershipSummary[userId];
if (membershipString == null) return null;
@ -83,32 +99,93 @@ class RoomSummaryResponse {
}
Map<String, ActivityRoleModel> get joinedUsersWithRoles {
if (activityRoles == null) return {};
return Map.fromEntries(
activityRoles.roles.entries.where(
activityRoles!.roles.entries.where(
(role) => getMembershipForUserId(role.value.userId) == Membership.join,
),
);
}
factory RoomSummaryResponse.fromJson(Map<String, dynamic> json) {
final planEntry =
json[PangeaEventTypes.activityPlan]?["default"]?["content"];
ActivityPlanModel? plan;
if (planEntry != null && planEntry is Map<String, dynamic>) {
plan = ActivityPlanModel.fromJson(planEntry);
}
final rolesEntry =
json[PangeaEventTypes.activityRole]?["default"]?["content"];
ActivityRolesModel? roles;
if (rolesEntry != null && rolesEntry is Map<String, dynamic>) {
roles = ActivityRolesModel.fromJson(rolesEntry);
}
final summaryEntry =
json[PangeaEventTypes.activitySummary]?["default"]?["content"];
ActivitySummaryModel? summary;
if (summaryEntry != null && summaryEntry is Map<String, dynamic>) {
summary = ActivitySummaryModel.fromJson(summaryEntry);
}
final coursePlanEntry =
json[PangeaEventTypes.coursePlan]?["default"]?["content"];
CoursePlanEvent? coursePlan;
if (coursePlanEntry != null && coursePlanEntry is Map<String, dynamic>) {
coursePlan = CoursePlanEvent.fromJson(coursePlanEntry);
}
final powerLevelsEntry =
json[EventTypes.RoomPowerLevels]?['default']?['content']?['users'];
Map<String, int>? powerLevels;
if (powerLevelsEntry != null) {
powerLevels = Map<String, int>.from(powerLevelsEntry);
}
final joinRulesString =
json[EventTypes.RoomJoinRules]?['default']?['content']?['join_rule'];
JoinRules? joinRule;
if (joinRulesString != null && joinRulesString is String) {
joinRule = JoinRules.values
.singleWhereOrNull((element) => element.text == joinRulesString);
}
final displayName =
json[EventTypes.RoomName]?['default']?['content']?['name'] as String?;
String? avatarUrl =
json[EventTypes.RoomAvatar]?['default']?['content']?['url'] as String?;
if (avatarUrl != null && Uri.tryParse(avatarUrl) == null) {
avatarUrl = null;
}
return RoomSummaryResponse(
activityPlan: ActivityPlanModel.fromJson(
json[PangeaEventTypes.activityPlan]?["default"]?["content"] ?? {},
),
activityRoles: ActivityRolesModel.fromJson(
json[PangeaEventTypes.activityRole]?["default"]?["content"] ?? {},
),
activityPlan: plan,
activityRoles: roles,
activitySummary: summary,
coursePlan: coursePlan,
powerLevels: powerLevels,
joinRule: joinRule,
membershipSummary: Map<String, String>.from(
json['membership_summary'] ?? {},
),
displayName: displayName,
avatarUrl: avatarUrl,
);
}
Map<String, dynamic> toJson() {
return {
PangeaEventTypes.activityPlan: activityPlan.toJson(),
PangeaEventTypes.activityRole: activityRoles.toJson(),
'membership_summary': membershipSummary,
'activityPlan': activityPlan?.toJson(),
'activityRoles': activityRoles?.toJson(),
'activitySummary': activitySummary?.toJson(),
'coursePlan': coursePlan?.toJson(),
'joinRule': joinRule?.text,
'powerLevels': powerLevels,
'membershipSummary': membershipSummary,
'displayName': displayName,
'avatarUrl': avatarUrl,
};
}
}

View file

@ -131,7 +131,7 @@ class Choreographer extends ChangeNotifier {
_choreoRecord = null;
itController.closeIT();
itController.clearSourceText();
itController.clearDissmissed();
itController.clearSession();
igcController.clear();
_resetDebounceTimer();
_setChoreoMode(ChoreoModeEnum.igc);
@ -372,7 +372,9 @@ class Choreographer extends ChangeNotifier {
}
void _onCloseIT() {
if (currentText.isEmpty && itController.sourceText.value != null) {
if (itController.dismissed &&
currentText.isEmpty &&
itController.sourceText.value != null) {
textController.setSystemText(
itController.sourceText.value!,
EditTypeEnum.itDismissed,

View file

@ -65,8 +65,9 @@ class ITController {
_sourceText.value = null;
}
void clearDissmissed() {
void clearSession() {
dismissed = false;
_progress.value = 0.0;
}
void dispose() {
@ -105,6 +106,7 @@ class ITController {
_queue.clear();
_currentITStep.value = null;
_goldRouteTracker = null;
_progress.value = 0.0;
_sourceText.value = text;
setEditingSourceText(false);
_continueIT();
@ -152,7 +154,6 @@ class ITController {
) +
1) /
_goldRouteTracker!.continuances.length;
debugPrint("Progress updated to $progress");
_progress.value = progress;
_continueIT();
}

View file

@ -22,30 +22,33 @@ class ShimmerBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!enabled) {
return child;
}
final borderRadius =
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
return Stack(
children: [
child,
if (enabled)
Positioned.fill(
child: IgnorePointer(
child: ClipRRect(
borderRadius: borderRadius,
child: Shimmer.fromColors(
baseColor: baseColor ?? shimmerColor.withValues(alpha: 0.1),
highlightColor: shimmerColor.withValues(alpha: 0.6),
direction: ShimmerDirection.ltr,
child: Container(
decoration: BoxDecoration(
color: shimmerColor.withValues(alpha: 0.3),
borderRadius: borderRadius,
),
Positioned.fill(
child: IgnorePointer(
child: ClipRRect(
borderRadius: borderRadius,
child: Shimmer.fromColors(
baseColor: baseColor ?? shimmerColor.withValues(alpha: 0.1),
highlightColor: shimmerColor.withValues(alpha: 0.6),
direction: ShimmerDirection.ltr,
child: Container(
decoration: BoxDecoration(
color: shimmerColor.withValues(alpha: 0.3),
borderRadius: borderRadius,
),
),
),
),
),
),
],
);
}

View file

@ -4,11 +4,13 @@ class ShrinkableText extends StatelessWidget {
final String text;
final double maxWidth;
final TextStyle? style;
final Alignment? alignment;
const ShrinkableText({
super.key,
required this.text,
required this.maxWidth,
this.alignment,
this.style,
});
@ -18,6 +20,7 @@ class ShrinkableText extends StatelessWidget {
builder: (context, constraints) {
return Container(
constraints: BoxConstraints(maxWidth: maxWidth),
alignment: alignment,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,

View file

@ -133,9 +133,13 @@ class CourseChatsController extends State<CourseChats>
}
final activity = summary.activityPlan;
final roles = summary.activityRoles;
final users = summary.joinedUsersWithRoles;
if (users.isEmpty || !validIDs.contains(activity.activityId)) {
if (activity == null ||
roles == null ||
users.isEmpty ||
!validIDs.contains(activity.activityId)) {
continue;
}
@ -148,7 +152,7 @@ class CourseChatsController extends State<CourseChats>
// It's possible for users to finish an activity and then for some of the
// users to leave, but if the activity was archived by anyone, that means
// it was full at some point.
if (summary.activityRoles.roles.values.any((role) => role.isArchived)) {
if (roles.roles.values.any((role) => role.isArchived)) {
continue;
}
@ -233,7 +237,10 @@ class CourseChatsController extends State<CourseChats>
Logs().w('Unable to load hierarchy', e, s);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toLocalizedString(context))),
SnackBar(
content: Text(e.toLocalizedString(context)),
showCloseIcon: true,
),
);
}
} finally {

View file

@ -0,0 +1,188 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/course_creation/public_course_preview_view.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart';
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PublicCoursePreview extends StatefulWidget {
final String? roomID;
const PublicCoursePreview({
super.key,
required this.roomID,
});
@override
PublicCoursePreviewController createState() =>
PublicCoursePreviewController();
}
class PublicCoursePreviewController extends State<PublicCoursePreview>
with CoursePlanProvider, ActivitySummariesProvider {
RoomSummaryResponse? roomSummary;
Object? roomSummaryError;
bool loadingRoomSummary = false;
@override
initState() {
super.initState();
_loadSummary();
}
@override
void didUpdateWidget(covariant PublicCoursePreview oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.roomID != oldWidget.roomID) {
_loadSummary();
}
}
bool get loading => loadingCourse || loadingRoomSummary;
bool get hasError =>
(courseError != null || (!loadingCourse && course == null)) ||
(roomSummaryError != null ||
(!loadingRoomSummary && roomSummary == null));
Future<void> _loadSummary() async {
try {
if (widget.roomID == null) {
throw Exception("roomID is required");
}
setState(() {
loadingRoomSummary = true;
roomSummaryError = null;
});
await loadRoomSummaries([widget.roomID!]);
if (roomSummaries == null || !roomSummaries!.containsKey(widget.roomID)) {
throw Exception("Room summary not found");
}
roomSummary = roomSummaries![widget.roomID];
} catch (e, s) {
roomSummaryError = e;
loadingCourse = false;
ErrorHandler.logError(
e: e,
s: s,
data: {'roomID': widget.roomID, 'roomSummary': roomSummary?.toJson()},
);
} finally {
if (mounted) {
setState(() {
loadingRoomSummary = false;
});
}
}
if (roomSummary?.coursePlan != null) {
await loadCourse(roomSummary!.coursePlan!.uuid).then((_) => loadTopics());
} else {
ErrorHandler.logError(
e: Exception("No course plan found in room summary"),
data: {'roomID': widget.roomID, 'roomSummary': roomSummary?.toJson()},
);
if (mounted) {
setState(() {
roomSummaryError = Exception("No course plan found in room summary");
loadingCourse = false;
});
}
}
}
Future<void> joinWithCode(String code) async {
if (code.isEmpty) {
return;
}
final roomId = await SpaceCodeController.joinSpaceWithCode(
context,
code,
);
if (roomId != null) {
final room = Matrix.of(context).client.getRoomById(roomId);
room?.isSpace ?? true
? context.go('/rooms/spaces/$roomId/details')
: context.go('/rooms/$roomId');
}
}
Future<void> joinCourse() async {
if (widget.roomID == null) {
throw Exception("roomID is required");
}
final roomID = widget.roomID;
final client = Matrix.of(context).client;
final r = client.getRoomById(roomID!);
if (r != null && r.membership == Membership.join) {
if (mounted) {
context.go("/rooms/spaces/${r.id}/details");
}
return;
}
final knock = roomSummary?.joinRule == JoinRules.knock;
final resp = await showFutureLoadingDialog(
context: context,
future: () async {
String roomId;
try {
roomId = knock
? await client.knockRoom(widget.roomID!)
: await client.joinRoom(widget.roomID!);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {'roomID': widget.roomID},
);
rethrow;
}
Room? room = client.getRoomById(roomId);
if (!knock && room?.membership != Membership.join) {
await client.waitForRoomInSync(roomId, join: true);
room = client.getRoomById(roomId);
}
if (knock) return;
if (room == null) {
ErrorHandler.logError(
e: Exception("Failed to load joined room in public course preview"),
data: {'roomID': widget.roomID},
);
throw Exception("Failed to join room");
}
context.go("/rooms/spaces/$roomId/details");
},
);
if (!knock || resp.isError) return;
await showOkAlertDialog(
context: context,
title: L10n.of(context).youHaveKnocked,
message: L10n.of(context).knockDesc,
);
}
@override
Widget build(BuildContext context) => PublicCoursePreviewView(this);
}

View file

@ -0,0 +1,388 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/common/widgets/url_image_widget.dart';
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
import 'package:fluffychat/pangea/course_creation/public_course_preview.dart';
import 'package:fluffychat/pangea/course_plans/map_clipper.dart';
import 'package:fluffychat/pangea/course_settings/pin_clipper.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PublicCoursePreviewView extends StatelessWidget {
final PublicCoursePreviewController controller;
const PublicCoursePreviewView(
this.controller, {
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
const double titleFontSize = 16.0;
const double descFontSize = 12.0;
const double largeIconSize = 24.0;
const double smallIconSize = 12.0;
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).joinWithClassCode),
),
body: SafeArea(
child: Container(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500.0),
child: Builder(
builder: (context) {
if (controller.loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
if (controller.hasError) {
return Center(
child: ErrorIndicator(
message: L10n.of(context).oopsSomethingWentWrong,
),
);
}
final course = controller.course!;
final summary = controller.roomSummary!;
Uri? avatarUrl = course.imageUrl;
if (summary.avatarUrl != null) {
avatarUrl = Uri.tryParse(summary.avatarUrl!);
}
final displayname = summary.displayName ?? course.title;
return Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(
top: 12.0,
left: 12.0,
right: 12.0,
),
child: ListView.builder(
itemCount: course.topicIds.length + 2,
itemBuilder: (context, index) {
if (index == 0) {
return Column(
spacing: 8.0,
children: [
ClipPath(
clipper: MapClipper(),
child: ImageByUrl(
imageUrl: avatarUrl,
width: 100.0,
borderRadius: BorderRadius.circular(0.0),
replacement: Avatar(
name: displayname,
size: 100.0,
borderRadius: BorderRadius.circular(
0.0,
),
),
),
),
Text(
displayname,
style: const TextStyle(
fontSize: titleFontSize,
),
),
if (summary.adminUserIDs.isNotEmpty)
_CourseAdminDisplay(summary),
Text(
course.description,
style: const TextStyle(
fontSize: descFontSize,
),
),
Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
CourseInfoChips(
course.uuid,
fontSize: descFontSize,
iconSize: smallIconSize,
),
CourseInfoChip(
icon: Icons.person,
text:
L10n.of(context).countParticipants(
summary.membershipSummary.length,
),
fontSize: descFontSize,
iconSize: smallIconSize,
),
],
),
Padding(
padding: const EdgeInsets.only(
top: 4.0,
bottom: 8.0,
),
child: Row(
spacing: 4.0,
children: [
const Icon(
Icons.map,
size: largeIconSize,
),
Text(
L10n.of(context).coursePlan,
style: const TextStyle(
fontSize: titleFontSize,
),
),
],
),
),
],
);
}
index--;
if (index >= course.topicIds.length) {
return const SizedBox(height: 12.0);
}
final topicId = course.topicIds[index];
final topic = course.loadedTopics[topicId];
if (topic == null) {
return const SizedBox();
}
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0,
),
child: Row(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipPath(
clipper: PinClipper(),
child: ImageByUrl(
imageUrl: topic.imageUrl,
width: 45.0,
replacement: Container(
width: 45.0,
height: 45.0,
decoration: BoxDecoration(
color: theme.colorScheme.secondary,
),
),
),
),
Flexible(
child: Column(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
topic.title,
style: const TextStyle(
fontSize: titleFontSize,
),
),
Text(
topic.description,
style: const TextStyle(
fontSize: descFontSize,
),
),
Padding(
padding: const EdgeInsetsGeometry
.symmetric(
vertical: 2.0,
),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
if (topic.location != null)
CourseInfoChip(
icon: Icons.location_on,
text: topic.location!,
fontSize: descFontSize,
iconSize: smallIconSize,
),
],
),
),
],
),
),
],
),
);
},
),
),
),
Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
border: Border(
top: BorderSide(
color: theme.dividerColor,
width: 1.0,
),
),
),
padding: const EdgeInsets.all(12.0),
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
spacing: 8.0,
children: [
if (summary.joinRule == JoinRules.knock) ...[
TextField(
decoration: InputDecoration(
hintText: L10n.of(context).enterCodeToJoin,
),
onSubmitted: controller.joinWithCode,
),
Row(
spacing: 8.0,
children: [
const Expanded(
child: Divider(),
),
Text(L10n.of(context).or),
const Expanded(
child: Divider(),
),
],
),
],
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
),
onPressed: controller.joinCourse,
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined),
Text(
summary.joinRule == JoinRules.knock
? L10n.of(context).knock
: L10n.of(context).join,
style: const TextStyle(
fontSize: titleFontSize,
),
),
],
),
),
],
),
),
),
],
);
},
),
),
),
),
);
}
}
class _CourseAdminDisplay extends StatelessWidget {
final RoomSummaryResponse summary;
const _CourseAdminDisplay(this.summary);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Wrap(
alignment: WrapAlignment.center,
spacing: 12.0,
runSpacing: 12.0,
children: [
...summary.adminUserIDs.map((adminId) {
return FutureBuilder(
future: Matrix.of(context).client.getProfileFromUserId(
adminId,
),
builder: (context, snapshot) {
final profile = snapshot.data;
final displayName =
profile?.displayName ?? adminId.localpart ?? adminId;
return InkWell(
onTap: profile != null
? () => UserDialog.show(
context: context,
profile: profile,
)
: null,
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(18.0),
),
padding: const EdgeInsets.all(4.0),
child: Opacity(
opacity: 0.5,
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
size: 18.0,
mxContent: profile?.avatarUrl,
name: displayName,
userId: adminId,
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 80.0,
),
child: Text(
displayName,
style: TextStyle(
fontSize: 12.0,
color: theme.colorScheme.onPrimaryContainer,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
),
);
},
);
}),
],
);
}
}

View file

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/course_creation/selected_course_view.dart';
@ -12,11 +11,11 @@ import 'package:fluffychat/pangea/course_plans/courses/course_plan_builder.dart'
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/join_codes/space_code_controller.dart';
import 'package:fluffychat/pangea/spaces/client_spaces_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum SelectedCourseMode { launch, addToSpace, join }
enum SelectedCourseMode { launch, addToSpace }
class SelectedCourse extends StatefulWidget {
final String courseId;
@ -26,15 +25,11 @@ class SelectedCourse extends StatefulWidget {
/// In join mode, the ID of the space to join that already has this course.
final String? spaceId;
/// In join mode, the room info for the space that already has this course.
final PublicRoomsChunk? roomChunk;
const SelectedCourse(
this.courseId,
this.mode, {
super.key,
this.spaceId,
this.roomChunk,
});
@override
@ -63,8 +58,6 @@ class SelectedCourseController extends State<SelectedCourse>
return L10n.of(context).newCourse;
case SelectedCourseMode.addToSpace:
return L10n.of(context).addCoursePlan;
case SelectedCourseMode.join:
return L10n.of(context).joinWithClassCode;
}
}
@ -74,10 +67,24 @@ class SelectedCourseController extends State<SelectedCourse>
return L10n.of(context).createCourse;
case SelectedCourseMode.addToSpace:
return L10n.of(context).addCoursePlan;
case SelectedCourseMode.join:
return widget.roomChunk?.joinRule == JoinRules.knock.name
? L10n.of(context).knock
: L10n.of(context).join;
}
}
Future<void> joinWithCode(String code) async {
if (code.isEmpty) {
return;
}
final roomId = await SpaceCodeController.joinSpaceWithCode(
context,
code,
);
if (roomId != null) {
final room = Matrix.of(context).client.getRoomById(roomId);
room?.isSpace ?? true
? context.go('/rooms/spaces/$roomId/details')
: context.go('/rooms/$roomId');
}
}
@ -87,8 +94,6 @@ class SelectedCourseController extends State<SelectedCourse>
return launchCourse(widget.courseId, course);
case SelectedCourseMode.addToSpace:
return addCourseToSpace(course);
case SelectedCourseMode.join:
return joinCourse();
}
}
@ -149,50 +154,6 @@ class SelectedCourseController extends State<SelectedCourse>
context.go("/rooms/spaces/${space.id}/details?tab=course");
}
Future<void> joinCourse() async {
if (widget.roomChunk == null) {
throw Exception("Room chunk is null");
}
final client = Matrix.of(context).client;
final r = client.getRoomById(widget.roomChunk!.roomId);
if (r != null && r.membership == Membership.join) {
if (mounted) {
context.go("/rooms/spaces/${r.id}/details");
}
return;
}
final knock = widget.roomChunk!.joinRule == JoinRules.knock.name;
final roomId = widget.roomChunk != null && knock
? await client.knockRoom(widget.roomChunk!.roomId)
: await client.joinRoom(widget.roomChunk!.roomId);
Room? room = client.getRoomById(roomId);
if (!knock && room?.membership != Membership.join) {
await client.waitForRoomInSync(roomId, join: true);
room = client.getRoomById(roomId);
}
if (knock) {
Navigator.of(context).pop();
await showOkAlertDialog(
context: context,
title: L10n.of(context).youHaveKnocked,
message: L10n.of(context).knockDesc,
);
return;
}
if (room == null) {
throw Exception("Failed to join room");
}
if (mounted) {
context.go("/rooms/spaces/$roomId/details");
}
}
@override
Widget build(BuildContext context) => SelectedCourseView(this);
}

View file

@ -60,13 +60,7 @@ class SelectedCourseView extends StatelessWidget {
child: ListView.builder(
itemCount: course.topicIds.length + 2,
itemBuilder: (context, index) {
String displayname = course.title;
final roomChunk = controller.widget.roomChunk;
if (roomChunk != null) {
displayname = roomChunk.name ??
roomChunk.canonicalAlias ??
L10n.of(context).emptyChat;
}
final String displayname = course.title;
if (index == 0) {
return Column(
@ -75,9 +69,7 @@ class SelectedCourseView extends StatelessWidget {
ClipPath(
clipper: MapClipper(),
child: ImageByUrl(
imageUrl: controller.widget
.roomChunk?.avatarUrl ??
course.imageUrl,
imageUrl: course.imageUrl,
width: 100.0,
borderRadius:
BorderRadius.circular(0.0),
@ -233,70 +225,74 @@ class SelectedCourseView extends StatelessWidget {
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
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.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(
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer,
foregroundColor:
theme.colorScheme.onPrimaryContainer,
),
onPressed: () => showFutureLoadingDialog(
context: context,
future: () => controller.submit(course),
),
child: Row(
spacing: 8.0,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined),
Text(
controller.buttonText,
style: const TextStyle(
fontSize: titleFontSize,
),
child: Column(
spacing: 8.0,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme
.colorScheme.primaryContainer,
foregroundColor: theme
.colorScheme.onPrimaryContainer,
),
],
),
onPressed: () =>
showFutureLoadingDialog(
context: context,
future: () =>
controller.submit(course),
),
child: Row(
spacing: 8.0,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined),
Text(
controller.buttonText,
style: const TextStyle(
fontSize: titleFontSize,
),
),
],
),
),
],
),
),
],

View file

@ -48,6 +48,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
final activityPlan = roomSummary.activityPlan;
final assignedRoles = roomSummary.joinedUsersWithRoles;
if (activityPlan == null) return false;
return activityPlan.roles.length - assignedRoles.length <= 0;
}
@ -56,6 +57,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
if (roomSummary == null) return false;
final activityRoles = roomSummary.activityRoles;
if (activityRoles == null) return false;
final roles = activityRoles.roles.values.where(
(r) => r.userId != BotName.byEnvironment,
);
@ -76,7 +78,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
Map<String, RoomSummaryResponse> activitySessions(String activityId) =>
Map.fromEntries(
roomSummaries?.entries
.where((v) => v.value.activityPlan.activityId == activityId) ??
.where((v) => v.value.activityPlan?.activityId == activityId) ??
[],
);
@ -115,7 +117,7 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
final summary = entry.value;
final roomId = entry.key;
if (summary.activityPlan.activityId != activityId) {
if (summary.activityPlan?.activityId != activityId) {
continue;
}
@ -132,11 +134,13 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
if (roomSummaries == null || roomSummaries!.isEmpty) return {};
return roomSummaries!.values
.where(
(entry) => entry.activityRoles.roles.values.any(
(v) => v.userId == userID && v.isArchived,
),
(entry) =>
entry.activityRoles != null &&
entry.activityRoles!.roles.values.any(
(v) => v.userId == userID && v.isArchived,
),
)
.map((e) => e.activityPlan.activityId)
.map((e) => e.activityPlan?.activityId)
.whereType<String>()
.toSet();
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_request.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -47,7 +48,12 @@ mixin CoursePlanProvider<T extends StatefulWidget> on State<T> {
),
);
await course!.fetchMediaUrls();
} catch (e) {
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {'courseId': courseId},
);
courseError = e;
} finally {
if (mounted) setState(() => loadingCourse = false);

View file

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart';
import 'package:matrix/matrix.dart';
import 'package:shimmer/shimmer.dart';
@ -54,6 +55,17 @@ class CourseSettings extends StatelessWidget {
}
if (controller.course == null || controller.courseError != null) {
if (controller.courseError is Response &&
(controller.courseError as Response).statusCode == 500) {
return Center(
child: Text(
L10n.of(context).courseLoadingError,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
return room.canChangeStateEvent(PangeaEventTypes.coursePlan)
? Column(
spacing: 50.0,

View file

@ -32,9 +32,9 @@ enum InstructionsEnum {
setLemmaEmoji,
disableLanguageTools,
selectMeaning,
clickTextMessages,
clickAudioMessages,
dismissSupportChat,
shimmerNewToken,
shimmerTranslation,
}
extension InstructionsEnumExtension on InstructionsEnum {
@ -66,9 +66,9 @@ extension InstructionsEnumExtension on InstructionsEnum {
case InstructionsEnum.noSavedActivitiesYet:
case InstructionsEnum.setLemmaEmoji:
case InstructionsEnum.disableLanguageTools:
case InstructionsEnum.clickTextMessages:
case InstructionsEnum.clickAudioMessages:
case InstructionsEnum.dismissSupportChat:
case InstructionsEnum.shimmerNewToken:
case InstructionsEnum.shimmerTranslation:
ErrorHandler.logError(
e: Exception("No title for this instruction"),
m: 'InstructionsEnumExtension.title',
@ -130,9 +130,9 @@ extension InstructionsEnumExtension on InstructionsEnum {
case InstructionsEnum.noSavedActivitiesYet:
return l10n.noSavedActivitiesYet;
case InstructionsEnum.setLemmaEmoji:
case InstructionsEnum.clickTextMessages:
case InstructionsEnum.clickAudioMessages:
case InstructionsEnum.dismissSupportChat:
case InstructionsEnum.shimmerNewToken:
case InstructionsEnum.shimmerTranslation:
return "";
case InstructionsEnum.disableLanguageTools:
return l10n.disableLanguageToolsDesc;

View file

@ -391,6 +391,14 @@ class _PublicCourseTile extends StatelessWidget {
this.course,
});
void _navigateToCoursePage(
BuildContext context,
) {
context.go(
'/rooms/course/${Uri.encodeComponent(chunk.room.roomId)}',
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -411,10 +419,7 @@ class _PublicCourseTile extends StatelessWidget {
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () => context.go(
'/rooms/course/$courseId',
extra: space,
),
onTap: () => _navigateToCoursePage(context),
borderRadius: BorderRadius.circular(12.0),
child: Container(
padding: const EdgeInsets.all(12.0),
@ -490,10 +495,7 @@ class _PublicCourseTile extends StatelessWidget {
const SizedBox(height: 12.0),
HoverBuilder(
builder: (context, hovered) => ElevatedButton(
onPressed: () => context.go(
'/rooms/course/$courseId',
extra: space,
),
onPressed: () => _navigateToCoursePage(context),
style: ElevatedButton.styleFrom(
backgroundColor:
theme.colorScheme.primaryContainer.withAlpha(

View file

@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/common/widgets/shrinkable_text.dart';
import 'package:fluffychat/pangea/languages/language_model.dart';
import 'package:fluffychat/pangea/languages/language_service.dart';
import 'package:fluffychat/pangea/languages/p_language_store.dart';
@ -102,14 +104,41 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).languages),
title: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500,
),
child: Row(
spacing: 12.0,
children: [
BackButton(
onPressed: Navigator.of(context).pop,
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return ShrinkableText(
text: L10n.of(context).onboardingLanguagesTitle,
maxWidth: constraints.maxWidth,
alignment: Alignment.center,
);
},
),
),
const SizedBox(
width: 40.0,
),
],
),
),
automaticallyImplyLeading: false,
),
body: SafeArea(
child: Center(
child: Container(
padding: const EdgeInsets.all(20.0),
constraints: const BoxConstraints(
maxWidth: 450,
maxWidth: 500,
),
child: Column(
spacing: 24.0,
@ -121,6 +150,7 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(50),
),
hintText: L10n.of(context).searchLanguagesHint,
),
),
Expanded(
@ -153,27 +183,38 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
),
)
.map(
(l) => FilterChip(
selected: _selectedLanguage == l,
backgroundColor:
_selectedLanguage == l
? theme.colorScheme.primary
: theme.colorScheme.surface,
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
(l) => ShimmerBackground(
enabled: _selectedLanguage == null,
borderRadius: const BorderRadius.all(
Radius.circular(16.0),
),
label: Text(
l.getDisplayName(context),
style: isColumnMode
? theme.textTheme.bodyLarge
: theme.textTheme.bodyMedium,
child: FilterChip(
selected: _selectedLanguage == l,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(16.0),
),
),
backgroundColor:
_selectedLanguage == l
? theme.colorScheme.primary
: theme.colorScheme.surface,
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
label: Text(
l.getDisplayName(context),
style: isColumnMode
? theme.textTheme.bodyLarge
: theme.textTheme.bodyMedium,
),
onSelected: (selected) {
_setSelectedLanguage(
selected ? l : null,
);
},
),
onSelected: (selected) {
_setSelectedLanguage(
selected ? l : null,
);
},
),
)
.toList(),
@ -220,23 +261,24 @@ class LanguageSelectionPageState extends State<LanguageSelectionPage> {
)
: const SizedBox(),
),
Text(
L10n.of(context).chooseLanguage,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
ElevatedButton(
onPressed: _selectedLanguage != null ? _submit : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
foregroundColor: theme.colorScheme.onPrimaryContainer,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).letsGo),
],
ShimmerBackground(
enabled: _selectedLanguage != null,
borderRadius: BorderRadius.circular(24.0),
child: ElevatedButton(
onPressed: _selectedLanguage != null ? _submit : null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
foregroundColor: theme.colorScheme.onPrimaryContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.0),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).letsGo),
],
),
),
),
],

View file

@ -21,9 +21,24 @@ class LoginOptionsView extends StatelessWidget {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context).loginToAccount,
title: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BackButton(
onPressed: Navigator.of(context).pop,
),
Text(L10n.of(context).login),
const SizedBox(
width: 40.0,
),
],
),
),
automaticallyImplyLeading: false,
),
body: SafeArea(
child: Center(
@ -36,6 +51,13 @@ class LoginOptionsView extends StatelessWidget {
spacing: 16.0,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
L10n.of(context).loginToAccount,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const PangeaSsoButton(
provider: SSOProvider.apple,
title: "Apple",

View file

@ -15,9 +15,24 @@ class PasswordLoginView extends StatelessWidget {
key: controller.formKey,
child: Scaffold(
appBar: AppBar(
title: Text(
L10n.of(context).loginWithEmail,
title: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BackButton(
onPressed: Navigator.of(context).pop,
),
Text(L10n.of(context).loginWithEmail),
const SizedBox(
width: 40.0,
),
],
),
),
automaticallyImplyLeading: false,
),
body: SafeArea(
child: Center(

View file

@ -27,7 +27,24 @@ class SignupPageView extends StatelessWidget {
return Form(
key: controller.formKey,
child: Scaffold(
appBar: AppBar(),
appBar: AppBar(
title: SizedBox(
width: 450,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BackButton(
onPressed: Navigator.of(context).pop,
),
Text(L10n.of(context).signUp),
const SizedBox(
width: 40.0,
),
],
),
),
automaticallyImplyLeading: false,
),
body: SafeArea(
child: Center(
child: ConstrainedBox(

View file

@ -15,7 +15,25 @@ class SignupWithEmailView extends StatelessWidget {
return Form(
key: controller.formKey,
child: Scaffold(
appBar: AppBar(),
appBar: AppBar(
title: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 450,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BackButton(
onPressed: Navigator.of(context).pop,
),
const SizedBox(
width: 40.0,
),
],
),
),
automaticallyImplyLeading: false,
),
body: SafeArea(
child: Center(
child: ConstrainedBox(

View file

@ -125,9 +125,6 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
final Duration transitionAnimationDuration =
const Duration(milliseconds: 300);
final Offset _defaultMessageOffset =
const Offset(Avatar.defaultSize + 16 + 8, 300);
double get _horizontalPadding =>
FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
@ -232,14 +229,14 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
null,
);
Offset get _originalMessageOffset {
Offset? get _originalMessageOffset {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageOffset;
return null;
}
return _runWithLogging(
() => _messageRenderBox?.localToGlobal(Offset.zero),
"Error getting message offset",
_defaultMessageOffset,
null,
);
}
@ -267,27 +264,36 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
double? get messageLeftOffset {
if (ownMessage) return null;
final offset = _originalMessageOffset;
if (offset == null) {
return Avatar.defaultSize + 16;
}
if (isRtl) {
return _originalMessageOffset.dx -
(showDetails ? FluffyThemes.columnWidth : 0);
return offset.dx - (showDetails ? FluffyThemes.columnWidth : 0);
}
if (ownMessage) return null;
return max(_originalMessageOffset.dx - columnWidth, 0);
return max(offset.dx - columnWidth, 0);
}
double? get messageRightOffset {
if (mediaQuery == null || !ownMessage) return null;
final offset = _originalMessageOffset;
if (offset == null) {
return 8.0;
}
if (isRtl) {
return mediaQuery!.size.width -
columnWidth -
_originalMessageOffset.dx -
offset.dx -
originalMessageSize.width;
}
return mediaQuery!.size.width -
_originalMessageOffset.dx -
offset.dx -
originalMessageSize.width -
(showDetails ? FluffyThemes.columnWidth : 0);
}
@ -344,7 +350,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
bool get _hasFooterOverflow {
if (_screenHeight == null) return false;
final bottomOffset = _originalMessageOffset.dy +
final offset = _originalMessageOffset;
if (offset == null) return false;
final bottomOffset = offset.dy +
originalMessageSize.height +
_reactionsHeight +
AppConfig.toolbarMenuHeight +
@ -357,6 +366,8 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
double get spaceBelowContent {
if (shouldScroll) return 0;
if (_hasFooterOverflow) return 0;
final offset = _originalMessageOffset;
if (offset == null) return 300;
final messageHeight = originalMessageSize.height;
final originalContentHeight =
@ -364,8 +375,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
final screenHeight = mediaQuery!.size.height - mediaQuery!.padding.bottom;
double boxHeight =
screenHeight - _originalMessageOffset.dy - originalContentHeight;
double boxHeight = screenHeight - offset.dy - originalContentHeight;
final neededSpace =
boxHeight + _fullContentHeight + mediaQuery!.padding.top + 4.0;

View file

@ -162,7 +162,7 @@ class NewVocabBubble extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.toys_and_games,
Symbols.dictionary,
color: theme.colorScheme.primary,
size: 24,
),

View file

@ -15,9 +15,11 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/common/widgets/shimmer_background.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/utils/report_message.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart';
@ -214,6 +216,9 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
}
if (updatedMode == SelectMode.translate) {
if (!InstructionsEnum.shimmerTranslation.isToggledOff) {
InstructionsEnum.shimmerTranslation.setToggledOff(true);
}
await controller.fetchTranslation();
}
@ -423,25 +428,32 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
colorFactor:
theme.brightness == Brightness.light ? 0.55 : 0.3,
builder: (context, depressed, shadowColor) =>
AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: buttonSize,
width: buttonSize,
decoration: BoxDecoration(
color: depressed
? shadowColor
: theme.colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: ValueListenableBuilder(
valueListenable: _isPlayingNotifier,
builder: (context, playing, __) =>
_SelectModeButtonIcon(
mode: mode,
loading: controller.isLoading &&
mode == selectedMode,
playing: mode == SelectMode.audio && playing,
color: theme.colorScheme.onPrimaryContainer,
ShimmerBackground(
enabled: !InstructionsEnum
.shimmerTranslation.isToggledOff &&
mode == SelectMode.translate &&
enabled,
borderRadius: BorderRadius.circular(100),
child: AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: buttonSize,
width: buttonSize,
decoration: BoxDecoration(
color: depressed
? shadowColor
: theme.colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: ValueListenableBuilder(
valueListenable: _isPlayingNotifier,
builder: (context, playing, __) =>
_SelectModeButtonIcon(
mode: mode,
loading: controller.isLoading &&
mode == selectedMode,
playing: mode == SelectMode.audio && playing,
color: theme.colorScheme.onPrimaryContainer,
),
),
),
),

View file

@ -3,6 +3,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_misc/constructs_model.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance/tokens_util.dart';
mixin TokenRenderingMixin {
@ -15,6 +16,10 @@ mixin TokenRenderingMixin {
String? eventId,
}) async {
TokensUtil.collectToken(cacheKey, token.text);
if (!InstructionsEnum.shimmerNewToken.isToggledOff) {
InstructionsEnum.shimmerNewToken.setToggledOff(true);
}
final constructs = [
OneConstructUse(
useType: ConstructUseTypeEnum.click,

View file

@ -0,0 +1,19 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
extension UserSearchExtension on Client {
Future<SearchUserDirectoryResponse> searchUser(
String search, {
int? limit,
}) async {
String searchText = search;
if (!searchText.startsWith("@")) {
searchText = "@$searchText";
}
if (!searchText.contains(":")) {
searchText = "$searchText:${Environment.homeServer}";
}
return searchUserDirectory(searchText, limit: limit);
}
}

View file

@ -22,6 +22,9 @@ class ErrorReporter {
content: Text(
l10n.oopsSomethingWentWrong, // Use the non-null L10n instance to get the error message
),
// #Pangea
showCloseIcon: true,
// Pangea#
),
);
} catch (err) {