diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2b41307..8ca25bbd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +## v1.24.0 +- build: Add missing libssl library (krille-chan) +- build: Update dart_webrtc package (Krille) +- build: Update matrix sdk and dependencies (Krille) +- build: Update to flutter 3.27 (Krille) +- chore: Better bottom sheets on desktop (krille-chan) +- chore: Check file size before loading (krille-chan) +- chore: Display normal Slider when no waveform provided in audioplayer (krille-chan) +- chore: Do not display sender prefix for DM rooms in notification ticker (krille-chan) +- chore: Enable share multiple files to app (krille-chan) +- chore: Improve alias UX in chat settings (Krille) +- chore: Improve join abandoned invite exception (Krille) +- chore: Improve UserBottomSheet UX (Krille) +- chore: Make message bubble color dark also in dark mode (krille-chan) +- chore: Remove conversationTitle if room is dm room in android notifications (krille-chan) +- feat: QR Code viewer for mxid sharing (Krille) +- fix: Do not set public visibility for private groups (Krille) +- fix: Use MB and KB instead of MiB and KiB for file sizes (Krille) +- refactor: Adjust chat list item UX (Krille) +- refactor: Better custom image resizer (Krille) +- refactor: Clean up android manifest (Krille) +- refactor: Implement own adaptive dialogs and remove package (krille-chan) +- refactor: Improve UX of user role in UserBottomSheet (Krille) +- refactor: Improved share / forward dialog (krille-chan) +- Translated using Weblate (Arabic) (Rex_sa) +- Translated using Weblate (Basque) (xabirequejo) +- Translated using Weblate (Catalan) (fadelkon) +- Translated using Weblate (Chinese (Simplified Han script)) (大王叫我来巡山) +- Translated using Weblate (Czech) (Erin) +- Translated using Weblate (Estonian) (Priit Jõerüüt) +- Translated using Weblate (Galician) (josé m) +- Translated using Weblate (German) (Christian) +- Translated using Weblate (Indonesian) (Linerly) +- Translated using Weblate (Irish) (Aindriú Mac Giolla Eoin) +- Translated using Weblate (Italian) (Angelo Schirinzi) +- Translated using Weblate (Latvian) (Edgars Andersons) +- Translated using Weblate (Polish) (Piotr Orzechowski) +- Translated using Weblate (Russian) (-) +- Translated using Weblate (Tamil) (Christian) +- Translated using Weblate (Tamil) (தமிழ்நேரம்) +- Translated using Weblate (Turkish) (goknarbahceli) +- Translated using Weblate (Ukrainian) (Bezruchenko Simon) +- Translated using Weblate (Vietnamese) (Tewuzij) + ## v1.23.0 - design: Highlight emoji only messages (Krille) - design: New login design (Krille) diff --git a/PRIVACY.md b/PRIVACY.md index 8c1ca45e5..018b85760 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -92,3 +92,21 @@ A typical push notification could look like this: ``` FluffyChat sets the `event_id_only` flag at the Matrix Server. This server is then responsible to send the correct data. + + +# Explanation of FluffyChat's Compliance with Google Play Store's Safety Standards + +FluffyChat is committed to promoting a safe and respectful environment for all users. As a Matrix client, FluffyChat connects users to various Matrix servers. Please note that FluffyChat does not host or manage any servers directly, and as such, we do not have the capability to enforce content moderation or deletion within the app itself. + +To enhance user safety and help protect against the sexual abuse and exploitation of children, FluffyChat enables users to report inappropriate content directly to server administrators. + +#### Reporting Content or Users: + +1. Mark a message in the chat: Tap and hold the message you wish to report. +2. Report the message: Select the "Report" option. +3. Provide a reason and score: Enter the reason for reporting and assign a score from 1-100 to indicate how offensive the content is. +4. Notification to admin: The server administrator will be notified of the reported content. + +In addition to reporting messages, users can also report other users following a similar process. + +We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/ \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2ef8b494d..aea5e1922 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -86,15 +86,6 @@ - - - - - - - @@ -102,32 +93,16 @@ - + - + - - - + - - - - - - - - - - - - - - - + diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index b1848ba5f..7ab790718 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -2885,5 +2885,14 @@ "pleaseFillOut": "من فضلك قم بتعبئته", "@pleaseFillOut": {}, "unableToJoinChat": "يتعذر الانضمام إلى الدردشة. ربما يكون الطرف الآخر قد أغلق المحادثة بالفعل.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "إرسال {count} صورة", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "ضغط", + "@compress": {} } diff --git a/assets/l10n/intl_ca.arb b/assets/l10n/intl_ca.arb index f0fb26747..e9c5f012a 100644 --- a/assets/l10n/intl_ca.arb +++ b/assets/l10n/intl_ca.arb @@ -66,7 +66,7 @@ "type": "text", "placeholders": {} }, - "areGuestsAllowedToJoin": "Es pot entrar a la sala com a convidadi", + "areGuestsAllowedToJoin": "Es pot entrar al xat com a convidadi", "@areGuestsAllowedToJoin": { "type": "text", "placeholders": {} @@ -1939,7 +1939,7 @@ "user": {} } }, - "banUserDescription": "Es vetarà li usuàriï vetadi a la sala i no podrà tornar-hi a entrar fins que se li aixequi el veto.", + "banUserDescription": "Es vetarà li usuàriï al xat i no podrà tornar-hi a entrar fins que se li aixequi el veto.", "@banUserDescription": {}, "widgetEtherpad": "Nota de text", "@widgetEtherpad": {}, @@ -1958,7 +1958,7 @@ "user": {} } }, - "unbanUserDescription": "L'usuàrïi ja pot tornar a entrar a la sala.", + "unbanUserDescription": "L'usuàrïi ja pot tornar a entrar al xat.", "@unbanUserDescription": {}, "youRejectedTheInvitation": "Has rebutjat la invitació", "@youRejectedTheInvitation": {}, @@ -2077,7 +2077,7 @@ "provider": {} } }, - "fileIsTooBigForServer": "El servidor ha rebutjat l'arxiu perquè és massa gran.", + "fileIsTooBigForServer": "No s'ha pogut enviar! El servidor només accepta adjunts de fins a {max}.", "@fileIsTooBigForServer": {}, "homeserver": "Servidor", "@homeserver": {}, @@ -2105,7 +2105,7 @@ "@optionalRedactReason": {}, "dehydrate": "Exporta la sessió i neteja el dispositiu", "@dehydrate": {}, - "archiveRoomDescription": "Aquest xat serà arxivat. Els altres contactes del grup ho veuran com si haguessis abandonat la sala.", + "archiveRoomDescription": "Aquest xat serà arxivat. Els altres contactes del grup ho veuran com si haguessis abandonat el xat.", "@archiveRoomDescription": {}, "exportEmotePack": "Exporta com un pack Emote en .zip", "@exportEmotePack": {}, @@ -2170,7 +2170,7 @@ "path": {} } }, - "redactMessageDescription": "S'estriparà el missatge per a totser d'aquesta sala. Aquesta acció és irreversible.", + "redactMessageDescription": "S'estriparà el missatge per a totser d'aquesta conversa. Aquesta acció és irreversible.", "@redactMessageDescription": {}, "recoveryKey": "Clau de recuperació", "@recoveryKey": {}, @@ -2254,7 +2254,7 @@ }, "importEmojis": "Importa emojis", "@importEmojis": {}, - "wasDirectChatDisplayName": "La sala buida ( va ser {oldDisplayName})", + "wasDirectChatDisplayName": "Xat buit ( era {oldDisplayName})", "@wasDirectChatDisplayName": { "type": "text", "placeholders": { @@ -2285,7 +2285,7 @@ "@dehydrateTor": {}, "removeFromSpace": "Esborra de l'espai", "@removeFromSpace": {}, - "roomUpgradeDescription": "El xat serà recreat amb una versió de sala nova. Totis lis participants seran notificadis que han de canviar a la nova sala. Pots llegir més sobre les versions de sala a https://spec.matrix.org/latest/rooms/", + "roomUpgradeDescription": "El xat serà recreat amb una versió de sala nova. Totis lis participants seran notificadis que han de canviar al nou xat. Pots llegir més sobre les versions de sala a https://spec.matrix.org/latest/rooms/", "@roomUpgradeDescription": {}, "pleaseEnterANumber": "Introdueix un número major que 0", "@pleaseEnterANumber": {}, @@ -2501,7 +2501,7 @@ "@searchForUsers": {}, "subspace": "Subespai", "@subspace": {}, - "addChatOrSubSpace": "Afegeix una sala o un subespai", + "addChatOrSubSpace": "Afegeix un xat o un subespai", "@addChatOrSubSpace": {}, "decline": "Denega", "@decline": {}, @@ -2644,7 +2644,7 @@ "@changeGeneralChatSettings": {}, "sendRoomNotifications": "Envia notificacions @room", "@sendRoomNotifications": {}, - "changeTheDescriptionOfTheGroup": "Canvia la descripció de la sala", + "changeTheDescriptionOfTheGroup": "Canvia la descripció del xat", "@changeTheDescriptionOfTheGroup": {}, "changelog": "Registre de canvis", "@changelog": {}, @@ -2664,11 +2664,11 @@ }, "inviteOtherUsers": "Convida més gent a la conversa", "@inviteOtherUsers": {}, - "changeTheChatPermissions": "Canvia els permisos de la sala", + "changeTheChatPermissions": "Canvia els permisos del xat", "@changeTheChatPermissions": {}, "changeTheVisibilityOfChatHistory": "Canvia la visibilitat de l'historial de conversa", "@changeTheVisibilityOfChatHistory": {}, - "changeTheCanonicalRoomAlias": "Canvia l'adreça principal de la sala", + "changeTheCanonicalRoomAlias": "Canvia l'adreça principal del xat", "@changeTheCanonicalRoomAlias": {}, "accessAndVisibilityDescription": "Qui pot entrar a aquesta conversa i com pot ser descoberta.", "@accessAndVisibilityDescription": {}, @@ -2707,7 +2707,7 @@ "@usersMustKnock": {}, "noOneCanJoin": "Ningú s'hi pot ficar", "@noOneCanJoin": {}, - "userWouldLikeToChangeTheChat": "{user} vol entrar a la sala.", + "userWouldLikeToChangeTheChat": "{user} vol entrar al xat.", "@userWouldLikeToChangeTheChat": { "placeholders": { "user": {} @@ -2717,7 +2717,7 @@ "@customEmojisAndStickersBody": {}, "hideRedactedMessagesBody": "Si algú estripa un missatge, ja no apareixerà a l'historial de la conversa.", "@hideRedactedMessagesBody": {}, - "searchIn": "Cerca a la sala \"{chat}\"...", + "searchIn": "Cerca al xat \"{chat}\"...", "@searchIn": { "type": "text", "placeholders": { @@ -2726,7 +2726,7 @@ }, "markAsUnread": "Marca com a no llegit", "@markAsUnread": {}, - "chatPermissionsDescription": "Defineix quin nivell de permisos cal per cada acció en aquesta sala. Els nivells 0, 50 i 100 normalment representen usuàriïs, mods i admins, però es pot canviar.", + "chatPermissionsDescription": "Defineix quin nivell de permisos cal per cada acció en aquest xat. Els nivells 0, 50 i 100 normalment representen usuàriïs, mods i admins, però es pot canviar.", "@chatPermissionsDescription": {}, "updateInstalled": "🎉 S'ha actualitzat a la versió {version}!", "@updateInstalled": { @@ -2751,7 +2751,7 @@ "@searchMore": {}, "files": "Arxius", "@files": {}, - "publicChatAddresses": "Adreces públiques de la sala", + "publicChatAddresses": "Adreces públiques del xat", "@publicChatAddresses": {}, "unreadChatsInApp": "{appname}: {unread} converses pendents", "@unreadChatsInApp": { @@ -2796,11 +2796,98 @@ "@spaces": {}, "noPublicLinkHasBeenCreatedYet": "No s'ha creat cap enllaç públic", "@noPublicLinkHasBeenCreatedYet": {}, - "chatCanBeDiscoveredViaSearchOnServer": "La sala es pot descobrir amb la cerca de {server}", + "chatCanBeDiscoveredViaSearchOnServer": "El xat es pot descobrir amb la cerca de {server}", "@chatCanBeDiscoveredViaSearchOnServer": { "type": "text", "placeholders": { "server": {} } - } + }, + "calculatingFileSize": "S'està calculant la mida de l'arxiu...", + "@calculatingFileSize": {}, + "prepareSendingAttachment": "S'està preparant per enviar l'adjunt...", + "@prepareSendingAttachment": {}, + "generatingVideoThumbnail": "S'està generant la miniatura del vídeo...", + "@generatingVideoThumbnail": {}, + "noticeChatBackupDeviceVerification": "Nota: quan connectes tots els dispositius al backup del xat, es verifiquen automàticament.", + "@noticeChatBackupDeviceVerification": {}, + "continueText": "Continua", + "@continueText": {}, + "strikeThrough": "Text ratllat", + "@strikeThrough": {}, + "addLink": "Afegeix un enllaç", + "@addLink": {}, + "noContactInformationProvided": "El servidor no ofereix cap informació de contacte vàlida", + "@noContactInformationProvided": {}, + "setWallpaper": "Tria imatge de fons", + "@setWallpaper": {}, + "sendImages": "Envia {count} imatge", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "sendingAttachment": "S'està enviant l'adjunt...", + "@sendingAttachment": {}, + "compressVideo": "S'està comprimint el vídeo...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "S'està enviant l'adjunt {index} de {length}...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "serverLimitReached": "S'ha arribat al límit del servidor! Esperant {seconds} segons...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "oneOfYourDevicesIsNotVerified": "Un dels teus dispositius no està verificat", + "@oneOfYourDevicesIsNotVerified": {}, + "welcomeText": "Hola hola! 👋 Això és FluffyChat. Pots iniciar sessió en qualsevol servidor compatible amb https://matrix.org. I llavors xatejar amb qualsevol. És una xarxa enorme de missatgeria descentralitzada !", + "@welcomeText": {}, + "blur": "Difumina:", + "@blur": {}, + "opacity": "Opacitat:", + "@opacity": {}, + "manageAccount": "Gestiona el compte", + "@manageAccount": {}, + "contactServerAdmin": "Contacta l'admin del servidor", + "@contactServerAdmin": {}, + "contactServerSecurity": "Contacta l'equip de seguretat del servidor", + "@contactServerSecurity": {}, + "version": "Versió", + "@version": {}, + "website": "Lloc web", + "@website": {}, + "compress": "Comprimeix", + "@compress": {}, + "pleaseFillOut": "Emplena", + "@pleaseFillOut": {}, + "invalidUrl": "URL invàlida", + "@invalidUrl": {}, + "unableToJoinChat": "No s'ha pogut entrar al xat. Pot ser que l'altri participant hagi tancat la conversa.", + "@unableToJoinChat": {}, + "aboutHomeserver": "Quant a {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "supportPage": "Pàgina de suport", + "@supportPage": {}, + "serverInformation": "Informació del servidor:", + "@serverInformation": {}, + "name": "Nom", + "@name": {}, + "boldText": "Text en negreta", + "@boldText": {}, + "italicText": "Text en cursiva", + "@italicText": {} } diff --git a/assets/l10n/intl_cs.arb b/assets/l10n/intl_cs.arb index 6fa9911ad..af2306e77 100644 --- a/assets/l10n/intl_cs.arb +++ b/assets/l10n/intl_cs.arb @@ -2437,5 +2437,12 @@ "@presencesToggle": { "type": "text", "placeholders": {} + }, + "aboutHomeserver": "O {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } } } diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index cd14b9911..c0dffd4f3 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2882,5 +2882,21 @@ "strikeThrough": "Durchgestrichen", "@strikeThrough": {}, "pleaseFillOut": "Bitte ausfüllen", - "@pleaseFillOut": {} + "@pleaseFillOut": {}, + "sendImages": "Sende {count} Bilder", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "contactServerSecurity": "Server-Sicherheit kontaktieren", + "@contactServerSecurity": {}, + "compress": "Komprimieren", + "@compress": {}, + "supportPage": "Support-Seite", + "@supportPage": {}, + "serverInformation": "Server-Informationen:", + "@serverInformation": {}, + "appIntroduction": "Mit FluffyChat kannst du über verschiedene Messenger hinweg mit deinen Freunden chatten. Erfahre mehr dazu auf https://matrix.org oder tippe einfach auf *Fortfahren*." } diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 988e9c987..fa1a9f64f 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1918,6 +1918,13 @@ "type": "text", "placeholders": {} }, + "synchronizingPleaseWaitCounter": " Synchronizing… ({percentage}%)", + "@synchronizingPleaseWaitCounter": { + "type": "text", + "placeholders": { + "percentage": {} + } + }, "systemTheme": "System", "@systemTheme": { "type": "text", @@ -4668,6 +4675,7 @@ "chooseBestDefinition": "What does this word mean?", "chooseBaseForm": "Choose the base form", "notTheCodeError": "Sorry, that's not the code!", + "previous": "Previous", "totalXP": "Total XP", "numLemmas": "Total number of lemmas", "listOfLemmas": "List of lemmas", @@ -4745,6 +4753,8 @@ }, "notInClass": "Not in a class!", "noClassCode": "No class code!", + "previous": "Previous", + "otherPartyNotLoggedIn": "The other party is currently not logged in and therefore cannot receive messages!", "chooseCorrectLabel": "Choose the correct label", "levelPopupTitle": "Congratulations on reaching\nLevel {level}", "@levelPopupTitle": { @@ -4774,6 +4784,7 @@ "activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!", "completeActivitiesToUnlock": "Complete the highlighted word activities to unlock", "myBookmarkedActivities": "My Bookmarked Activities", + "noBookmarkedActivities": "No bookmarked activities", "noBookmarkedActivities": "When you bookmark activities, they will appear here. Bookmarked activities can be re-used across spaces and chats.", "activityTitle": "Activity Title", "addVocabulary": "Add Vocabulary", @@ -4791,5 +4802,17 @@ }, "constructUsePvmDesc": "Produced in voice message", "lockedMorphFeature": "Waiting to be unlocked", - "leaveSpaceDescription": "The space will be moved to the archive. Other users will be able to see that you have left the chat." -} \ No newline at end of file + "leaveSpaceDescription": "The space will be moved to the archive. Other users will be able to see that you have left the chat.", + "otherPartyNotLoggedIn": "The other party is currently not logged in and therefore cannot receive messages!", + "appWantsToUseForLogin": "Use '{server}' to log in", + "@appWantsToUseForLogin": { + "type": "text", + "placeholders": { + "server": {} + } + }, + "appWantsToUseForLoginDescription": "You hereby allow the app and website to share information about you.", + "open": "Open", + "waitingForServer": "Waiting for server...", + "appIntroduction": "FluffyChat lets you chat with your friends across different messengers. Learn more at https://matrix.org or just tap *Continue*." +} diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 0048eff77..92eb1839e 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -2885,5 +2885,14 @@ "italicText": "Kaldkiri", "@italicText": {}, "unableToJoinChat": "Vestlusega liitumine ei õnnestu. Võib-olla on teine osapool juba vestluse sulgenud.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "Saada {count} pilti", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Paki kokku", + "@compress": {} } diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index be2044b3c..d8f170604 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -2885,5 +2885,14 @@ "pleaseFillOut": "Bete ezazu", "@pleaseFillOut": {}, "unableToJoinChat": "Ezin da txatera batu. Agian besteak elkarrizketa itxiko zuen honezkero.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "Bidali {count} irudi", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Konprimatu", + "@compress": {} } diff --git a/assets/l10n/intl_ga.arb b/assets/l10n/intl_ga.arb index 3dd2c9011..e0ab9a9ab 100644 --- a/assets/l10n/intl_ga.arb +++ b/assets/l10n/intl_ga.arb @@ -2888,5 +2888,14 @@ "invalidUrl": "URL neamhbhailí", "@invalidUrl": {}, "unableToJoinChat": "Ní féidir páirt a ghlacadh sa chomhrá. B’fhéidir go bhfuil an comhrá dúnta cheana féin ag an bpáirtí eile.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "compress": "Comhbhrúigh", + "@compress": {}, + "sendImages": "Seol {count} íomhá", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + } } diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index e3e70e764..dbc4d1593 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2885,5 +2885,14 @@ "addLink": "Engadir ligazón", "@addLink": {}, "unableToJoinChat": "Non se puido acceder. Pode que a outra parte xa pechase a conversa.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "Enviar {count} imaxe", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Comprimir", + "@compress": {} } diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index 3328eba9f..8a03af882 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -2882,5 +2882,16 @@ "boldText": "Teks tebal", "@boldText": {}, "italicText": "Teks miring", - "@italicText": {} + "@italicText": {}, + "unableToJoinChat": "Tidak dapat bergabung dalam chat. Mungkin pihak lain telah menutup percakapan.", + "@unableToJoinChat": {}, + "sendImages": "Kirim {count} gambar", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Kompres", + "@compress": {} } diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index 7a9403b9d..8e80d047c 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -240,7 +240,7 @@ "joinRules": {} } }, - "changedTheProfileAvatar": "{username} ha cambiato il loro avatar", + "changedTheProfileAvatar": "{username} ha cambiato il suo avatar", "@changedTheProfileAvatar": { "type": "text", "placeholders": { @@ -764,7 +764,7 @@ "targetName": {} } }, - "invitedUsersOnly": "Solo per gli utenti invitati", + "invitedUsersOnly": "Solo utenti invitati", "@invitedUsersOnly": { "type": "text", "placeholders": {} @@ -815,7 +815,7 @@ "targetName": {} } }, - "kickFromChat": "Espulsa dalla discussione", + "kickFromChat": "Espelli dalla chat", "@kickFromChat": { "type": "text", "placeholders": {} @@ -2884,5 +2884,14 @@ "addLink": "Aggiungi collegamento", "@addLink": {}, "unableToJoinChat": "Impossibile partecipare alla chat. Forse l'altra parte ha già chiuso la conversazione.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "Invia {count} immagine", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Comprimere", + "@compress": {} } diff --git a/assets/l10n/intl_lv.arb b/assets/l10n/intl_lv.arb index b242f8ae8..513537ee7 100644 --- a/assets/l10n/intl_lv.arb +++ b/assets/l10n/intl_lv.arb @@ -643,7 +643,7 @@ "count": {} } }, - "noKeyForThisMessage": "Tā var notikt, ja ziņa tika nosūtīta, pirms pieteicies savā kontā šajā ierīcē.\n\nIr arī iespējams, ka sūtītājs noliedza Tavu ierīci vai kaut kas nogāja greizi ar interneta savienojumu.\n\nVai ziņas ir lasāmas citā sesijā? Tad Tu vari pārsūtīt ziņo no tās. Jādodas uz Iestatījumi > Ierīces un jāpārliecinās, ka ierīces viena otru ir apliecinājušas. Kad nākamreiz atvērsi istabu un abas sesijas būs priekšplānā, atslēgas tiks automātiski pārsūtītas.\n\nVai nevēlies zaudēt atslēgas, kad atsakies vai maini ierīces? Jāpārliecinās, ka iestatījumos ir iespējota tērzēšanas rezerves kopija.", + "noKeyForThisMessage": "Tā var notikt, ja ziņa tika nosūtīta, pirms pieteicies savā kontā šajā ierīcē.\n\nIr arī iespējams, ka sūtītājs noliedza Tavu ierīci vai kaut kas nogāja greizi ar interneta savienojumu.\n\nVai ziņas ir lasāmas citā sesijā? Tad Tu vari pārsūtīt ziņo no tās. Jādodas uz Iestatījumi > Ierīces un jāpārliecinās, ka ierīces viena otru ir apliecinājušas. Kad nākamreiz atvērsi istabu un abas sesijas būs priekšplānā, atslēgas tiks automātiski pārsūtītas.\n\nVai nevēlies zaudēt atslēgas, kad atsakies vai maini ierīces? Jāpārliecinās, ka iestatījumos ir iespējota tērzēšanu rezerves kopija.", "@noKeyForThisMessage": {}, "enableEncryptionWarning": "Vairs nebūs iespējams atspējot šifrēšanu. Vai tiešām to darīt?", "@enableEncryptionWarning": { @@ -712,7 +712,7 @@ "supportedVersions": {} } }, - "wipeChatBackup": "Notīrīt tērzēšanas rezerves kopiju, lai izveidotu jaunu atkopšanas atslēgu?", + "wipeChatBackup": "Notīrīt tērzēšanu rezerves kopiju, lai izveidotu jaunu atkopšanas atslēgu?", "@wipeChatBackup": { "type": "text", "placeholders": {} @@ -911,9 +911,9 @@ }, "storeSecurlyOnThisDevice": "Droši uzglabāt šajā ierīcē", "@storeSecurlyOnThisDevice": {}, - "yourChatBackupHasBeenSetUp": "Tērzēšanas rezerves kopēšana tika iestatīta.", + "yourChatBackupHasBeenSetUp": "Tērzēšanu rezerves kopēšana tika iestatīta.", "@yourChatBackupHasBeenSetUp": {}, - "chatBackup": "Tērzēšanas rezerves kopēšana", + "chatBackup": "Tērzēšanu rezerves kopēšana", "@chatBackup": { "type": "text", "placeholders": {} @@ -2320,7 +2320,7 @@ }, "custom": "Pielāgots", "@custom": {}, - "noBackupWarning": "Uzmanību! Bez tērzēšanas rezerves kopijas iespējošanas tiks zaudēta piekļuve savām šifrētajām ziņām. Ir ļoti ieteicams iespējot tērzēšanas rezerves kopiju pirms atteikšanās.", + "noBackupWarning": "Uzmanību! Bez tērzēšanu rezerves kopiju veidošanas iespējošanas tiks zaudēta piekļuve savām šifrētajām ziņām. Ir ļoti ieteicams iespējot tērzēšanu rezerves kopiju veidošanu pirms atteikšanās.", "@noBackupWarning": {}, "fromJoining": "No pievienošanās", "@fromJoining": { @@ -2809,7 +2809,7 @@ "@compressVideo": {}, "oneOfYourDevicesIsNotVerified": "Viena no ierīcēm nav apliecināta", "@oneOfYourDevicesIsNotVerified": {}, - "noticeChatBackupDeviceVerification": "Piezīme: kad visas ierīces tiek savienotas ar tērzēšanas rezerves kopiju, tās tiek automātiski apliecinātas.", + "noticeChatBackupDeviceVerification": "Piezīme: kad visas ierīces tiek savienotas ar tērzēšanu rezerves kopiju, tās tiek automātiski apliecinātas.", "@noticeChatBackupDeviceVerification": {}, "continueText": "Turpināt", "@continueText": {}, @@ -2861,5 +2861,16 @@ "pleaseFillOut": "Lūgums aizpildīt", "@pleaseFillOut": {}, "sendUncompressed": "Sūtīt nesaspiestu", - "@sendUncompressed": {} + "@sendUncompressed": {}, + "sendImages": "Nosūtīt {count} attēlu(s)", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Saspiest", + "@compress": {}, + "unableToJoinChat": "Nevarēja pievienoties tērzēšanai. Varbūt otra puse jau ir aizvērusi sarunu.", + "@unableToJoinChat": {} } diff --git a/assets/l10n/intl_pl.arb b/assets/l10n/intl_pl.arb index 506b85f6d..9808a82c6 100644 --- a/assets/l10n/intl_pl.arb +++ b/assets/l10n/intl_pl.arb @@ -45,7 +45,7 @@ "type": "text", "placeholders": {} }, - "answeredTheCall": "{senderName} odebrał połączenie", + "answeredTheCall": "{senderName} odebrał/-a połączenie", "@answeredTheCall": { "type": "text", "placeholders": { @@ -89,7 +89,7 @@ "username": {} } }, - "badServerLoginTypesException": "Serwer wspiera typy logowania:\n{serverVersions}\nAle ta aplikacja wpiera tylko:\n{supportedVersions}", + "badServerLoginTypesException": "Serwer obsługuje typy logowania:\n{serverVersions}\nAle ta aplikacja obsługuje tylko:\n{supportedVersions}", "@badServerLoginTypesException": { "type": "text", "placeholders": { @@ -97,7 +97,7 @@ "supportedVersions": {} } }, - "badServerVersionsException": "Serwer wspiera wersje Spec:\n{serverVersions}\nAle aplikacja wspiera tylko {supportedVersions}", + "badServerVersionsException": "Serwer obsługuje wersje Spec:\n{serverVersions}\nAle aplikacja obsługuje tylko {supportedVersions}", "@badServerVersionsException": { "type": "text", "placeholders": { @@ -105,7 +105,7 @@ "supportedVersions": {} } }, - "banFromChat": "Ban na czacie", + "banFromChat": "Ban w czacie", "@banFromChat": { "type": "text", "placeholders": {} @@ -123,7 +123,7 @@ "targetName": {} } }, - "blockDevice": "Zablokuj Urządzenie", + "blockDevice": "Zablokuj urządzenie", "@blockDevice": { "type": "text", "placeholders": {} @@ -133,7 +133,7 @@ "type": "text", "placeholders": {} }, - "botMessages": "Wiadomości Botów", + "botMessages": "Wiadomości botów", "@botMessages": { "type": "text", "placeholders": {} @@ -155,7 +155,7 @@ "username": {} } }, - "changedTheChatDescriptionTo": "{username} zmienił/-a opis czatu na: '{description}'", + "changedTheChatDescriptionTo": "{username} zmienił/-a opis czatu na '{description}'", "@changedTheChatDescriptionTo": { "type": "text", "placeholders": { @@ -163,7 +163,7 @@ "description": {} } }, - "changedTheChatNameTo": "{username} zmienił/-a nick na: '{chatname}'", + "changedTheChatNameTo": "{username} zmienił/-a nazwę czatu na '{chatname}'", "@changedTheChatNameTo": { "type": "text", "placeholders": { @@ -171,14 +171,14 @@ "chatname": {} } }, - "changedTheChatPermissions": "{username} zmienił/-a uprawnienia czatu", + "changedTheChatPermissions": "{username} zmienił/-a uprawnienia w czacie", "@changedTheChatPermissions": { "type": "text", "placeholders": { "username": {} } }, - "changedTheDisplaynameTo": "{username} zmienił/-a swój nick na: '{displayname}'", + "changedTheDisplaynameTo": "{username} zmienił/-a swoją nazwę wyświetlaną na '{displayname}'", "@changedTheDisplaynameTo": { "type": "text", "placeholders": { @@ -193,7 +193,7 @@ "username": {} } }, - "changedTheGuestAccessRulesTo": "{username} zmienił/-a zasady dostępu dla gości na: {rules}", + "changedTheGuestAccessRulesTo": "{username} zmienił/-a zasady dostępu dla gości na {rules}", "@changedTheGuestAccessRulesTo": { "type": "text", "placeholders": { @@ -208,7 +208,7 @@ "username": {} } }, - "changedTheHistoryVisibilityTo": "{username} zmienił/-a widoczność historii na: {rules}", + "changedTheHistoryVisibilityTo": "{username} zmienił/-a widoczność historii na {rules}", "@changedTheHistoryVisibilityTo": { "type": "text", "placeholders": { @@ -223,7 +223,7 @@ "username": {} } }, - "changedTheJoinRulesTo": "{username} zmienił/-a zasady wejścia na: {joinRules}", + "changedTheJoinRulesTo": "{username} zmienił/-a zasady wejścia na {joinRules}", "@changedTheJoinRulesTo": { "type": "text", "placeholders": { @@ -238,14 +238,14 @@ "username": {} } }, - "changedTheRoomAliases": "{username} zmienił/-a skrót pokoju", + "changedTheRoomAliases": "{username} zmienił/-a alias pokoju", "@changedTheRoomAliases": { "type": "text", "placeholders": { "username": {} } }, - "changedTheRoomInvitationLink": "{username} zmienił/-a link do zaproszenia do pokoju", + "changedTheRoomInvitationLink": "{username} zmienił/-a link z zaproszeniem do pokoju", "@changedTheRoomInvitationLink": { "type": "text", "placeholders": { @@ -277,12 +277,12 @@ "type": "text", "placeholders": {} }, - "chat": "Rozmowa", + "chat": "Czat", "@chat": { "type": "text", "placeholders": {} }, - "chatBackup": "Kopia zapasowa Rozmów", + "chatBackup": "Kopia zapasowa czatów", "@chatBackup": { "type": "text", "placeholders": {} @@ -297,7 +297,7 @@ "type": "text", "placeholders": {} }, - "chats": "Rozmowy", + "chats": "Czaty", "@chats": { "type": "text", "placeholders": {} @@ -314,7 +314,7 @@ "type": "text", "placeholders": {} }, - "commandHint_ban": "Zablokuj użytkownika w tym pokoju", + "commandHint_ban": "Zbanuj użytkownika w tym pokoju", "@commandHint_ban": { "type": "text", "description": "Usage hint for the command /ban" @@ -339,7 +339,7 @@ "type": "text", "description": "Usage hint for the command /kick" }, - "commandHint_leave": "Wyjdź z tego pokoju", + "commandHint_leave": "Opuść ten pokój", "@commandHint_leave": { "type": "text", "description": "Usage hint for the command /leave" @@ -349,17 +349,17 @@ "type": "text", "description": "Usage hint for the command /me" }, - "commandHint_myroomavatar": "Ustaw awatar dla tego pokoju (przez mxc-uri)", + "commandHint_myroomavatar": "Ustaw swoje zdjęcie w tym pokoju (przez mxc-uri)", "@commandHint_myroomavatar": { "type": "text", "description": "Usage hint for the command /myroomavatar" }, - "commandHint_myroomnick": "Ustaw nazwę wyświetlaną dla tego pokoju", + "commandHint_myroomnick": "Ustaw swoją nazwę wyświetlaną w tym pokoju", "@commandHint_myroomnick": { "type": "text", "description": "Usage hint for the command /myroomnick" }, - "commandHint_op": "Ustaw moc uprawnień użytkownika (domyślnie: 50)", + "commandHint_op": "Ustaw poziom uprawnień tego użytkownika (domyślnie: 50)", "@commandHint_op": { "type": "text", "description": "Usage hint for the command /op" @@ -379,7 +379,7 @@ "type": "text", "description": "Usage hint for the command /send" }, - "commandHint_unban": "Odblokuj użytkownika w tym pokoju", + "commandHint_unban": "Odbanuj użytkownika w tym pokoju", "@commandHint_unban": { "type": "text", "description": "Usage hint for the command /unban" @@ -421,12 +421,12 @@ "type": "text", "placeholders": {} }, - "containsDisplayName": "Posiada wyświetlaną nazwę", + "containsDisplayName": "Zawiera nazwę wyświetlaną", "@containsDisplayName": { "type": "text", "placeholders": {} }, - "containsUserName": "Posiada nazwę użytkownika", + "containsUserName": "Zawiera nazwę użytkownika", "@containsUserName": { "type": "text", "placeholders": {} @@ -465,7 +465,7 @@ "type": "text", "placeholders": {} }, - "createdTheChat": "💬 {username} zaczął/-ęła rozmowę", + "createdTheChat": "💬 {username} utworzył/-a czat", "@createdTheChat": { "type": "text", "placeholders": { @@ -482,7 +482,7 @@ "type": "text", "placeholders": {} }, - "dateAndTimeOfDay": "{date}, {timeOfDay}", + "dateAndTimeOfDay": "{date} {timeOfDay}", "@dateAndTimeOfDay": { "type": "text", "placeholders": { @@ -532,7 +532,7 @@ "type": "text", "placeholders": {} }, - "deviceId": "ID Urządzenia", + "deviceId": "Identyfikator urządzenia", "@deviceId": { "type": "text", "placeholders": {} @@ -542,12 +542,12 @@ "type": "text", "placeholders": {} }, - "directChats": "Rozmowy bezpośrednie", + "directChats": "Czaty bezpośrednie", "@directChats": { "type": "text", "placeholders": {} }, - "displaynameHasBeenChanged": "Wyświetlany nick został zmieniony", + "displaynameHasBeenChanged": "Nazwa wyświetlana została zmieniona", "@displaynameHasBeenChanged": { "type": "text", "placeholders": {} @@ -562,7 +562,7 @@ "type": "text", "placeholders": {} }, - "editDisplayname": "Edytuj wyświetlany nick", + "editDisplayname": "Edytuj nazwę wyświetlaną", "@editDisplayname": { "type": "text", "placeholders": {} @@ -577,27 +577,27 @@ "type": "text", "placeholders": {} }, - "emoteInvalid": "Nieprawidłowy kod emotikony!", + "emoteInvalid": "Nieprawidłowy kod emotikonu!", "@emoteInvalid": { "type": "text", "placeholders": {} }, - "emotePacks": "Paczki emotikon dla pokoju", + "emotePacks": "Paczki emotikonów dla pokoju", "@emotePacks": { "type": "text", "placeholders": {} }, - "emoteSettings": "Ustawienia Emotikon", + "emoteSettings": "Ustawienia emotikonów", "@emoteSettings": { "type": "text", "placeholders": {} }, - "emoteShortcode": "Kod Emotikony", + "emoteShortcode": "Skrócony kod emotikonu", "@emoteShortcode": { "type": "text", "placeholders": {} }, - "emoteWarnNeedToPick": "Musisz wybrać kod emotikony oraz obraz!", + "emoteWarnNeedToPick": "Musisz wybrać kod emotikonu oraz obraz!", "@emoteWarnNeedToPick": { "type": "text", "placeholders": {} @@ -607,7 +607,7 @@ "type": "text", "placeholders": {} }, - "enableEmotesGlobally": "Włącz paczkę emotikon globalnie", + "enableEmotesGlobally": "Włącz paczkę emotikonów globalnie", "@enableEmotesGlobally": { "type": "text", "placeholders": {} @@ -632,14 +632,14 @@ "type": "text", "placeholders": {} }, - "endedTheCall": "{senderName} zakończył połączenie", + "endedTheCall": "{senderName} zakończył/-a połączenie", "@endedTheCall": { "type": "text", "placeholders": { "senderName": {} } }, - "enterAnEmailAddress": "Wpisz adres email", + "enterAnEmailAddress": "Wpisz adres e-mail", "@enterAnEmailAddress": { "type": "text", "placeholders": {} @@ -701,7 +701,7 @@ "displayname": {} } }, - "guestsAreForbidden": "Goście są zabronieni", + "guestsAreForbidden": "Goście są zakazani", "@guestsAreForbidden": { "type": "text", "placeholders": {} @@ -724,7 +724,7 @@ "type": "text", "placeholders": {} }, - "hideRedactedEvents": "Ukryj informacje o zredagowaniu", + "hideRedactedEvents": "Ukryj informacje o usuniętych zdarzeniach", "@hideRedactedEvents": { "type": "text", "placeholders": {} @@ -734,7 +734,7 @@ "type": "text", "placeholders": {} }, - "id": "ID", + "id": "Identyfikator", "@id": { "type": "text", "placeholders": {} @@ -759,7 +759,7 @@ "type": "text", "placeholders": {} }, - "inviteContact": "Zaproś kontakty", + "inviteContact": "Zaproś kontakt", "@inviteContact": { "type": "text", "placeholders": {} @@ -794,7 +794,7 @@ "type": "text", "placeholders": {} }, - "inviteText": "{username} zaprosił/-a cię do FluffyChat. \n1. Odwiedź fluffychat.im i zainstaluj aplikację\n2. Zarejestuj się lub zaloguj \n3. Otwórz link zaproszenia:\n{link}", + "inviteText": "{username} zaprosił/-a Cię do FluffyChat.\n1. Odwiedź fluffychat.im i zainstaluj aplikację\n2. Zarejestuj się lub zaloguj\n3. Otwórz link zaproszenia:\n{link}", "@inviteText": { "type": "text", "placeholders": { @@ -840,7 +840,7 @@ "type": "text", "placeholders": {} }, - "lastActiveAgo": "Ostatnio widziano: {localizedTimeShort}", + "lastActiveAgo": "Ostatnio widziano {localizedTimeShort}", "@lastActiveAgo": { "type": "text", "placeholders": { @@ -921,7 +921,7 @@ "type": "text", "placeholders": {} }, - "needPantalaimonWarning": "Należy pamiętać, że Pantalaimon wymaga na razie szyfrowania end-to-end.", + "needPantalaimonWarning": "Należy pamiętać, że Pantalaimon wymaga na razie szyfrowania od końca do końca.", "@needPantalaimonWarning": { "type": "text", "placeholders": {} @@ -931,7 +931,7 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nowa wiadomość w FluffyChat", + "newMessageInFluffyChat": "💬 Nowa wiadomość we FluffyChat", "@newMessageInFluffyChat": { "type": "text", "placeholders": {} @@ -956,7 +956,7 @@ "type": "text", "placeholders": {} }, - "noGoogleServicesWarning": "Wygląda na to, że nie masz usług Google w swoim telefonie. To dobra decyzja dla twojej prywatności! Aby otrzymywać powiadomienia wysyłane w FluffyChat, zalecamy korzystanie z https://microg.org/ lub https://unifiedpush.org/.", + "noGoogleServicesWarning": "Wygląda na to, że Twoje urządzenie nie obsługuje Firebase Cloud Messaging. Aby wciąż otrzymywać powiadomienia push, zalecamy istalację ntfy. Używając ntfy lub inengo zunifikowanego dostawcy powiadomień push, możesz bezpiecznie otrzymywać takowe powiadomienia. Ntfy można pobrać ze sklepu Google Play Store lub z F-Droid.", "@noGoogleServicesWarning": { "type": "text", "placeholders": {} @@ -966,7 +966,7 @@ "type": "text", "placeholders": {} }, - "noPasswordRecoveryDescription": "Nie dodałeś jeszcze sposobu aby odzyskać swoje hasło.", + "noPasswordRecoveryDescription": "Nie dodałeś/-aś jeszcze sposobu odzyskiwania swojego hasła.", "@noPasswordRecoveryDescription": { "type": "text", "placeholders": {} @@ -976,7 +976,7 @@ "type": "text", "placeholders": {} }, - "noRoomsFound": "Nie znaleziono pokoi…", + "noRoomsFound": "Nie znaleziono pokojów…", "@noRoomsFound": { "type": "text", "placeholders": {} @@ -991,7 +991,7 @@ "type": "text", "placeholders": {} }, - "oopsSomethingWentWrong": "Ups! Coś poszło nie tak…", + "oopsSomethingWentWrong": "Ojej! Coś poszło nie tak…", "@oopsSomethingWentWrong": { "type": "text", "placeholders": {} @@ -1038,12 +1038,12 @@ "type": "text", "placeholders": {} }, - "pleaseEnterYourUsername": "Wpisz swój nick", + "pleaseEnterYourUsername": "Wpisz swoją nazwę użytkownika", "@pleaseEnterYourUsername": { "type": "text", "placeholders": {} }, - "pleaseFollowInstructionsOnWeb": "Wykonaj instrukcje na stronie internetowej i naciśnij dalej.", + "pleaseFollowInstructionsOnWeb": "Wykonaj instrukcje na stronie internetowej i naciśnij „dalej”.", "@pleaseFollowInstructionsOnWeb": { "type": "text", "placeholders": {} @@ -1053,7 +1053,7 @@ "type": "text", "placeholders": {} }, - "pushRules": "Zasady push", + "pushRules": "Reguły push", "@pushRules": { "type": "text", "placeholders": {} @@ -1063,7 +1063,7 @@ "type": "text", "placeholders": {} }, - "redactedAnEvent": "{username} stworzył/-a wydarzenie", + "redactedAnEvent": "{username} usunął/-ęła zdarzenie", "@redactedAnEvent": { "type": "text", "placeholders": { @@ -1109,7 +1109,7 @@ "type": "text", "placeholders": {} }, - "unbanFromChat": "Odbanuj z czatu", + "unbanFromChat": "Odbanuj w czacie", "@unbanFromChat": { "type": "text", "placeholders": {} @@ -1163,7 +1163,7 @@ "username": {} } }, - "sentAnAudio": "🎤 {username} wysłał/-a plik audio", + "sentAnAudio": "🎤 {username} wysłał/-a plik dżwiękowy", "@sentAnAudio": { "type": "text", "placeholders": { @@ -1196,7 +1196,7 @@ "type": "text", "placeholders": {} }, - "setInvitationLink": "Ustaw link zaproszeniowy", + "setInvitationLink": "Ustaw link z zaproszeniem", "@setInvitationLink": { "type": "text", "placeholders": {} @@ -1216,7 +1216,7 @@ "type": "text", "placeholders": {} }, - "sharedTheLocation": "{username} udostępnił/-a swoją lokalizacje", + "sharedTheLocation": "{username} udostępnił/-a swoją lokalizację", "@sharedTheLocation": { "type": "text", "placeholders": { @@ -1306,7 +1306,7 @@ "unreadCount": {} } }, - "userAndOthersAreTyping": "{username} oraz {count} innych pisze…", + "userAndOthersAreTyping": "{username} oraz {count} pozostałych pisze…", "@userAndOthersAreTyping": { "type": "text", "placeholders": { @@ -1314,7 +1314,7 @@ "count": {} } }, - "userAndUserAreTyping": "{username} oraz {username2} piszą…", + "userAndUserAreTyping": "{username} i {username2} piszą…", "@userAndUserAreTyping": { "type": "text", "placeholders": { @@ -1341,7 +1341,7 @@ "type": "text", "placeholders": {} }, - "userSentUnknownEvent": "{username} wysłał/-a wydarzenie {type}", + "userSentUnknownEvent": "{username} wysłał/-a zdarzenie {type}", "@userSentUnknownEvent": { "type": "text", "placeholders": { @@ -1374,7 +1374,7 @@ "type": "text", "placeholders": {} }, - "visibleForEveryone": "Widoczny dla każdego", + "visibleForEveryone": "Widoczne dla każdego", "@visibleForEveryone": { "type": "text", "placeholders": {} @@ -1414,7 +1414,7 @@ "type": "text", "placeholders": {} }, - "youHaveBeenBannedFromThisChat": "Zostałeś/-aś zbanowany/-a z tego czatu", + "youHaveBeenBannedFromThisChat": "Zostałeś/-aś zbanowany/-a w tym czacie", "@youHaveBeenBannedFromThisChat": { "type": "text", "placeholders": {} @@ -1441,7 +1441,7 @@ }, "addAccount": "Dodaj konto", "@addAccount": {}, - "serverRequiresEmail": "Ten serwer wymaga potwierdzenia twojego adresu email w celu rejestracji.", + "serverRequiresEmail": "Ten serwer wymaga potwierdzenia Twojego adresu email w celu rejestracji.", "@serverRequiresEmail": {}, "or": "Lub", "@or": { @@ -1453,17 +1453,17 @@ "type": "text", "placeholders": {} }, - "passwordForgotten": "Zapomniano hasła", + "passwordForgotten": "Nie pamiętam hasła", "@passwordForgotten": { "type": "text", "placeholders": {} }, - "pleaseChoose": "Proszę wybierz", + "pleaseChoose": "Proszę wybrać", "@pleaseChoose": { "type": "text", "placeholders": {} }, - "pleaseClickOnLink": "Proszę kliknij w odnośnik wysłany na email aby kontynuować.", + "pleaseClickOnLink": "Proszę kliknij w odnośnik wysłany w wiadomości e-mail, aby kontynuować.", "@pleaseClickOnLink": { "type": "text", "placeholders": {} @@ -1473,7 +1473,7 @@ "type": "text", "placeholders": {} }, - "removeYourAvatar": "Usuń swój avatar", + "removeYourAvatar": "Usuń swoje zdjęcie", "@removeYourAvatar": { "type": "text", "placeholders": {} @@ -1483,7 +1483,7 @@ "type": "text", "placeholders": {} }, - "replaceRoomWithNewerVersion": "Zamień pokój na nową wersję", + "replaceRoomWithNewerVersion": "Zamień pokój na nowszą wersję", "@replaceRoomWithNewerVersion": { "type": "text", "placeholders": {} @@ -1537,7 +1537,7 @@ "type": "text", "placeholders": {} }, - "startedACall": "{senderName} rozpoczął rozmowę", + "startedACall": "{senderName} rozpoczął/-ęła rozmowę", "@startedACall": { "type": "text", "placeholders": { @@ -1549,7 +1549,7 @@ "type": "text", "placeholders": {} }, - "enableMultiAccounts": "(BETA) Włącza obsługę wiele kont na tym urządzeniu", + "enableMultiAccounts": "(BETA) Włącza obsługę wielu kont na tym urządzeniu", "@enableMultiAccounts": {}, "pickImage": "Wybierz obraz", "@pickImage": { @@ -1585,14 +1585,14 @@ "type": "text", "placeholders": {} }, - "theyMatch": "Pasują", + "theyMatch": "Zgadzają się", "@theyMatch": { "type": "text", "placeholders": {} }, "sendOnEnter": "Wyślij enterem", "@sendOnEnter": {}, - "autoplayImages": "Automatycznie odtwarzaj animowane naklejki i emotki", + "autoplayImages": "Automatycznie odtwarzaj animowane naklejki i emotikony", "@autoplayImages": { "type": "text", "placeholder": {} @@ -1604,7 +1604,7 @@ "uri": {} } }, - "configureChat": "Konfiguruj chat", + "configureChat": "Konfiguruj czat", "@configureChat": { "type": "text", "placeholders": {} @@ -1623,7 +1623,7 @@ "type": "text", "placeholders": {} }, - "theyDontMatch": "Nie pasują", + "theyDontMatch": "Nie zgadzają się", "@theyDontMatch": { "type": "text", "placeholders": {} @@ -1633,11 +1633,11 @@ "type": "text", "placeholders": {} }, - "yourChatBackupHasBeenSetUp": "Twoja kopia zapasowa chatu została ustawiona.", + "yourChatBackupHasBeenSetUp": "Kopia zapasowa Twojego czatu została ustawiona.", "@yourChatBackupHasBeenSetUp": {}, - "chatHasBeenAddedToThisSpace": "Chat został dodany do tej przestrzeni", + "chatHasBeenAddedToThisSpace": "Czat został dodany do tej przestrzeni", "@chatHasBeenAddedToThisSpace": {}, - "contentHasBeenReported": "Zawartość została zgłoszona administratorom serwera", + "contentHasBeenReported": "Treść została zgłoszona administratorom serwera", "@contentHasBeenReported": { "type": "text", "placeholders": {} @@ -1647,7 +1647,7 @@ "type": "text", "placeholders": {} }, - "fontSize": "Rozmiar czcionki", + "fontSize": "Rozmiar fontu", "@fontSize": { "type": "text", "placeholders": {} @@ -1672,7 +1672,7 @@ "type": "text", "placeholders": {} }, - "offensive": "Agresywne", + "offensive": "Obraźliwe", "@offensive": { "type": "text", "placeholders": {} @@ -1687,12 +1687,12 @@ "type": "text", "placeholders": {} }, - "redactMessage": "Utajnij wiadomość", + "redactMessage": "Usuń wiadomość", "@redactMessage": { "type": "text", "placeholders": {} }, - "setCustomEmotes": "Ustaw niestandardowe emotki", + "setCustomEmotes": "Ustaw niestandardowe emotikony", "@setCustomEmotes": { "type": "text", "placeholders": {} @@ -1758,7 +1758,7 @@ }, "addToSpace": "Dodaj do przestrzeni", "@addToSpace": {}, - "changeYourAvatar": "Zmień avatar", + "changeYourAvatar": "Zmień swoje zdjęcie", "@changeYourAvatar": { "type": "text", "placeholders": {} @@ -1768,27 +1768,27 @@ "type": "text", "description": "Usage hint for the command /clearcache" }, - "commandHint_create": "Stwórz pusty chat\nUżyj --no-encryption by wyłączyć szyfrowanie", + "commandHint_create": "Stwórz pusty czat\nUżyj --no-encryption by wyłączyć szyfrowanie", "@commandHint_create": { "type": "text", "description": "Usage hint for the command /create" }, - "commandHint_dm": "Rozpocznij bezpośredni chat\nUżyj --no-encryption by wyłączyć szyfrowanie", + "commandHint_dm": "Rozpocznij czat bezpośredni\nUżyj --no-encryption by wyłączyć szyfrowanie", "@commandHint_dm": { "type": "text", "description": "Usage hint for the command /dm" }, - "editBlockedServers": "Edytuj blokowane serwery", + "editBlockedServers": "Edytuj zablokowane serwery", "@editBlockedServers": { "type": "text", "placeholders": {} }, - "enableEncryption": "Aktywuj szyfowanie", + "enableEncryption": "Włącz szyfowanie", "@enableEncryption": { "type": "text", "placeholders": {} }, - "defaultPermissionLevel": "Domyślny poziom uprawnień", + "defaultPermissionLevel": "Domyślny poziom uprawnień dla nowych użytkowników", "@defaultPermissionLevel": { "type": "text", "placeholders": {} @@ -1798,7 +1798,7 @@ "type": "text", "placeholders": {} }, - "oopsPushError": "Ups! Wystąpił błąd podczas ustawiania powiadomień push.", + "oopsPushError": "Ojej! Wystąpił błąd podczas ustawiania powiadomień push.", "@oopsPushError": { "type": "text", "placeholders": {} @@ -1818,7 +1818,7 @@ "type": "text", "placeholders": {} }, - "tooManyRequestsWarning": "Zbyt wiele zapytań. Proszę spróbuj ponownie później.", + "tooManyRequestsWarning": "Zbyt wiele żądań. Proszę spróbować później.", "@tooManyRequestsWarning": { "type": "text", "placeholders": {} @@ -1883,7 +1883,7 @@ "error": {} } }, - "howOffensiveIsThisContent": "Jak bardzo obraźliwe są te treści?", + "howOffensiveIsThisContent": "Jak bardzo obraźliwa jest ta treść?", "@howOffensiveIsThisContent": { "type": "text", "placeholders": {} @@ -1952,7 +1952,7 @@ "@dehydrateTorLong": {}, "hydrate": "Przywracanie z pliku kopii zapasowej", "@hydrate": {}, - "noMatrixServer": "{server1} nie jest serwerem matriksa, czy chcesz zamiast niego użyć {server2}?", + "noMatrixServer": "{server1} nie jest serwerem Matriksa, czy chcesz zamiast niego użyć {server2}?", "@noMatrixServer": { "type": "text", "placeholders": { @@ -1960,7 +1960,7 @@ "server2": {} } }, - "hydrateTor": "Użytkownicy TOR-a: Importuj eksport sesji", + "hydrateTor": "Użytkownicy TOR: Importuj eksport sesji", "@hydrateTor": {}, "numUsersTyping": "{count} użytkowników pisze…", "@numUsersTyping": { @@ -1981,7 +1981,7 @@ "type": "text", "placeholders": {} }, - "weSentYouAnEmail": "Wysłaliśmy Ci maila", + "weSentYouAnEmail": "Wysłaliśmy Ci wiadomość e-mail", "@weSentYouAnEmail": { "type": "text", "placeholders": {} @@ -2026,7 +2026,7 @@ "@confirmMatrixId": {}, "commandHint_markasgroup": "Oznacz jako grupę", "@commandHint_markasgroup": {}, - "noEmotesFound": "Nie znaleziono żadnych emotek. 😕", + "noEmotesFound": "Nie znaleziono żadnych emotikonów. 😕", "@noEmotesFound": { "type": "text", "placeholders": {} @@ -2035,7 +2035,7 @@ "@dehydrate": {}, "dehydrateWarning": "Tego nie można cofnąć. Upewnij się, że plik kopii zapasowej jest bezpiecznie przechowywany.", "@dehydrateWarning": {}, - "dehydrateTor": "Użytkownicy TOR-a: Eksportuj sesję", + "dehydrateTor": "Użytkownicy TOR: Eksportuj sesję", "@dehydrateTor": {}, "unsupportedAndroidVersion": "Nieobsługiwana wersja systemu Android", "@unsupportedAndroidVersion": {}, @@ -2059,7 +2059,7 @@ "@newGroup": {}, "newSpace": "Nowa przestrzeń", "@newSpace": {}, - "fileIsTooBigForServer": "Serwer zgłasza, że plik jest zbyt duży, aby go wysłać.", + "fileIsTooBigForServer": "Nie udało się wysłać! Ten serwer obsługuje załączniki o maksymalnej wielkości {max}.", "@fileIsTooBigForServer": {}, "youBannedUser": "Zbanowałeś/-aś {user}", "@youBannedUser": { @@ -2276,7 +2276,7 @@ "@addToBundle": {}, "bundleName": "Nazwa pakietu", "@bundleName": {}, - "editBundlesForAccount": "Edytuj paczki dla tego konta", + "editBundlesForAccount": "Edytuj pakiety dla tego konta", "@editBundlesForAccount": {}, "jumpToLastReadMessage": "Przejdź do ostatnio przeczytanej wiadomości", "@jumpToLastReadMessage": {}, @@ -2284,7 +2284,7 @@ "@readUpToHere": {}, "jump": "Przejdź", "@jump": {}, - "removeFromBundle": "Usuń z tej paczki", + "removeFromBundle": "Usuń z tego pakietu", "@removeFromBundle": {}, "openLinkInBrowser": "Otwórz link w przeglądarce", "@openLinkInBrowser": {}, @@ -2293,7 +2293,7 @@ "type": "text", "placeholders": {} }, - "reportErrorDescription": "O nie. Coś poszło nie tak. Spróbuj ponownie później. Jeśli chcesz, możesz zgłosić błąd programistom.", + "reportErrorDescription": "😭 O nie! Coś poszło nie tak. Spróbuj ponownie później. Jeśli chcesz, możesz zgłosić ten błąd autorom programu.", "@reportErrorDescription": {}, "setColorTheme": "Ustal styl kolorów:", "@setColorTheme": {}, @@ -2305,15 +2305,15 @@ "@chatDescription": {}, "invalidServerName": "Nieprawidłowa nazwa serwera", "@invalidServerName": {}, - "chatPermissions": "Uprawnienia czatu", + "chatPermissions": "Uprawnienia w czacie", "@chatPermissions": {}, - "signInWithPassword": "Zaloguj się z hasłem", + "signInWithPassword": "Zaloguj się z hasłem", "@signInWithPassword": {}, "setChatDescription": "Ustaw opis czatu", "@setChatDescription": {}, "importFromZipFile": "Zaimportuj z pliku .zip", "@importFromZipFile": {}, - "redactedBy": "Utajnione przez {username}", + "redactedBy": "Usunięte przez {username}", "@redactedBy": { "type": "text", "placeholders": { @@ -2327,13 +2327,13 @@ "provider": {} } }, - "optionalRedactReason": "(Opcjonalnie) Powód utajnienia tej wiadomości...", + "optionalRedactReason": "(Opcjonalnie) Powód usunięcia tej wiadomości...", "@optionalRedactReason": {}, - "exportEmotePack": "Eksportuj pakiet Emotikon jako .zip", + "exportEmotePack": "Eksportuj pakiet emotikonów jako .zip", "@exportEmotePack": {}, "inviteContactToGroupQuestion": "Czy chcesz zaprosić {contact} do czatu „{groupName}”?", "@inviteContactToGroupQuestion": {}, - "redactedByBecause": "Utajnione przez {username} z powodu: \"{reason}\"", + "redactedByBecause": "Usunięte przez {username} z powodu „{reason}”", "@redactedByBecause": { "type": "text", "placeholders": { @@ -2341,7 +2341,7 @@ "reason": {} } }, - "redactMessageDescription": "Wiadomość zostanie utajniona u wszystkich uczestników tej rozmowy. Nie można tego cofnąć.", + "redactMessageDescription": "Wiadomość zostanie usunięta u wszystkich uczestników tej rozmowy. Tego nie można cofnąć.", "@redactMessageDescription": {}, "invalidInput": "Nieprawidłowe dane!", "@invalidInput": {}, @@ -2349,7 +2349,7 @@ "@report": {}, "addChatDescription": "Dodaj opis tego czatu...", "@addChatDescription": {}, - "directChat": "Rozmowa bezpośrednia", + "directChat": "Czat bezpośredni", "@directChat": {}, "wrongPinEntered": "Wprowadzono nieprawidłowy kod PIN! Spróbuj ponownie za {seconds} sekund...", "@wrongPinEntered": { @@ -2364,7 +2364,7 @@ "@inviteGroupChat": {}, "invitePrivateChat": "📨 Zaproszenie do rozmowy prywatnej", "@invitePrivateChat": {}, - "importEmojis": "Zaimportuj Emoji", + "importEmojis": "Zaimportuj emotikony", "@importEmojis": {}, "noChatDescriptionYet": "Nie utworzono jeszcze opisu czatu.", "@noChatDescriptionYet": {}, @@ -2374,7 +2374,7 @@ "@chatDescriptionHasBeenChanged": {}, "profileNotFound": "Nie można odnaleźć użytkownika na serwerze. Być może wystąpił problem z połączeniem lub użytkownik nie istnieje.", "@profileNotFound": {}, - "shareInviteLink": "Udostępnij link zaproszenia", + "shareInviteLink": "Udostępnij link z zaproszeniem", "@shareInviteLink": {}, "emoteKeyboardNoRecents": "Tutaj pojawiają się ostatnio używane emotikony...", "@emoteKeyboardNoRecents": { @@ -2432,5 +2432,463 @@ "nothingFound": "Nic nie odnaleziono...", "@nothingFound": {}, "stickers": "Naklejki", - "@stickers": {} + "@stickers": {}, + "noChatsFoundHere": "Nie jeszcze ma żadnych czatów. Wciśnij poniższy przycisk, aby rozpocząć nowy czat. ⤵️", + "@noChatsFoundHere": {}, + "hideRedactedMessagesBody": "Usunięte wiadomości nie będą widoczne w czacie.", + "@hideRedactedMessagesBody": {}, + "hideMemberChangesInPublicChats": "Ukryj zmiany członkostwa w publicznych czatach", + "@hideMemberChangesInPublicChats": {}, + "passwordRecoverySettings": "Ustawienia odzyskiwania hasła", + "@passwordRecoverySettings": {}, + "hideMemberChangesInPublicChatsBody": "W celu poprawienia czytelności, nie pokazuj w historii publicznego czatu, czy ktoś do niego dołączył lub go opuścił.", + "@hideMemberChangesInPublicChatsBody": {}, + "presenceStyle": "Obecność:", + "@presenceStyle": { + "type": "text", + "placeholders": {} + }, + "invitedBy": "Zaproszony/-a przez {user}", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "archiveRoomDescription": "Czat zostanie przeniesiony do archiwum. Pozostali użytkownicy będą mogli zobaczyć, że opuściłeś/-aś czat.", + "@archiveRoomDescription": {}, + "yourGlobalUserIdIs": "Twój globalny identyfikator to: ", + "@yourGlobalUserIdIs": {}, + "canceledKeyVerification": "{sender} anulował/-a weryfikację kluczy", + "@canceledKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "isReadyForKeyVerification": "{sender} jest gotowy/-a do weryfikacji kluczy", + "@isReadyForKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "commandHint_ignore": "Ignoruj podany identyfikator Matrix", + "@commandHint_ignore": {}, + "commandHint_unignore": "Przestań ignorować podany identyfikator Matrix", + "@commandHint_unignore": {}, + "changeTheChatPermissions": "Zmień uprawnienia w czacie", + "@changeTheChatPermissions": {}, + "changelog": "Lista zmian", + "@changelog": {}, + "inviteOtherUsers": "Zaproś innych użytkowników do tego czatu", + "@inviteOtherUsers": {}, + "blockListDescription": "Możesz zablokować uciążliwych użytkowników. Nie będziesz widzieć ani otrzymywać wiadomości oraz zaproszeń od nich.", + "@blockListDescription": {}, + "formattedMessages": "Sformatowane wiadomości", + "@formattedMessages": {}, + "banUserDescription": "Użytkownik zostanie zbanowany w czacie i nie będzie w stanie dołączyć do czatu do momentu odbanowania.", + "@banUserDescription": {}, + "subspace": "Podprzestrzeń", + "@subspace": {}, + "sendReadReceipts": "Wysyłaj powiadomienia o przeczytaniu wiadomości", + "@sendReadReceipts": {}, + "verifyOtherDevice": "🔐 Zweryfikuj inne urządzenie", + "@verifyOtherDevice": {}, + "prepareSendingAttachment": "Przygotuj wysyłanie załącznika...", + "@prepareSendingAttachment": {}, + "acceptedKeyVerification": "{sender} zaakceptował/-a weryfikację kluczy", + "@acceptedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "databaseMigrationTitle": "Baza danych jest zoptymalizowana", + "@databaseMigrationTitle": {}, + "hasKnocked": "{user} zapukał-/a", + "@hasKnocked": { + "placeholders": { + "user": {} + } + }, + "userLevel": "{level} - Użytkownik", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeTheVisibilityOfChatHistory": "Zmień widoczność historii czatu", + "@changeTheVisibilityOfChatHistory": {}, + "sendImages": "Wyślij {count} obrazów", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "noPublicLinkHasBeenCreatedYet": "Nie utworzono jeszcze żadnego publicznego linku", + "@noPublicLinkHasBeenCreatedYet": {}, + "knock": "Zapukaj", + "@knock": {}, + "databaseBuildErrorBody": "Nie udało się utworzyć bazy danych SQLite. Aplikacja na razie spróbuje korzystać ze starej bazy. Prosimy zgłosić ten błąd autorom aplikacji na {url}. Treść błędu to: {error}", + "@databaseBuildErrorBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "restoreSessionBody": "Aplikacja spróbuje teraz odzyskać Twoją sesję z kopii zapasowej. Prosimy zgłosić ten błąd autorom aplikacji na {url}. Treść błędu to: {error}", + "@restoreSessionBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "startedKeyVerification": "{sender} rozpoczął/-ęła weryfikację kluczy", + "@startedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "sendTypingNotificationsDescription": "Pozostali uczestnicy czatu mogą widzieć kiedy piszesz nową wiadomość.", + "@sendTypingNotificationsDescription": {}, + "sendReadReceiptsDescription": "Pozostali uczestnicy czatu mogą widzieć zobaczyć kiedy przeczytasz wiadomość.", + "@sendReadReceiptsDescription": {}, + "noDatabaseEncryption": "Szyfrowanie bazy danych nie jest obsługiwane na tej platformie", + "@noDatabaseEncryption": {}, + "thereAreCountUsersBlocked": "Obecnie jest {count} zablokowanych użytkowników.", + "@thereAreCountUsersBlocked": { + "type": "text", + "count": {} + }, + "goToSpace": "Przejdź do przestrzeni {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "markAsUnread": "Oznacz jako nieprzeczytane", + "@markAsUnread": {}, + "moderatorLevel": "{level} - Moderator", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - Administrator", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "sendRoomNotifications": "Wysyłaj powiadomienia @room", + "@sendRoomNotifications": {}, + "chatPermissionsDescription": "Ustal jaki poziom uprawnień jest wymagany dla określonych czynności w czacie. Poziomy uprawnień 0, 50 i 100 zwykle dotyczą odpowiednio użytkowników, moderatorów i administratorów, ale możliwa jest dowolna gradacja.", + "@chatPermissionsDescription": {}, + "changeTheCanonicalRoomAlias": "Zmień główny publiczny czatu", + "@changeTheCanonicalRoomAlias": {}, + "changeTheDescriptionOfTheGroup": "Zmień opis czatu", + "@changeTheDescriptionOfTheGroup": {}, + "sendCanceled": "Anulowano wysyłanie", + "@sendCanceled": {}, + "homeserverDescription": "Wszystkie Twoje dane trzymane są na serwerze domowym, jak u dostawców usług e-mail. Możesz wybrać swój serwer domowy i nadal rozmawiać ze wszystkimi. Dowiedz się więcej na https://matrix.org.", + "@homeserverDescription": {}, + "doesNotSeemToBeAValidHomeserver": "Wydaje się nie być kompatybilnym serwerem domowym. Niepoprawny adres URL?", + "@doesNotSeemToBeAValidHomeserver": {}, + "calculatingFileSize": "Obliczanie rozmiaru pliku...", + "@calculatingFileSize": {}, + "sendingAttachment": "Wysyłanie załącznika...", + "@sendingAttachment": {}, + "generatingVideoThumbnail": "Generowanie podglądu filmu...", + "@generatingVideoThumbnail": {}, + "compressVideo": "Kompresowanie filmu...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "Wysyłanie {index} z {length} części załącznika...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "welcomeText": "No cześć! 👋 Tutaj FluffyChat. Możesz zapisać się do dowolnego serwera domowego, kompatybilnego z https://matrix.org i rozmawiać ze wszystkimi. To duża zdecentralizowana sieć czatów!", + "@welcomeText": {}, + "blur": "Rozmazanie:", + "@blur": {}, + "opacity": "Przezroczystość:", + "@opacity": {}, + "setWallpaper": "Ustaw tapetę", + "@setWallpaper": {}, + "manageAccount": "Zarządzaj kontem", + "@manageAccount": {}, + "noContactInformationProvided": "Serwer nie dostarcza żadnych poprawnych danych kontaktowych", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Skontaktuj się z administratorem serwera", + "@contactServerAdmin": {}, + "compress": "Skompresuj", + "@compress": {}, + "pleaseFillOut": "Proszę wypełnić", + "@pleaseFillOut": {}, + "invalidUrl": "Niepoprawny adres URL", + "@invalidUrl": {}, + "unableToJoinChat": "Nie udało się dołączyć do czatu. Może druga strona zakończyła już rozmowę?", + "@unableToJoinChat": {}, + "aboutHomeserver": "O {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "updateInstalled": "🎉 Zainstalowano aktualizację do wersji {version}!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "continueText": "Kontynuuj", + "@continueText": {}, + "noticeChatBackupDeviceVerification": "Uwaga: Urządzenia dodane do kopii zapasowej czatu automatycznie zostają zweryfikowane.", + "@noticeChatBackupDeviceVerification": {}, + "serverLimitReached": "Osiągnięto limit serwera. Czekanie {seconds} sekund...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "oneOfYourDevicesIsNotVerified": "Tylko kiedy Twoje urządzenie nie jest zweryfikowane", + "@oneOfYourDevicesIsNotVerified": {}, + "supportPage": "Strona obsługi użytkownika", + "@supportPage": {}, + "serverInformation": "Informacje o serwerze:", + "@serverInformation": {}, + "name": "Nazwa", + "@name": {}, + "website": "Strona internetowa", + "@website": {}, + "contactServerSecurity": "Skontaktuj się z działem bezpieczeństwa serwera", + "@contactServerSecurity": {}, + "version": "Wersja", + "@version": {}, + "accessAndVisibility": "Dostęp i widoczność", + "@accessAndVisibility": {}, + "customEmojisAndStickers": "Własne emotikony i naklejki", + "@customEmojisAndStickers": {}, + "globalChatId": "Globalny identyfikator czatu", + "@globalChatId": {}, + "accessAndVisibilityDescription": "Kto może dołączyć do tego czatu i w jaki sposób można ten czat znaleźć.", + "@accessAndVisibilityDescription": {}, + "customEmojisAndStickersBody": "Dodaj lub podziel się własnymi emotikonami i naklejkami, które będą mogły być użyte w dowolnym czacie.", + "@customEmojisAndStickersBody": {}, + "hideRedactedMessages": "Nie pokazuj usuniętych wiadomości", + "@hideRedactedMessages": {}, + "hideInvalidOrUnknownMessageFormats": "Ukryj niepoprawne lub nieznane typy wiadomości", + "@hideInvalidOrUnknownMessageFormats": {}, + "notifyMeFor": "Powiadom mnie o", + "@notifyMeFor": {}, + "pushNotificationsNotAvailable": "Powiadomienia push nie są dostępne", + "@pushNotificationsNotAvailable": {}, + "noUsersFoundWithQuery": "Niestety nie udało się nikogo znaleźć poprzez \"{query}\". Proszę sprawdzić, czy w zapytaniu nie ma literówek.", + "@noUsersFoundWithQuery": { + "type": "text", + "placeholders": { + "query": {} + } + }, + "chatCanBeDiscoveredViaSearchOnServer": "Czat będzie można znaleźć, szukając na {server}", + "@chatCanBeDiscoveredViaSearchOnServer": { + "type": "text", + "placeholders": { + "server": {} + } + }, + "publicSpaces": "Przestrzenie publiczne", + "@publicSpaces": {}, + "searchMore": "Szukaj dalej...", + "@searchMore": {}, + "formattedMessagesDescription": "Używaj Markdown do wyświetlania dodatkowego formatowania w wiadomościach, jak np. pogrubienie tekstu.", + "@formattedMessagesDescription": {}, + "verifyOtherUser": "🔐 Zweryfikuj innego użytkownika", + "@verifyOtherUser": {}, + "knockRestricted": "Pukanie jest ograniczone", + "@knockRestricted": {}, + "appLockDescription": "Zablokuj aplikację pinem kiedy nie jest używana", + "@appLockDescription": {}, + "knocking": "Pukanie", + "@knocking": {}, + "pleaseChooseAStrongPassword": "Proszę wybrać silne hasło", + "@pleaseChooseAStrongPassword": {}, + "usersMustKnock": "Użytkownicy muszą zapukać", + "@usersMustKnock": {}, + "noOneCanJoin": "Nikt nie może dołączyć", + "@noOneCanJoin": {}, + "alwaysUse24HourFormat": "false", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "swipeRightToLeftToReply": "Przeciągnij w lewo, by odpowiedzieć", + "@swipeRightToLeftToReply": {}, + "presencesToggle": "Pokazuj zmiany statusów innych użytkowników", + "@presencesToggle": { + "type": "text", + "placeholders": {} + }, + "hidePresences": "Ukryć listę statusów?", + "@hidePresences": {}, + "pleaseEnterANumber": "Proszę podać liczbę większą od 0", + "@pleaseEnterANumber": {}, + "commandHint_sendraw": "Wyślij zwykły JSON", + "@commandHint_sendraw": {}, + "databaseMigrationBody": "Proszę czekać. Może to potrwać chwilę.", + "@databaseMigrationBody": {}, + "leaveEmptyToClearStatus": "Pozostaw puste, aby wyczyścić swój status.", + "@leaveEmptyToClearStatus": {}, + "sessionLostBody": "Twoja sesja została utracona. Prosimy zgłosić ten błąd autorom aplikacji na {url}. Treść błędu to: {error}", + "@sessionLostBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "forwardMessageTo": "Przekazać wiadomość do {roomName}?", + "@forwardMessageTo": { + "type": "text", + "placeholders": { + "roomName": {} + } + }, + "publicChatAddresses": "Adresy publicznych czatów", + "@publicChatAddresses": {}, + "createNewAddress": "Utwórz nowy adres", + "@createNewAddress": {}, + "userRole": "Rola użytkownika/-czki", + "@userRole": {}, + "completedKeyVerification": "{sender} zakończył/-a weryfikację kluczy", + "@completedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "italicText": "Kursywa", + "@italicText": {}, + "boldText": "Pogrubienie", + "@boldText": {}, + "strikeThrough": "Przekreślenie", + "@strikeThrough": {}, + "incomingMessages": "Wiadomości przychodzące", + "@incomingMessages": {}, + "discoverHomeservers": "Odkrywaj serwery domowe", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "Czym jest serwer domowy?", + "@whatIsAHomeserver": {}, + "loginWithMatrixId": "Zaloguj się identyfikatorem Matrix", + "@loginWithMatrixId": {}, + "passwordsDoNotMatch": "Hasła się nie zgadzają", + "@passwordsDoNotMatch": {}, + "unbanUserDescription": "Użytkownik będzie w stanie dołączyć do czatu ponownie.", + "@unbanUserDescription": {}, + "roomUpgradeDescription": "Czat zostanie przeniesiony do pokoju w nowej wersji. Wszyscy użytkownicy zostaną powiadomieni o konieczności dołączenia do nowego czatu. Możesz dowiedzieć się więcej o wersjach pokojów na https://spec.matrix.org/latest/rooms/", + "@roomUpgradeDescription": {}, + "userWouldLikeToChangeTheChat": "{user} chce dołączyć do czatu.", + "@userWouldLikeToChangeTheChat": { + "placeholders": { + "user": {} + } + }, + "requestedKeyVerification": "{sender} poprosił/-a o weryfikację kluczy", + "@requestedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "changeGeneralChatSettings": "Zmień ogólne ustawienia czatu", + "@changeGeneralChatSettings": {}, + "youInvitedToBy": "Otrzymałeś/-aś link z zaproszeniem do:\n{alias}", + "@youInvitedToBy": { + "placeholders": { + "alias": {} + } + }, + "verifyOtherUserDescription": "Jeśli zweryfikujesz innego użytkownika, możesz być pewien/-na z kim naprawdę piszesz. 💪\n\nKiedy rozpoczniesz weryfikację, Ty i ta druga osoba zobaczycie okienko dialogowe. Zobaczycie w nim serię emotikonów lub numery do porównania.\n\nNajlepiej potwierdzić ich zgodność osobiście lub przez wideorozmowę. 👭", + "@verifyOtherUserDescription": {}, + "verifyOtherDeviceDescription": "Jeśli zweryfikujesz inne urządzenie, będzie mogło ono wymienić klucze z dotychczasowym, zwiększając ogólne bezpieczeństwo. 💪 Kiedy rozpoczniesz weryfikację, na obu urządzeniach wyświetli się okno dialogowe. Zobaczysz w nim serię emotikonów lub numery do porównania. Najlepiej mieć oba urządzenia pod ręką przed rozpoczęciem weryfikacji. 🤳", + "@verifyOtherDeviceDescription": {}, + "unreadChatsInApp": "{appname}: {unread} nieprzeczytanych czatów", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "addLink": "Dodaj link", + "@addLink": {}, + "unread": "Nieprzeczytane", + "@unread": {}, + "space": "Przestrzeń", + "@space": {}, + "spaces": "Przestrzenie", + "@spaces": {}, + "countChatsAndCountParticipants": "{participants}{chats} czatów i {participants} uczestników", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "Nie znaleziono więcej czatów...", + "@noMoreChatsFound": {}, + "joinedChats": "Czaty, do których dołączono", + "@joinedChats": {}, + "removeDevicesDescription": "Nastąpi wylogowanie z tego urządzenia. Nie będziesz w stanie odbierać na nim wiadomości.", + "@removeDevicesDescription": {}, + "makeAdminDescription": "Kiedy użytkownik zostanie adminem, nie będziesz móc tego cofnąć, bo nabierze takich samych uprawnień, jak Ty.", + "@makeAdminDescription": {}, + "searchChatsRooms": "Szukaj #czatów, @użytkowników...", + "@searchChatsRooms": {}, + "createGroupAndInviteUsers": "Utwórz grupę i zaproś użytkowników", + "@createGroupAndInviteUsers": {}, + "groupCanBeFoundViaSearch": "Grupa może być znaleziona poprzez wyszukiwanie", + "@groupCanBeFoundViaSearch": {}, + "wrongRecoveryKey": "Niestety to nie wygląda na poprawny klucz odzyskiwania.", + "@wrongRecoveryKey": {}, + "searchForUsers": "Szukaj @użytkowników...", + "@searchForUsers": {}, + "pleaseEnterYourCurrentPassword": "Proszę podać swoje obecne hasło", + "@pleaseEnterYourCurrentPassword": {}, + "passwordIsWrong": "Podano niepoprawne hasło", + "@passwordIsWrong": {}, + "joinSpace": "Dołącz do przestrzeni", + "@joinSpace": {}, + "addChatOrSubSpace": "Dodaj czat lub podprzestrzeń", + "@addChatOrSubSpace": {}, + "initAppError": "Wystąpił błąd podczas inicjalizacji aplikacji", + "@initAppError": {}, + "minimumPowerLevel": "{level} jest minimalnym poziomem uprawnień.", + "@minimumPowerLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "searchIn": "Szukaj w czacie \"{chat}\"...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "kickUserDescription": "Użytkownik jest wyrzucony z czatu, ale nie zbanowany. Do czatu publicznego może dołączyć ponownie.", + "@kickUserDescription": {} } diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 56c1b1467..0d3a77d34 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -2883,5 +2883,14 @@ "version": "Версия", "@version": {}, "website": "Сайт", - "@website": {} + "@website": {}, + "sendImages": "Отправить {count} изображений", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Сжатие", + "@compress": {} } diff --git a/assets/l10n/intl_ta.arb b/assets/l10n/intl_ta.arb index 92d897376..b9f7df38e 100644 --- a/assets/l10n/intl_ta.arb +++ b/assets/l10n/intl_ta.arb @@ -1,1884 +1,2878 @@ { - "@@last_modified": "2021-08-14 12:41:09.826673", - "acceptedTheInvitation": "{username} அழைப்பை ஏற்றுக்கொண்டார்", - "@acceptedTheInvitation": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "accept": "ஏற்றுக்கொள்", - "@accept": { - "type": "text", - "placeholders": {} - }, - "about": "பற்றி", - "@about": { - "type": "text", - "placeholders": {} - }, - "@showPassword": { - "type": "text", - "placeholders": {} - }, - "@hugContent": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@darkTheme": { - "type": "text", - "placeholders": {} - }, - "@passphraseOrKey": { - "type": "text", - "placeholders": {} - }, - "@pleaseEnterYourPassword": { - "type": "text", - "placeholders": {} - }, - "@theyMatch": { - "type": "text", - "placeholders": {} - }, - "@connect": { - "type": "text", - "placeholders": {} - }, - "@jumpToLastReadMessage": {}, - "@allRooms": { - "type": "text", - "placeholders": {} - }, - "@obtainingLocation": { - "type": "text", - "placeholders": {} - }, - "@commandHint_cuddle": {}, - "@chats": { - "type": "text", - "placeholders": {} - }, - "@widgetVideo": {}, - "@dismiss": {}, - "@unknownDevice": { - "type": "text", - "placeholders": {} - }, - "@emoteShortcode": { - "type": "text", - "placeholders": {} - }, - "@noEncryptionForPublicRooms": { - "type": "text", - "placeholders": {} - }, - "@admin": { - "type": "text", - "placeholders": {} - }, - "@reportErrorDescription": {}, - "@directChats": { - "type": "text", - "placeholders": {} - }, - "@setPermissionsLevel": { - "type": "text", - "placeholders": {} - }, - "@inviteContactToGroup": { - "type": "text", - "placeholders": { - "groupName": {} - } - }, - "@addAccount": {}, - "@close": { - "type": "text", - "placeholders": {} - }, - "@configureChat": { - "type": "text", - "placeholders": {} - }, - "@chatHasBeenAddedToThisSpace": {}, - "@reply": { - "type": "text", - "placeholders": {} - }, - "@currentlyActive": { - "type": "text", - "placeholders": {} - }, - "@removeYourAvatar": { - "type": "text", - "placeholders": {} - }, - "@unsupportedAndroidVersion": {}, - "@device": { - "type": "text", - "placeholders": {} - }, - "@blockDevice": { - "type": "text", - "placeholders": {} - }, - "@commandHint_html": { - "type": "text", - "description": "Usage hint for the command /html" - }, - "@widgetJitsi": {}, - "@youAreNoLongerParticipatingInThisChat": { - "type": "text", - "placeholders": {} - }, - "@encryption": { - "type": "text", - "placeholders": {} - }, - "@messageType": {}, - "@indexedDbErrorLong": {}, - "@oneClientLoggedOut": {}, - "@toggleMuted": { - "type": "text", - "placeholders": {} - }, - "@kicked": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "@title": { - "description": "Title for the application", - "type": "text", - "placeholders": {} - }, - "@changeTheNameOfTheGroup": { - "type": "text", - "placeholders": {} - }, - "@changedTheChatAvatar": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@verifySuccess": { - "type": "text", - "placeholders": {} - }, - "@sendFile": { - "type": "text", - "placeholders": {} - }, - "@newVerificationRequest": { - "type": "text", - "placeholders": {} - }, - "@startFirstChat": {}, - "@callingAccount": {}, - "@requestPermission": { - "type": "text", - "placeholders": {} - }, - "@sentAPicture": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@invited": { - "type": "text", - "placeholders": {} - }, - "@setColorTheme": {}, - "@nextAccount": {}, - "@commandHint_create": { - "type": "text", - "description": "Usage hint for the command /create" - }, - "@singlesignon": { - "type": "text", - "placeholders": {} - }, - "@warning": { - "type": "text", - "placeholders": {} - }, - "@password": { - "type": "text", - "placeholders": {} - }, - "@allSpaces": {}, - "@supposedMxid": { - "type": "text", - "placeholders": { - "mxid": {} - } - }, - "@editDisplayname": { - "type": "text", - "placeholders": {} - }, - "@user": {}, - "@roomVersion": { - "type": "text", - "placeholders": {} - }, - "@sentAFile": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@videoCall": { - "type": "text", - "placeholders": {} - }, - "@youAcceptedTheInvitation": {}, - "@banFromChat": { - "type": "text", - "placeholders": {} - }, - "@noMatrixServer": { - "type": "text", - "placeholders": { - "server1": {}, - "server2": {} - } - }, - "@userAndOthersAreTyping": { - "type": "text", - "placeholders": { - "username": {}, - "count": {} - } - }, - "@youInvitedBy": { - "placeholders": { - "user": {} - } - }, - "@userIsTyping": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@openAppToReadMessages": { - "type": "text", - "placeholders": {} - }, - "@sentAVideo": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@banUserDescription": {}, - "@inviteContact": { - "type": "text", - "placeholders": {} - }, - "@askSSSSSign": { - "type": "text", - "placeholders": {} - }, - "@widgetEtherpad": {}, - "@waitingPartnerAcceptRequest": { - "type": "text", - "placeholders": {} - }, - "@remove": { - "type": "text", - "placeholders": {} - }, - "@writeAMessage": { - "type": "text", - "placeholders": {} - }, - "@changeTheme": { - "type": "text", - "placeholders": {} - }, - "@id": { - "type": "text", - "placeholders": {} - }, - "@removeDevicesDescription": {}, - "@changedTheChatDescriptionTo": { - "type": "text", - "placeholders": { - "username": {}, - "description": {} - } - }, - "@countParticipants": { - "type": "text", - "placeholders": { - "count": {} - } - }, - "@separateChatTypes": { - "type": "text", - "placeholders": {} - }, - "@tryAgain": {}, - "@areGuestsAllowedToJoin": { - "type": "text", - "placeholders": {} - }, - "@blocked": { - "type": "text", - "placeholders": {} - }, - "@youKickedAndBanned": { - "placeholders": { - "user": {} - } - }, - "@dateWithoutYear": { - "type": "text", - "placeholders": { - "month": {}, - "day": {} - } - }, - "@removeDevice": { - "type": "text", - "placeholders": {} - }, - "@unbanUserDescription": {}, - "@userAndUserAreTyping": { - "type": "text", - "placeholders": { - "username": {}, - "username2": {} - } - }, - "@pleaseClickOnLink": { - "type": "text", - "placeholders": {} - }, - "@saveFile": { - "type": "text", - "placeholders": {} - }, - "@sendOnEnter": {}, - "@pickImage": { - "type": "text", - "placeholders": {} - }, - "@answeredTheCall": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@youRejectedTheInvitation": {}, - "@otherCallingPermissions": {}, - "@messagesStyle": {}, - "@couldNotDecryptMessage": { - "type": "text", - "placeholders": { - "error": {} - } - }, - "@invitedUsersOnly": { - "type": "text", - "placeholders": {} - }, - "@link": {}, - "@widgetUrlError": {}, - "@emailOrUsername": {}, - "@newSpaceDescription": {}, - "@chatDescription": {}, - "@callingAccountDetails": {}, - "@next": { - "type": "text", - "placeholders": {} - }, - "@pleaseFollowInstructionsOnWeb": { - "type": "text", - "placeholders": {} - }, - "@changedTheGuestAccessRules": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@dateWithYear": { - "type": "text", - "placeholders": { - "year": {}, - "month": {}, - "day": {} - } - }, - "@editRoomAliases": { - "type": "text", - "placeholders": {} - }, - "@enterSpace": {}, - "@encryptThisChat": {}, - "@fileName": { - "type": "text", - "placeholders": {} - }, - "@unavailable": { - "type": "text", - "placeholders": {} - }, - "@previousAccount": {}, - "@publicRooms": { - "type": "text", - "placeholders": {} - }, - "@fromTheInvitation": { - "type": "text", - "placeholders": {} - }, - "@sendMessages": { - "type": "text", - "placeholders": {} - }, - "@incorrectPassphraseOrKey": { - "type": "text", - "placeholders": {} - }, - "@emoteWarnNeedToPick": { - "type": "text", - "placeholders": {} - }, - "@reopenChat": {}, - "@pleaseEnterRecoveryKey": {}, - "@create": { - "type": "text", - "placeholders": {} - }, - "@toggleFavorite": { - "type": "text", - "placeholders": {} - }, - "@no": { - "type": "text", - "placeholders": {} - }, - "@alias": { - "type": "text", - "placeholders": {} - }, - "@widgetNameError": {}, - "@inoffensive": { - "type": "text", - "placeholders": {} - }, - "@unpin": { - "type": "text", - "placeholders": {} - }, - "@addToBundle": {}, - "@reportMessage": { - "type": "text", - "placeholders": {} - }, - "@spaceIsPublic": { - "type": "text", - "placeholders": {} - }, - "@addWidget": {}, - "@all": { - "type": "text", - "placeholders": {} - }, - "@removeAllOtherDevices": { - "type": "text", - "placeholders": {} - }, - "@unblockDevice": { - "type": "text", - "placeholders": {} - }, - "@countFiles": { - "placeholders": { - "count": {} - } - }, - "@noKeyForThisMessage": {}, - "@enableEncryptionWarning": { - "type": "text", - "placeholders": {} - }, - "@inviteText": { - "type": "text", - "placeholders": { - "username": {}, - "link": {} - } - }, - "@shareLocation": { - "type": "text", - "placeholders": {} - }, - "@reason": { - "type": "text", - "placeholders": {} - }, - "@commandHint_markasgroup": {}, - "@errorObtainingLocation": { - "type": "text", - "placeholders": { - "error": {} - } - }, - "@hydrateTor": {}, - "@pushNotificationsNotAvailable": {}, - "@passwordRecovery": { - "type": "text", - "placeholders": {} - }, - "@storeInAppleKeyChain": {}, - "@replaceRoomWithNewerVersion": { - "type": "text", - "placeholders": {} - }, - "@hydrate": {}, - "@invalidServerName": {}, - "@chatPermissions": {}, - "@voiceMessage": { - "type": "text", - "placeholders": {} - }, - "@badServerLoginTypesException": { - "type": "text", - "placeholders": { - "serverVersions": {}, - "supportedVersions": {} - } - }, - "@wipeChatBackup": { - "type": "text", - "placeholders": {} - }, - "@cantOpenUri": { - "type": "text", - "placeholders": { - "uri": {} - } - }, - "@sender": {}, - "@storeInAndroidKeystore": {}, - "@hideRedactedEvents": { - "type": "text", - "placeholders": {} - }, - "@online": { - "type": "text", - "placeholders": {} - }, - "@signInWithPassword": {}, - "@ignoredUsers": { - "type": "text", - "placeholders": {} - }, - "@lastActiveAgo": { - "type": "text", - "placeholders": { - "localizedTimeShort": {} - } - }, - "@changedTheGuestAccessRulesTo": { - "type": "text", - "placeholders": { - "username": {}, - "rules": {} - } - }, - "@weSentYouAnEmail": { - "type": "text", - "placeholders": {} - }, - "@offensive": { - "type": "text", - "placeholders": {} - }, - "@needPantalaimonWarning": { - "type": "text", - "placeholders": {} - }, - "@makeAdminDescription": {}, - "@edit": { - "type": "text", - "placeholders": {} - }, - "@loadMore": { - "type": "text", - "placeholders": {} - }, - "@noEmotesFound": { - "type": "text", - "placeholders": {} - }, - "@synchronizingPleaseWait": { - "type": "text", - "placeholders": {} - }, - "@transferFromAnotherDevice": { - "type": "text", - "placeholders": {} - }, - "@passwordHasBeenChanged": { - "type": "text", - "placeholders": {} - }, - "@pushRules": { - "type": "text", - "placeholders": {} - }, - "@goToTheNewRoom": { - "type": "text", - "placeholders": {} - }, - "@commandHint_clearcache": { - "type": "text", - "description": "Usage hint for the command /clearcache" - }, - "@loadingPleaseWait": { - "type": "text", - "placeholders": {} - }, - "@copy": { - "type": "text", - "placeholders": {} - }, - "@saveKeyManuallyDescription": {}, - "@none": { - "type": "text", - "placeholders": {} - }, - "@editBundlesForAccount": {}, - "@renderRichContent": { - "type": "text", - "placeholders": {} - }, - "@enableEncryption": { - "type": "text", - "placeholders": {} - }, - "@whyIsThisMessageEncrypted": {}, - "@rejectedTheInvitation": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@setChatDescription": {}, - "@userLeftTheChat": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@spaceName": { - "type": "text", - "placeholders": {} - }, - "@importFromZipFile": {}, - "@toggleUnread": { - "type": "text", - "placeholders": {} - }, - "@or": { - "type": "text", - "placeholders": {} - }, - "@dehydrateWarning": {}, - "@sendOriginal": { - "type": "text", - "placeholders": {} - }, - "@noOtherDevicesFound": {}, - "@whoIsAllowedToJoinThisGroup": { - "type": "text", - "placeholders": {} - }, - "@emptyChat": { - "type": "text", - "placeholders": {} - }, - "@seenByUser": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@yourChatBackupHasBeenSetUp": {}, - "@chatBackup": { - "type": "text", - "placeholders": {} - }, - "@redactedBy": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@submit": { - "type": "text", - "placeholders": {} - }, - "@videoCallsBetaWarning": {}, - "@unmuteChat": { - "type": "text", - "placeholders": {} - }, - "@createdTheChat": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@redactedAnEvent": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@autoplayImages": { - "type": "text", - "placeholder": {} - }, - "@compareEmojiMatch": { - "type": "text", - "placeholders": {} - }, - "@participant": { - "type": "text", - "placeholders": {} - }, - "@logInTo": { - "type": "text", - "placeholders": { - "homeserver": {} - } - }, - "@yes": { - "type": "text", - "placeholders": {} - }, - "@containsDisplayName": { - "type": "text", - "placeholders": {} - }, - "@signInWith": { - "type": "text", - "placeholders": { - "provider": {} - } - }, - "@username": { - "type": "text", - "placeholders": {} - }, - "@changedTheRoomAliases": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@fileIsTooBigForServer": {}, - "@homeserver": {}, - "@help": { - "type": "text", - "placeholders": {} - }, - "@chatDetails": { - "type": "text", - "placeholders": {} - }, - "@people": { - "type": "text", - "placeholders": {} - }, - "@changedTheHistoryVisibilityTo": { - "type": "text", - "placeholders": { - "username": {}, - "rules": {} - } - }, - "@leftTheChat": { - "type": "text", - "placeholders": {} - }, - "@verified": { - "type": "text", - "placeholders": {} - }, - "@repeatPassword": {}, - "@setStatus": { - "type": "text", - "placeholders": {} - }, - "@groupWith": { - "type": "text", - "placeholders": { - "displayname": {} - } - }, - "@callingPermissions": {}, - "@delete": { - "type": "text", - "placeholders": {} - }, - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, - "@readUpToHere": {}, - "@start": {}, - "@downloadFile": { - "type": "text", - "placeholders": {} - }, - "@deviceId": { - "type": "text", - "placeholders": {} - }, - "@register": { - "type": "text", - "placeholders": {} - }, - "@unlockOldMessages": {}, - "@identity": { - "type": "text", - "placeholders": {} - }, - "@numChats": { - "type": "number", - "placeholders": { - "number": {} - } - }, - "@changedTheJoinRulesTo": { - "type": "text", - "placeholders": { - "username": {}, - "joinRules": {} - } - }, - "@ignore": { - "type": "text", - "placeholders": {} - }, - "@recording": { - "type": "text", - "placeholders": {} - }, - "@changedTheChatPermissions": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@moderator": { - "type": "text", - "placeholders": {} - }, - "@optionalRedactReason": {}, - "@waitingPartnerEmoji": { - "type": "text", - "placeholders": {} - }, - "@channelCorruptedDecryptError": { - "type": "text", - "placeholders": {} - }, - "@tryToSendAgain": { - "type": "text", - "placeholders": {} - }, - "@guestsCanJoin": { - "type": "text", - "placeholders": {} - }, - "@copyToClipboard": { - "type": "text", - "placeholders": {} - }, - "@dehydrate": {}, - "@locationPermissionDeniedNotice": { - "type": "text", - "placeholders": {} - }, - "@send": { - "type": "text", - "placeholders": {} - }, - "@hasWithdrawnTheInvitationFor": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "@visibleForAllParticipants": { - "type": "text", - "placeholders": {} - }, - "@noRoomsFound": { - "type": "text", - "placeholders": {} - }, - "@banned": { - "type": "text", - "placeholders": {} - }, - "@sendAsText": { - "type": "text" - }, - "@inviteForMe": { - "type": "text", - "placeholders": {} - }, - "@archiveRoomDescription": {}, - "@exportEmotePack": {}, - "@changedTheChatNameTo": { - "type": "text", - "placeholders": { - "username": {}, - "chatname": {} - } - }, - "@sendSticker": { - "type": "text", - "placeholders": {} - }, - "@account": { - "type": "text", - "placeholders": {} - }, - "@switchToAccount": { - "type": "number", - "placeholders": { - "number": {} - } - }, - "@commandInvalid": { - "type": "text" - }, - "@setAsCanonicalAlias": { - "type": "text", - "placeholders": {} - }, - "@whyDoYouWantToReportThis": { - "type": "text", - "placeholders": {} - }, - "@locationDisabledNotice": { - "type": "text", - "placeholders": {} - }, - "@removedBy": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@changedTheRoomInvitationLink": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@newChat": { - "type": "text", - "placeholders": {} - }, - "@notifications": { - "type": "text", - "placeholders": {} - }, - "@commandHint_plain": { - "type": "text", - "description": "Usage hint for the command /plain" - }, - "@emoteSettings": { - "type": "text", - "placeholders": {} - }, - "@experimentalVideoCalls": {}, - "@openCamera": { - "type": "text", - "placeholders": {} - }, - "@pleaseEnterRecoveryKeyDescription": {}, - "@guestsAreForbidden": { - "type": "text", - "placeholders": {} - }, - "@mention": { - "type": "text", - "placeholders": {} - }, - "@openInMaps": { - "type": "text", - "placeholders": {} - }, - "@withTheseAddressesRecoveryDescription": { - "type": "text", - "placeholders": {} - }, - "@inviteContactToGroupQuestion": {}, - "@emoteExists": { - "type": "text", - "placeholders": {} - }, - "@redactedByBecause": { - "type": "text", - "placeholders": { - "username": {}, - "reason": {} - } - }, - "@isTyping": { - "type": "text", - "placeholders": {} - }, - "@youHaveWithdrawnTheInvitationFor": { - "placeholders": { - "user": {} - } - }, - "@chat": { - "type": "text", - "placeholders": {} - }, - "@group": { - "type": "text", - "placeholders": {} - }, - "@leave": { - "type": "text", - "placeholders": {} - }, - "@skip": { - "type": "text", - "placeholders": {} - }, - "@appearOnTopDetails": {}, - "@roomHasBeenUpgraded": { - "type": "text", - "placeholders": {} - }, - "@enterRoom": {}, - "@enableEmotesGlobally": { - "type": "text", - "placeholders": {} - }, - "@areYouSure": { - "type": "text", - "placeholders": {} - }, - "@pleaseChooseAPasscode": { - "type": "text", - "placeholders": {} - }, - "@noPasswordRecoveryDescription": { - "type": "text", - "placeholders": {} - }, - "@changedTheProfileAvatar": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@allChats": { - "type": "text", - "placeholders": {} - }, - "@reportUser": {}, - "@commandHint_send": { - "type": "text", - "description": "Usage hint for the command /send" - }, - "@onlineKeyBackupEnabled": { - "type": "text", - "placeholders": {} - }, - "@unbannedUser": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "@confirmEventUnpin": {}, - "@badServerVersionsException": { - "type": "text", - "placeholders": { - "serverVersions": {}, - "supportedVersions": {} - } - }, - "@youInvitedUser": { - "placeholders": { - "user": {} - } - }, - "@kickedAndBanned": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "@noConnectionToTheServer": { - "type": "text", - "placeholders": {} - }, - "@fileHasBeenSavedAt": { - "type": "text", - "placeholders": { - "path": {} - } - }, - "@license": { - "type": "text", - "placeholders": {} - }, - "@addToSpace": {}, - "@commandMissing": { - "type": "text", - "placeholders": { - "command": {} - }, - "description": "State that {command} is not a valid /command." - }, - "@redactMessageDescription": {}, - "@rejoin": { - "type": "text", - "placeholders": {} - }, - "@recoveryKey": {}, - "@redactMessage": { - "type": "text", - "placeholders": {} - }, - "@forward": { - "type": "text", - "placeholders": {} - }, - "@commandHint_discardsession": { - "type": "text", - "description": "Usage hint for the command /discardsession" - }, - "@invalidInput": {}, - "@chooseAStrongPassword": { - "type": "text", - "placeholders": {} - }, - "@hideUnknownEvents": { - "type": "text", - "placeholders": {} - }, - "@dehydrateTorLong": {}, - "@yourPublicKey": { - "type": "text", - "placeholders": {} - }, - "@tooManyRequestsWarning": { - "type": "text", - "placeholders": {} - }, - "@invitedUser": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "@kickFromChat": { - "type": "text", - "placeholders": {} - }, - "@commandHint_myroomnick": { - "type": "text", - "description": "Usage hint for the command /myroomnick" - }, - "@offline": { - "type": "text", - "placeholders": {} - }, - "@noPermission": { - "type": "text", - "placeholders": {} - }, - "@doNotShowAgain": {}, - "@activatedEndToEndEncryption": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@report": {}, - "@status": { - "type": "text", - "placeholders": {} - }, - "@compareNumbersMatch": { - "type": "text", - "placeholders": {} - }, - "@groupIsPublic": { - "type": "text", - "placeholders": {} - }, - "@verifyStart": { - "type": "text", - "placeholders": {} - }, - "@memberChanges": { - "type": "text", - "placeholders": {} - }, - "@joinRoom": { - "type": "text", - "placeholders": {} - }, - "@unverified": {}, - "@fluffychat": { - "type": "text", - "placeholders": {} - }, - "@howOffensiveIsThisContent": { - "type": "text", - "placeholders": {} - }, - "@serverRequiresEmail": {}, - "@hideUnimportantStateEvents": {}, - "@screenSharingTitle": {}, - "@widgetCustom": {}, - "@sentCallInformations": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@addToSpaceDescription": {}, - "@googlyEyesContent": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@youBannedUser": { - "placeholders": { - "user": {} - } - }, - "@theyDontMatch": { - "type": "text", - "placeholders": {} - }, - "@youHaveBeenBannedFromThisChat": { - "type": "text", - "placeholders": {} - }, - "@displaynameHasBeenChanged": { - "type": "text", - "placeholders": {} - }, - "@addChatDescription": {}, - "@sentAnAudio": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@editRoomAvatar": { - "type": "text", - "placeholders": {} - }, - "@encrypted": { - "type": "text", - "placeholders": {} - }, - "@commandHint_leave": { - "type": "text", - "description": "Usage hint for the command /leave" - }, - "@commandHint_myroomavatar": { - "type": "text", - "description": "Usage hint for the command /myroomavatar" - }, - "@cancel": { - "type": "text", - "placeholders": {} - }, - "@hasKnocked": { - "placeholders": { - "user": {} - } - }, - "@publish": {}, - "@openLinkInBrowser": {}, - "@clearArchive": {}, - "@appLock": { - "type": "text", - "placeholders": {} - }, - "@commandHint_react": { - "type": "text", - "description": "Usage hint for the command /react" - }, - "@changedTheHistoryVisibility": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@commandHint_me": { - "type": "text", - "description": "Usage hint for the command /me" - }, - "@pleaseEnterYourUsername": { - "type": "text", - "placeholders": {} - }, - "@messageInfo": {}, - "@disableEncryptionWarning": {}, - "@directChat": {}, - "@encryptionNotEnabled": { - "type": "text", - "placeholders": {} - }, - "@wrongPinEntered": { - "type": "text", - "placeholders": { - "seconds": {} - } - }, - "@sendTypingNotifications": {}, - "@lightTheme": { - "type": "text", - "placeholders": {} - }, - "@inviteGroupChat": {}, - "@appearOnTop": {}, - "@invitePrivateChat": {}, - "@verifyTitle": { - "type": "text", - "placeholders": {} - }, - "@foregroundServiceRunning": {}, - "@enterAnEmailAddress": { - "type": "text", - "placeholders": {} - }, - "@voiceCall": {}, - "@commandHint_kick": { - "type": "text", - "description": "Usage hint for the command /kick" - }, - "@copiedToClipboard": { - "type": "text", - "placeholders": {} - }, - "@createNewSpace": { - "type": "text", - "placeholders": {} - }, - "@commandHint_unban": { - "type": "text", - "description": "Usage hint for the command /unban" - }, - "@unknownEncryptionAlgorithm": { - "type": "text", - "placeholders": {} - }, - "@commandHint_ban": { - "type": "text", - "description": "Usage hint for the command /ban" - }, - "@importEmojis": {}, - "@confirm": { - "type": "text", - "placeholders": {} - }, - "@wasDirectChatDisplayName": { - "type": "text", - "placeholders": { - "oldDisplayName": {} - } - }, - "@noChatDescriptionYet": {}, - "@defaultPermissionLevel": { - "type": "text", - "placeholders": {} - }, - "@removeFromBundle": {}, - "@numUsersTyping": { - "type": "text", - "placeholders": { - "count": {} - } - }, - "@fontSize": { - "type": "text", - "placeholders": {} - }, - "@whoCanPerformWhichAction": { - "type": "text", - "placeholders": {} - }, - "@confirmMatrixId": {}, - "@learnMore": {}, - "@iHaveClickedOnLink": { - "type": "text", - "placeholders": {} - }, - "@you": { - "type": "text", - "placeholders": {} - }, - "@notAnImage": {}, - "@users": {}, - "@openGallery": {}, - "@chatDescriptionHasBeenChanged": {}, - "@search": { - "type": "text", - "placeholders": {} - }, - "@newGroup": {}, - "@bundleName": {}, - "@dehydrateTor": {}, - "@removeFromSpace": {}, - "@dateAndTimeOfDay": { - "type": "text", - "placeholders": { - "date": {}, - "timeOfDay": {} - } - }, - "@commandHint_op": { - "type": "text", - "description": "Usage hint for the command /op" - }, - "@commandHint_join": { - "type": "text", - "description": "Usage hint for the command /join" - }, - "@sourceCode": { - "type": "text", - "placeholders": {} - }, - "@roomUpgradeDescription": {}, - "@commandHint_invite": { - "type": "text", - "description": "Usage hint for the command /invite" - }, - "@userSentUnknownEvent": { - "type": "text", - "placeholders": { - "username": {}, - "type": {} - } - }, - "@scanQrCode": {}, - "@logout": { - "type": "text", - "placeholders": {} - }, - "@pleaseEnterANumber": {}, - "@contactHasBeenInvitedToTheGroup": { - "type": "text", - "placeholders": {} - }, - "@youKicked": { - "placeholders": { - "user": {} - } - }, - "@areYouSureYouWantToLogout": { - "type": "text", - "placeholders": {} - }, - "@changedTheJoinRules": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@profileNotFound": {}, - "@jump": {}, - "@groups": { - "type": "text", - "placeholders": {} - }, - "@reactedWith": { - "type": "text", - "placeholders": { - "sender": {}, - "reaction": {} - } - }, - "@bannedUser": { - "type": "text", - "placeholders": { - "username": {}, - "targetName": {} - } - }, - "@sorryThatsNotPossible": {}, - "@videoWithSize": { - "type": "text", - "placeholders": { - "size": {} - } - }, - "@oopsSomethingWentWrong": { - "type": "text", - "placeholders": {} - }, - "@loadCountMoreParticipants": { - "type": "text", - "placeholders": { - "count": {} - } - }, - "@shareInviteLink": {}, - "@commandHint_markasdm": {}, - "@recoveryKeyLost": {}, - "@cuddleContent": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@askVerificationRequest": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@containsUserName": { - "type": "text", - "placeholders": {} - }, - "@messages": { - "type": "text", - "placeholders": {} - }, - "@login": { - "type": "text", - "placeholders": {} - }, - "@deviceKeys": {}, - "@waitingPartnerNumbers": { - "type": "text", - "placeholders": {} - }, - "@noGoogleServicesWarning": { - "type": "text", - "placeholders": {} - }, - "@everythingReady": { - "type": "text", - "placeholders": {} - }, - "@addEmail": { - "type": "text", - "placeholders": {} - }, - "@emoteKeyboardNoRecents": { - "type": "text", - "placeholders": {} - }, - "@setCustomEmotes": { - "type": "text", - "placeholders": {} - }, - "@startedACall": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@emoteInvalid": { - "type": "text", - "placeholders": {} - }, - "@systemTheme": { - "type": "text", - "placeholders": {} - }, - "@notificationsEnabledForThisAccount": { - "type": "text", - "placeholders": {} - }, - "@deleteMessage": { - "type": "text", - "placeholders": {} - }, - "@visibilityOfTheChatHistory": { - "type": "text", - "placeholders": {} - }, - "@settings": { - "type": "text", - "placeholders": {} - }, - "@setTheme": {}, - "@changeTheHomeserver": { - "type": "text", - "placeholders": {} - }, - "@youJoinedTheChat": {}, - "@wallpaper": { - "type": "text", - "placeholders": {} - }, - "@openVideoCamera": { - "type": "text", - "placeholders": {} - }, - "@play": { - "type": "text", - "placeholders": { - "fileName": {} - } - }, - "@chatBackupDescription": { - "type": "text", - "placeholders": {} - }, - "@changeDeviceName": { - "type": "text", - "placeholders": {} - }, - "@passwordForgotten": { - "type": "text", - "placeholders": {} - }, - "@statusExampleMessage": { - "type": "text", - "placeholders": {} - }, - "@security": { - "type": "text", - "placeholders": {} - }, - "@markAsRead": {}, - "@sendAudio": { - "type": "text", - "placeholders": {} - }, - "@widgetName": {}, - "@sentASticker": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@errorAddingWidget": {}, - "@commandHint_dm": { - "type": "text", - "description": "Usage hint for the command /dm" - }, - "@commandHint_hug": {}, - "@replace": {}, - "@reject": { - "type": "text", - "placeholders": {} - }, - "@editBlockedServers": { - "type": "text", - "placeholders": {} - }, - "@oopsPushError": { - "type": "text", - "placeholders": {} - }, - "@youUnbannedUser": { - "placeholders": { - "user": {} - } - }, - "@deactivateAccountWarning": { - "type": "text", - "placeholders": {} - }, - "@archive": { - "type": "text", - "placeholders": {} - }, - "@joinedTheChat": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@visibleForEveryone": { - "type": "text", - "placeholders": {} - }, - "@pleaseEnter4Digits": { - "type": "text", - "placeholders": {} - }, - "@newSpace": {}, - "@changePassword": { - "type": "text", - "placeholders": {} - }, - "@devices": { - "type": "text", - "placeholders": {} - }, - "@unknownEvent": { - "type": "text", - "placeholders": { - "type": {} - } - }, - "@emojis": {}, - "@pleaseEnterYourPin": { - "type": "text", - "placeholders": {} - }, - "@pleaseChoose": { - "type": "text", - "placeholders": {} - }, - "@share": { - "type": "text", - "placeholders": {} - }, - "@commandHint_googly": {}, - "@pleaseTryAgainLaterOrChooseDifferentServer": {}, - "@createGroup": {}, - "@privacy": { - "type": "text", - "placeholders": {} - }, - "@changeYourAvatar": { - "type": "text", - "placeholders": {} - }, - "@sendImage": { - "type": "text", - "placeholders": {} - }, - "@hydrateTorLong": {}, - "@time": {}, - "@enterYourHomeserver": { - "type": "text", - "placeholders": {} - }, - "@botMessages": { - "type": "text", - "placeholders": {} - }, - "@contentHasBeenReported": { - "type": "text", - "placeholders": {} - }, - "@custom": {}, - "@noBackupWarning": {}, - "@fromJoining": { - "type": "text", - "placeholders": {} - }, - "@verify": { - "type": "text", - "placeholders": {} - }, - "@sendVideo": { - "type": "text", - "placeholders": {} - }, - "@storeInSecureStorageDescription": {}, - "@openChat": {}, - "@kickUserDescription": {}, - "@sendAMessage": { - "type": "text", - "placeholders": {} - }, - "@pin": { - "type": "text", - "placeholders": {} - }, - "@importNow": {}, - "@deleteAccount": { - "type": "text", - "placeholders": {} - }, - "@setInvitationLink": { - "type": "text", - "placeholders": {} - }, - "@pinMessage": {}, - "@muteChat": { - "type": "text", - "placeholders": {} - }, - "@invite": {}, - "@enableMultiAccounts": {}, - "@anyoneCanJoin": { - "type": "text", - "placeholders": {} - }, - "@emotePacks": { - "type": "text", - "placeholders": {} - }, - "@indexedDbErrorTitle": {}, - "@endedTheCall": { - "type": "text", - "placeholders": { - "senderName": {} - } - }, - "@unsupportedAndroidVersionLong": {}, - "@storeSecurlyOnThisDevice": {}, - "@ok": { - "type": "text", - "placeholders": {} - }, - "@sharedTheLocation": { - "type": "text", - "placeholders": { - "username": {} - } - }, - "@unbanFromChat": { - "type": "text", - "placeholders": {} - }, - "@screenSharingDetail": {}, - "@changedTheDisplaynameTo": { - "type": "text", - "placeholders": { - "username": {}, - "displayname": {} - } - }, - "@unreadChats": { - "type": "text", - "placeholders": { - "unreadCount": {} - } - }, - "@placeCall": {}, - "@extremeOffensive": { - "type": "text", - "placeholders": {} + "@@last_modified": "2021-08-14 12:41:09.826673", + "acceptedTheInvitation": "👍 {username} அழைப்பை ஏற்றுக்கொண்டது", + "@acceptedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} } -} \ No newline at end of file + }, + "accept": "ஏற்றுக்கொள்", + "@accept": { + "type": "text", + "placeholders": {} + }, + "about": "பற்றி", + "@about": { + "type": "text", + "placeholders": {} + }, + "sendCanceled": "அனுப்பப்பட்டது ரத்து செய்யப்பட்டது", + "@sendCanceled": {}, + "chatDetails": "அரட்டை விவரங்கள்", + "@chatDetails": { + "type": "text", + "placeholders": {} + }, + "globalChatId": "உலகளாவிய அரட்டை ஐடி", + "@globalChatId": {}, + "accessAndVisibility": "அணுகல் மற்றும் தெரிவுநிலை", + "@accessAndVisibility": {}, + "enterYourHomeserver": "உங்கள் ஓம்சர்வரை உள்ளிடவும்", + "@enterYourHomeserver": { + "type": "text", + "placeholders": {} + }, + "onlineKeyBackupEnabled": "நிகழ்நிலை விசை காப்புப்பிரதி இயக்கப்பட்டது", + "@onlineKeyBackupEnabled": { + "type": "text", + "placeholders": {} + }, + "recoveryKey": "மீட்பு விசை", + "@recoveryKey": {}, + "setStatus": "நிலையை அமைக்கவும்", + "@setStatus": { + "type": "text", + "placeholders": {} + }, + "title": "பஞ்சுபோன்ற", + "@title": { + "description": "Title for the application", + "type": "text", + "placeholders": {} + }, + "verify": "சரிபார்க்கவும்", + "@verify": { + "type": "text", + "placeholders": {} + }, + "verifyStart": "சரிபார்ப்பைத் தொடங்கவும்", + "@verifyStart": { + "type": "text", + "placeholders": {} + }, + "unsupportedAndroidVersion": "ஆதரிக்கப்படாத ஆண்ட்ராய்டு பதிப்பு", + "@unsupportedAndroidVersion": {}, + "invitedBy": "{user} அழைத்தார்", + "@invitedBy": { + "placeholders": { + "user": {} + } + }, + "startConversation": "உரையாடலைத் தொடங்குங்கள்", + "@startConversation": {}, + "commandHint_sendraw": "மூல சாதொபொகு ஐ அனுப்புங்கள்", + "@commandHint_sendraw": {}, + "passwordIsWrong": "நீங்கள் உள்ளிட்ட கடவுச்சொல் தவறு", + "@passwordIsWrong": {}, + "publicLink": "பொது இணைப்பு", + "@publicLink": {}, + "forwardMessageTo": "செய்தியை {roomName}க்கு அனுப்பவா?", + "@forwardMessageTo": { + "type": "text", + "placeholders": { + "roomName": {} + } + }, + "completedKeyVerification": "{sender} நிறைவு செய்யப்பட்ட விசை சரிபார்ப்பு", + "@completedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "incomingMessages": "உள்வரும் செய்திகள்", + "@incomingMessages": {}, + "password": "கடவுச்சொல்", + "@password": { + "type": "text", + "placeholders": {} + }, + "emptyChat": "வெற்று அரட்டை", + "@emptyChat": { + "type": "text", + "placeholders": {} + }, + "encrypted": "குறியாக்கப்பட்டது", + "@encrypted": { + "type": "text", + "placeholders": {} + }, + "reject": "நிராகரிக்கவும்", + "@reject": { + "type": "text", + "placeholders": {} + }, + "importNow": "இப்போது இறக்குமதி செய்யுங்கள்", + "@importNow": {}, + "changedTheChatAvatar": "{username} அரட்டை அவதாரத்தை மாற்றியது", + "@changedTheChatAvatar": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "widgetName": "பெயர்", + "@widgetName": {}, + "chooseAStrongPassword": "வலுவான கடவுச்சொல்லைத் தேர்வுசெய்க", + "@chooseAStrongPassword": { + "type": "text", + "placeholders": {} + }, + "commandHint_me": "உங்களை விவரிக்கவும்", + "@commandHint_me": { + "type": "text", + "description": "Usage hint for the command /me" + }, + "commandHint_unban": "இந்த அறையிலிருந்து கொடுக்கப்பட்ட பயனரைத் தடுக்கிறது", + "@commandHint_unban": { + "type": "text", + "description": "Usage hint for the command /unban" + }, + "countParticipants": "{count} பங்கேற்பாளர்கள்", + "@countParticipants": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "editDisplayname": "காட்சி பெயர் திருத்து", + "@editDisplayname": { + "type": "text", + "placeholders": {} + }, + "fileName": "கோப்பு பெயர்", + "@fileName": { + "type": "text", + "placeholders": {} + }, + "hasWithdrawnTheInvitationFor": "{targetName} க்கான அழைப்பை {username} திரும்பப் பெற்றுள்ளார்", + "@hasWithdrawnTheInvitationFor": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "identity": "முற்றொருமை", + "@identity": { + "type": "text", + "placeholders": {} + }, + "removedBy": "{username} ஆல் அகற்றப்பட்டது", + "@removedBy": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "requestPermission": "இசைவு கோருங்கள்", + "@requestPermission": { + "type": "text", + "placeholders": {} + }, + "user": "பயனர்", + "@user": {}, + "optionalRedactReason": "(விரும்பினால்) இந்த செய்தியை மாற்றியமைப்பதற்கான காரணம் ...", + "@optionalRedactReason": {}, + "device": "சாதனம்", + "@device": { + "type": "text", + "placeholders": {} + }, + "license": "உரிமம்", + "@license": { + "type": "text", + "placeholders": {} + }, + "contactHasBeenInvitedToTheGroup": "குழுவிற்கு தொடர்பு அழைக்கப்பட்டுள்ளது", + "@contactHasBeenInvitedToTheGroup": { + "type": "text", + "placeholders": {} + }, + "passphraseOrKey": "கடவுச்சொல் அல்லது மீட்பு விசை", + "@passphraseOrKey": { + "type": "text", + "placeholders": {} + }, + "widgetNameError": "காட்சி பெயரை வழங்கவும்.", + "@widgetNameError": {}, + "select": "தேர்ந்தெடு", + "@select": {}, + "compareEmojiMatch": "தயவுசெய்து ஈமோசிகளை ஒப்பிடுக", + "@compareEmojiMatch": { + "type": "text", + "placeholders": {} + }, + "dateWithoutYear": "{month}-{day}", + "@dateWithoutYear": { + "type": "text", + "placeholders": { + "month": {}, + "day": {} + } + }, + "fromTheInvitation": "அழைப்பிலிருந்து", + "@fromTheInvitation": { + "type": "text", + "placeholders": {} + }, + "help": "உதவி", + "@help": { + "type": "text", + "placeholders": {} + }, + "invited": "அழைக்கப்பட்டார்", + "@invited": { + "type": "text", + "placeholders": {} + }, + "invitedUsersOnly": "பயனர்களை மட்டுமே அழைத்தது", + "@invitedUsersOnly": { + "type": "text", + "placeholders": {} + }, + "kickedAndBanned": "🙅 {username} உதைத்து {targetName} ஐத் தடை செய்தார்", + "@kickedAndBanned": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "mention": "குறிப்பு", + "@mention": { + "type": "text", + "placeholders": {} + }, + "newVerificationRequest": "புதிய சரிபார்ப்பு கோரிக்கை!", + "@newVerificationRequest": { + "type": "text", + "placeholders": {} + }, + "shareInviteLink": "இணைப்பைப் பகிரவும்", + "@shareInviteLink": {}, + "pickImage": "ஒரு படத்தைத் தேர்ந்தெடுக்கவும்", + "@pickImage": { + "type": "text", + "placeholders": {} + }, + "pin": "முள்", + "@pin": { + "type": "text", + "placeholders": {} + }, + "unavailable": "கிடைக்கவில்லை", + "@unavailable": { + "type": "text", + "placeholders": {} + }, + "voiceCall": "குரல் அழைப்பு", + "@voiceCall": {}, + "youKickedAndBanned": "🙅 நீங்கள் உதைத்து தடைசெய்துள்ளீர்கள் {user}", + "@youKickedAndBanned": { + "placeholders": { + "user": {} + } + }, + "youKicked": "👞 {user}ஐ உதைத்துள்ளீர்கள்", + "@youKicked": { + "placeholders": { + "user": {} + } + }, + "storeInAppleKeyChain": "ஆப்பிள் கீச்சினில் சேமிக்கவும்", + "@storeInAppleKeyChain": {}, + "searchForUsers": "@Users ஐத் தேடுங்கள் ...", + "@searchForUsers": {}, + "pleaseEnterYourCurrentPassword": "உங்கள் தற்போதைய கடவுச்சொல்லை உள்ளிடவும்", + "@pleaseEnterYourCurrentPassword": {}, + "anyoneCanJoin": "யார் வேண்டுமானாலும் சேரலாம்", + "@anyoneCanJoin": { + "type": "text", + "placeholders": {} + }, + "archive": "காப்பகம்", + "@archive": { + "type": "text", + "placeholders": {} + }, + "banFromChat": "அரட்டையிலிருந்து தடை", + "@banFromChat": { + "type": "text", + "placeholders": {} + }, + "cancel": "ரத்துசெய்", + "@cancel": { + "type": "text", + "placeholders": {} + }, + "edit": "தொகு", + "@edit": { + "type": "text", + "placeholders": {} + }, + "iHaveClickedOnLink": "நான் இணைப்பைக் சொடுக்கு செய்துள்ளேன்", + "@iHaveClickedOnLink": { + "type": "text", + "placeholders": {} + }, + "pleaseClickOnLink": "மின்னஞ்சலில் உள்ள இணைப்பைக் சொடுக்கு செய்து தொடரவும்.", + "@pleaseClickOnLink": { + "type": "text", + "placeholders": {} + }, + "changeTheme": "உங்கள் பாணியை மாற்றவும்", + "@changeTheme": { + "type": "text", + "placeholders": {} + }, + "fontSize": "எழுத்துரு அளவு", + "@fontSize": { + "type": "text", + "placeholders": {} + }, + "cantOpenUri": "யூரி {uri} வேலை ஐ திறக்க முடியாது", + "@cantOpenUri": { + "type": "text", + "placeholders": { + "uri": {} + } + }, + "repeatPassword": "கடவுச்சொல்லை மீண்டும் செய்யவும்", + "@repeatPassword": {}, + "youRejectedTheInvitation": "நீங்கள் அழைப்பை நிராகரித்தீர்கள்", + "@youRejectedTheInvitation": {}, + "confirmMatrixId": "உங்கள் கணக்கை நீக்க உங்கள் மேட்ரிக்ச் ஐடியை உறுதிப்படுத்தவும்.", + "@confirmMatrixId": {}, + "supposedMxid": "இது {mxid} be ஆக இருக்க வேண்டும்", + "@supposedMxid": { + "type": "text", + "placeholders": { + "mxid": {} + } + }, + "commandHint_googly": "சில கூகிள் கண்களை அனுப்பவும்", + "@commandHint_googly": {}, + "commandHint_cuddle": "ஒரு கசப்பு அனுப்பவும்", + "@commandHint_cuddle": {}, + "startFirstChat": "உங்கள் முதல் அரட்டையைத் தொடங்கவும்", + "@startFirstChat": {}, + "importEmojis": "ஈமோசிகளை இறக்குமதி செய்யுங்கள்", + "@importEmojis": {}, + "exportEmotePack": "எமோட் பேக் .zip என ஏற்றுமதி செய்யுங்கள்", + "@exportEmotePack": {}, + "replace": "மாற்றவும்", + "@replace": {}, + "tryAgain": "மீண்டும் முயற்சிக்கவும்", + "@tryAgain": {}, + "pushNotificationsNotAvailable": "புச் அறிவிப்புகள் கிடைக்கவில்லை", + "@pushNotificationsNotAvailable": {}, + "blockUsername": "பயனர்பெயரை புறக்கணிக்கவும்", + "@blockUsername": {}, + "start": "தொடங்கு", + "@start": {}, + "chatBackup": "அரட்டை காப்புப்பிரதி", + "@chatBackup": { + "type": "text", + "placeholders": {} + }, + "send": "அனுப்பு", + "@send": { + "type": "text", + "placeholders": {} + }, + "theyDontMatch": "அவர்கள் பொருந்தவில்லை", + "@theyDontMatch": { + "type": "text", + "placeholders": {} + }, + "sendMessages": "செய்திகளை அனுப்பவும்", + "@sendMessages": { + "type": "text", + "placeholders": {} + }, + "fluffychat": "பஞ்சுபோன்ற", + "@fluffychat": { + "type": "text", + "placeholders": {} + }, + "downloadFile": "கோப்பைப் பதிவிறக்கவும்", + "@downloadFile": { + "type": "text", + "placeholders": {} + }, + "createNewSpace": "புதிய இடம்", + "@createNewSpace": { + "type": "text", + "placeholders": {} + }, + "synchronizingPleaseWait": "ஒத்திசைத்தல்… தயவுசெய்து காத்திருங்கள்.", + "@synchronizingPleaseWait": { + "type": "text", + "placeholders": {} + }, + "unknownEncryptionAlgorithm": "அறியப்படாத குறியாக்க வழிமுறை", + "@unknownEncryptionAlgorithm": { + "type": "text", + "placeholders": {} + }, + "unverified": "சரிபார்க்கப்படாதது", + "@unverified": {}, + "unmuteChat": "மாறுதல் அரட்டை", + "@unmuteChat": { + "type": "text", + "placeholders": {} + }, + "unreadChats": "{unreadCount, plural, =1{1 unread chat} other{{unreadCount} unread chats}}", + "@unreadChats": { + "type": "text", + "placeholders": { + "unreadCount": {} + } + }, + "verifyTitle": "பிற கணக்கை சரிபார்க்கிறது", + "@verifyTitle": { + "type": "text", + "placeholders": {} + }, + "videoCall": "வீடியோ அழைப்பு", + "@videoCall": { + "type": "text", + "placeholders": {} + }, + "visibleForAllParticipants": "பங்கேற்பாளர்கள் அனைவருக்கும் தெரியும்", + "@visibleForAllParticipants": { + "type": "text", + "placeholders": {} + }, + "voiceMessage": "குரல் செய்தி", + "@voiceMessage": { + "type": "text", + "placeholders": {} + }, + "reopenChat": "அரட்டையை மீண்டும் திறக்கவும்", + "@reopenChat": {}, + "formattedMessages": "வடிவமைக்கப்பட்ட செய்திகள்", + "@formattedMessages": {}, + "darkTheme": "இருண்ட", + "@darkTheme": { + "type": "text", + "placeholders": {} + }, + "guestsAreForbidden": "விருந்தினர்கள் தடைசெய்யப்பட்டுள்ளனர்", + "@guestsAreForbidden": { + "type": "text", + "placeholders": {} + }, + "guestsCanJoin": "விருந்தினர்கள் சேரலாம்", + "@guestsCanJoin": { + "type": "text", + "placeholders": {} + }, + "widgetUrlError": "இது சரியான முகவரி அல்ல.", + "@widgetUrlError": {}, + "commandHint_invite": "கொடுக்கப்பட்ட பயனரை இந்த அறைக்கு அழைக்கவும்", + "@commandHint_invite": { + "type": "text", + "description": "Usage hint for the command /invite" + }, + "commandHint_op": "கொடுக்கப்பட்ட பயனரின் ஆற்றல் மட்டத்தை அமைக்கவும் (இயல்புநிலை: 50)", + "@commandHint_op": { + "type": "text", + "description": "Usage hint for the command /op" + }, + "commandHint_plain": "வடிவமைக்கப்படாத உரையை அனுப்பவும்", + "@commandHint_plain": { + "type": "text", + "description": "Usage hint for the command /plain" + }, + "commandMissing": "{command} என்பது கட்டளை அல்ல.", + "@commandMissing": { + "type": "text", + "placeholders": { + "command": {} + }, + "description": "State that {command} is not a valid /command." + }, + "inviteForMe": "எனக்கு அழைக்கவும்", + "@inviteForMe": { + "type": "text", + "placeholders": {} + }, + "moderator": "மதிப்பீட்டாளர்", + "@moderator": { + "type": "text", + "placeholders": {} + }, + "shareLocation": "இருப்பிடத்தைப் பகிரவும்", + "@shareLocation": { + "type": "text", + "placeholders": {} + }, + "addEmail": "மின்னஞ்சல் சேர்க்கவும்", + "@addEmail": { + "type": "text", + "placeholders": {} + }, + "all": "அனைத்தும்", + "@all": { + "type": "text", + "placeholders": {} + }, + "lightTheme": "ஒளி", + "@lightTheme": { + "type": "text", + "placeholders": {} + }, + "allChats": "அனைத்து அரட்டைகளும்", + "@allChats": { + "type": "text", + "placeholders": {} + }, + "sendOnEnter": "Enter ஐ அனுப்பவும்", + "@sendOnEnter": {}, + "pleaseEnterRecoveryKey": "உங்கள் மீட்பு விசையை உள்ளிடவும்:", + "@pleaseEnterRecoveryKey": {}, + "dehydrate": "ஏற்றுமதி அமர்வு மற்றும் சாதனத்தை துடைக்கவும்", + "@dehydrate": {}, + "ok": "சரி", + "@ok": { + "type": "text", + "placeholders": {} + }, + "configureChat": "அரட்டையை உள்ளமைக்கவும்", + "@configureChat": { + "type": "text", + "placeholders": {} + }, + "deviceId": "சாதன ஐடி", + "@deviceId": { + "type": "text", + "placeholders": {} + }, + "isTyping": "தட்டச்சு செய்கிறது…", + "@isTyping": { + "type": "text", + "placeholders": {} + }, + "joinedTheChat": "👋 {username} அரட்டையில் சேர்ந்தார்", + "@joinedTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "noConnectionToTheServer": "சேவையகத்துடன் எந்த தொடர்பும் இல்லை", + "@noConnectionToTheServer": { + "type": "text", + "placeholders": {} + }, + "noEmotesFound": "உணர்ச்சிகள் எதுவும் காணப்படவில்லை. .", + "@noEmotesFound": { + "type": "text", + "placeholders": {} + }, + "notifications": "அறிவிப்புகள்", + "@notifications": { + "type": "text", + "placeholders": {} + }, + "oopsPushError": "அச்சச்சோ! துரதிர்ச்டவசமாக, புச் அறிவிப்புகளை அமைக்கும் போது பிழை ஏற்பட்டது.", + "@oopsPushError": { + "type": "text", + "placeholders": {} + }, + "oopsSomethingWentWrong": "அச்சச்சோ, ஏதோ தவறு நடந்தது…", + "@oopsSomethingWentWrong": { + "type": "text", + "placeholders": {} + }, + "bundleName": "மூட்டை பெயர்", + "@bundleName": {}, + "enableMultiAccounts": "(பீட்டா) இந்த சாதனத்தில் பல கணக்குகளை இயக்கவும்", + "@enableMultiAccounts": {}, + "remove": "அகற்று", + "@remove": { + "type": "text", + "placeholders": {} + }, + "recoveryKeyLost": "மீட்பு விசை இழந்ததா?", + "@recoveryKeyLost": {}, + "sendAMessage": "ஒரு செய்தியை அனுப்பவும்", + "@sendAMessage": { + "type": "text", + "placeholders": {} + }, + "spaceIsPublic": "இடம் பொது", + "@spaceIsPublic": { + "type": "text", + "placeholders": {} + }, + "transferFromAnotherDevice": "மற்றொரு சாதனத்திலிருந்து மாற்றவும்", + "@transferFromAnotherDevice": { + "type": "text", + "placeholders": {} + }, + "visibleForEveryone": "அனைவருக்கும் தெரியும்", + "@visibleForEveryone": { + "type": "text", + "placeholders": {} + }, + "weSentYouAnEmail": "நாங்கள் உங்களுக்கு ஒரு மின்னஞ்சல் அனுப்பினோம்", + "@weSentYouAnEmail": { + "type": "text", + "placeholders": {} + }, + "enterRoom": "அறையை உள்ளிடவும்", + "@enterRoom": {}, + "report": "அறிக்கை", + "@report": {}, + "verifyOtherDevice": "Sevice பிற சாதனத்தை சரிபார்க்கவும்", + "@verifyOtherDevice": {}, + "startedACall": "{senderName} அழைப்பைத் தொடங்கினார்", + "@startedACall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "toggleFavorite": "பிடித்ததை மாற்றவும்", + "@toggleFavorite": { + "type": "text", + "placeholders": {} + }, + "userSentUnknownEvent": "{username} ஒரு {type} நிகழ்வை அனுப்பியது", + "@userSentUnknownEvent": { + "type": "text", + "placeholders": { + "username": {}, + "type": {} + } + }, + "verified": "சரிபார்க்கப்பட்டது", + "@verified": { + "type": "text", + "placeholders": {} + }, + "verifySuccess": "நீங்கள் வெற்றிகரமாக சரிபார்த்தீர்கள்!", + "@verifySuccess": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerNumbers": "பங்குதாரர் எண்களை ஏற்றுக்கொள்வதற்காக காத்திருக்கிறார்…", + "@waitingPartnerNumbers": { + "type": "text", + "placeholders": {} + }, + "warning": "எச்சரிக்கை!", + "@warning": { + "type": "text", + "placeholders": {} + }, + "pinMessage": "அறைக்கு முள்", + "@pinMessage": {}, + "addWidget": "விட்செட்டைச் சேர்க்கவும்", + "@addWidget": {}, + "widgetEtherpad": "உரை குறிப்பு", + "@widgetEtherpad": {}, + "widgetCustom": "தனிப்பயன்", + "@widgetCustom": {}, + "unlockOldMessages": "பழைய செய்திகளைத் திறக்கவும்", + "@unlockOldMessages": {}, + "appearOnTop": "மேலே தோன்றும்", + "@appearOnTop": {}, + "serverLimitReached": "சேவையக வரம்பு அடைந்தது! {seconds} விநாடிகள் காத்திருக்கிறது ...", + "@serverLimitReached": { + "type": "integer", + "placeholders": { + "seconds": {} + } + }, + "acceptedKeyVerification": "{sender} ஏற்றுக்கொள்ளப்பட்ட விசை சரிபார்ப்பு", + "@acceptedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "copyToClipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கவும்", + "@copyToClipboard": { + "type": "text", + "placeholders": {} + }, + "createGroup": "குழுவை உருவாக்கவும்", + "@createGroup": {}, + "editBundlesForAccount": "இந்த கணக்கிற்கான மூட்டைகளைத் திருத்தவும்", + "@editBundlesForAccount": {}, + "seenByUser": "{username} ஆல் பார்த்தது", + "@seenByUser": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "unpin": "Unpin", + "@unpin": { + "type": "text", + "placeholders": {} + }, + "emojis": "ஈமோசிகள்", + "@emojis": {}, + "indexedDbErrorTitle": "தனியார் பயன்முறை சிக்கல்கள்", + "@indexedDbErrorTitle": {}, + "jumpToLastReadMessage": "கடைசி வாசிப்பு செய்திக்கு செல்லவும்", + "@jumpToLastReadMessage": {}, + "commandHint_markasgroup": "குழுவாக குறி", + "@commandHint_markasgroup": {}, + "commandHint_html": "உஉகுமொ வடிவமைக்கப்பட்ட உரையை அனுப்பவும்", + "@commandHint_html": { + "type": "text", + "description": "Usage hint for the command /html" + }, + "commandHint_kick": "இந்த அறையிலிருந்து கொடுக்கப்பட்ட பயனரை அகற்றவும்", + "@commandHint_kick": { + "type": "text", + "description": "Usage hint for the command /kick" + }, + "deleteMessage": "செய்தியை நீக்கு", + "@deleteMessage": { + "type": "text", + "placeholders": {} + }, + "messageInfo": "செய்தி செய்தி", + "@messageInfo": {}, + "sentAFile": "📁 {username} கோப்பை அனுப்பியுள்ளார்", + "@sentAFile": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "videoWithSize": "வீடியோ ({size})", + "@videoWithSize": { + "type": "text", + "placeholders": { + "size": {} + } + }, + "readUpToHere": "இங்கே படிக்கவும்", + "@readUpToHere": {}, + "chatDescriptionHasBeenChanged": "அரட்டை விளக்கம் மாற்றப்பட்டது", + "@chatDescriptionHasBeenChanged": {}, + "reportMessage": "செய்தி அறிக்கை", + "@reportMessage": { + "type": "text", + "placeholders": {} + }, + "kickFromChat": "அரட்டையிலிருந்து கிக்", + "@kickFromChat": { + "type": "text", + "placeholders": {} + }, + "widgetVideo": "ஒளிதோற்றம்", + "@widgetVideo": {}, + "redactedAnEvent": "{username} ஒரு நிகழ்வை மறுவடிவமைத்தது", + "@redactedAnEvent": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "opacity": "ஒளிபுகாநிலை:", + "@opacity": {}, + "blur": "மங்கலானது:", + "@blur": {}, + "setWallpaper": "வால்பேப்பரை அமைக்கவும்", + "@setWallpaper": {}, + "statusExampleMessage": "இன்று நீங்கள் எப்படி இருக்கிறீர்கள்?", + "@statusExampleMessage": { + "type": "text", + "placeholders": {} + }, + "toggleUnread": "மார்க் படிக்க/படிக்கவில்லை", + "@toggleUnread": { + "type": "text", + "placeholders": {} + }, + "tooManyRequestsWarning": "பல கோரிக்கைகள். தயவுசெய்து பின்னர் மீண்டும் முயற்சிக்கவும்!", + "@tooManyRequestsWarning": { + "type": "text", + "placeholders": {} + }, + "unblockDevice": "சாதனத்தைத் தடைசெய்க", + "@unblockDevice": { + "type": "text", + "placeholders": {} + }, + "unknownDevice": "தெரியாத சாதனம்", + "@unknownDevice": { + "type": "text", + "placeholders": {} + }, + "addToSpaceDescription": "இந்த அரட்டையைச் சேர்க்க ஒரு இடத்தைத் தேர்ந்தெடுக்கவும்.", + "@addToSpaceDescription": {}, + "errorAddingWidget": "விட்செட்டைச் சேர்ப்பதில் பிழை.", + "@errorAddingWidget": {}, + "youInvitedToBy": "In இணைப்பு வழியாக நீங்கள் அழைக்கப்பட்டுள்ளீர்கள்:\n {alias}", + "@youInvitedToBy": { + "placeholders": { + "alias": {} + } + }, + "storeSecurlyOnThisDevice": "இந்த சாதனத்தில் பாதுகாப்பாக சேமிக்கவும்", + "@storeSecurlyOnThisDevice": {}, + "screenSharingTitle": "திரை பகிர்வு", + "@screenSharingTitle": {}, + "appearOnTopDetails": "பயன்பாடு மேலே தோன்ற அனுமதிக்கிறது (நீங்கள் ஏற்கனவே ஒரு அழைப்பு கணக்காக பஞ்சுபோன்ற அமைப்பைக் கொண்டிருந்தால் தேவையில்லை)", + "@appearOnTopDetails": {}, + "newGroup": "புதிய குழு", + "@newGroup": {}, + "noOtherDevicesFound": "வேறு சாதனங்கள் எதுவும் கிடைக்கவில்லை", + "@noOtherDevicesFound": {}, + "sendRoomNotifications": "ஒரு @ROOM அறிவிப்புகளை அனுப்பவும்", + "@sendRoomNotifications": {}, + "generatingVideoThumbnail": "வீடியோ சிறு உருவத்தை உருவாக்குதல் ...", + "@generatingVideoThumbnail": {}, + "reply": "பதில்", + "@reply": { + "type": "text", + "placeholders": {} + }, + "askSSSSSign": "மற்ற நபரில் கையெழுத்திட, தயவுசெய்து உங்கள் பாதுகாப்பான கடை பாச்ஃபிரேச் அல்லது மீட்பு விசையை உள்ளிடவும்.", + "@askSSSSSign": { + "type": "text", + "placeholders": {} + }, + "areGuestsAllowedToJoin": "விருந்தினர் பயனர்கள் சேர அனுமதிக்கப்படுகிறார்களா", + "@areGuestsAllowedToJoin": { + "type": "text", + "placeholders": {} + }, + "areYouSureYouWantToLogout": "நீங்கள் நிச்சயமாக வெளியேற விரும்புகிறீர்களா?", + "@areYouSureYouWantToLogout": { + "type": "text", + "placeholders": {} + }, + "aboutHomeserver": "{homeserver} பற்றி", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "swipeRightToLeftToReply": "பதிலளிக்க வலமிருந்து இடமாக ச்வைப் செய்யவும்", + "@swipeRightToLeftToReply": {}, + "unread": "படிக்காதது", + "@unread": {}, + "changedTheChatDescriptionTo": "{username} பயனர்பெயர் the அரட்டை விளக்கத்தை மாற்றியது: '{description}'", + "@changedTheChatDescriptionTo": { + "type": "text", + "placeholders": { + "username": {}, + "description": {} + } + }, + "dateAndTimeOfDay": "{date}, {timeOfDay}", + "@dateAndTimeOfDay": { + "type": "text", + "placeholders": { + "date": {}, + "timeOfDay": {} + } + }, + "deleteAccount": "கணக்கை நீக்கு", + "@deleteAccount": { + "type": "text", + "placeholders": {} + }, + "deviceKeys": "சாதன விசைகள்:", + "@deviceKeys": {}, + "noUsersFoundWithQuery": "துரதிர்ச்டவசமாக \"{query}\" உடன் எந்த பயனரையும் காண முடியவில்லை. நீங்கள் ஒரு எழுத்துப்பிழை செய்தீர்களா என்பதை சரிபார்க்கவும்.", + "@noUsersFoundWithQuery": { + "type": "text", + "placeholders": { + "query": {} + } + }, + "jump": "தாவு", + "@jump": {}, + "yourGlobalUserIdIs": "உங்கள் உலகளாவிய பயனர் ஐடி: ", + "@yourGlobalUserIdIs": {}, + "publicSpaces": "பொது இடங்கள்", + "@publicSpaces": {}, + "discover": "கண்டுபிடி", + "@discover": {}, + "commandHint_unignore": "கொடுக்கப்பட்ட மேட்ரிக்ச் ஐடியை இணைக்கவும்", + "@commandHint_unignore": {}, + "prepareSendingAttachment": "அனுப்பும் இணைப்பைத் தயாரிக்கவும் ...", + "@prepareSendingAttachment": {}, + "sendingAttachment": "இணைப்பை அனுப்புகிறது ...", + "@sendingAttachment": {}, + "continueText": "தொடரவும்", + "@continueText": {}, + "welcomeText": "ஏய் ஏய் 👋 இது பஞ்சுபோன்றது. Https://matrix.org உடன் இணக்கமான எந்த ஓம்சர்வரில் நீங்கள் உள்நுழையலாம். பின்னர் யாருடனும் அரட்டையடிக்கவும். இது ஒரு பெரிய பரவலாக்கப்பட்ட செய்தியிடல் நெட்வொர்க்!", + "@welcomeText": {}, + "name": "பெயர்", + "@name": {}, + "username": "பயனர்பெயர்", + "@username": { + "type": "text", + "placeholders": {} + }, + "whoCanPerformWhichAction": "எந்த செயலைச் செய்ய முடியும்", + "@whoCanPerformWhichAction": { + "type": "text", + "placeholders": {} + }, + "whoIsAllowedToJoinThisGroup": "இந்த குழுவில் சேர யார் அனுமதிக்கப்படுகிறார்கள்", + "@whoIsAllowedToJoinThisGroup": { + "type": "text", + "placeholders": {} + }, + "youAreNoLongerParticipatingInThisChat": "இந்த அரட்டையில் நீங்கள் இனி பங்கேற்கவில்லை", + "@youAreNoLongerParticipatingInThisChat": { + "type": "text", + "placeholders": {} + }, + "yourPublicKey": "உங்கள் பொது விசை", + "@yourPublicKey": { + "type": "text", + "placeholders": {} + }, + "time": "நேரம்", + "@time": {}, + "publish": "வெளியிடுங்கள்", + "@publish": {}, + "openChat": "திறந்த அரட்டை", + "@openChat": {}, + "markAsRead": "படித்தபடி குறி", + "@markAsRead": {}, + "reportUser": "பயனர் புகாரளிக்கவும்", + "@reportUser": {}, + "dismiss": "தள்ளுபடி", + "@dismiss": {}, + "reactedWith": "{sender} {reaction} உடன் பதிலளித்தார்", + "@reactedWith": { + "type": "text", + "placeholders": { + "sender": {}, + "reaction": {} + } + }, + "placeCall": "அழைப்பு அழைப்பு", + "@placeCall": {}, + "videoCallsBetaWarning": "வீடியோ அழைப்புகள் தற்போது பீட்டாவில் உள்ளன என்பதை நினைவில் கொள்க. அவர்கள் எதிர்பார்த்தபடி வேலை செய்யக்கூடாது அல்லது எல்லா தளங்களிலும் வேலை செய்யக்கூடாது.", + "@videoCallsBetaWarning": {}, + "experimentalVideoCalls": "சோதனை வீடியோ அழைப்புகள்", + "@experimentalVideoCalls": {}, + "emailOrUsername": "மின்னஞ்சல் அல்லது பயனர்பெயர்", + "@emailOrUsername": {}, + "previousAccount": "முந்தைய கணக்கு", + "@previousAccount": {}, + "noOneCanJoin": "யாரும் சேர முடியாது", + "@noOneCanJoin": {}, + "userWouldLikeToChangeTheChat": "{user} அரட்டையில் சேர விரும்புகிறார்.", + "@userWouldLikeToChangeTheChat": { + "placeholders": { + "user": {} + } + }, + "newSpace": "புதிய இடம்", + "@newSpace": {}, + "enterSpace": "இடத்தை உள்ளிடவும்", + "@enterSpace": {}, + "wasDirectChatDisplayName": "வெற்று அரட்டை ({oldDisplayName})", + "@wasDirectChatDisplayName": { + "type": "text", + "placeholders": { + "oldDisplayName": {} + } + }, + "openLinkInBrowser": "உலாவியில் திறந்த இணைப்பை திறக்கவும்", + "@openLinkInBrowser": {}, + "reportErrorDescription": "😭 ஓ இல்லை. ஏதோ தவறு நடந்தது. நீங்கள் விரும்பினால், இந்த பிழையை டெவலப்பர்களிடம் புகாரளிக்கலாம்.", + "@reportErrorDescription": {}, + "setTheme": "கருப்பொருள் அமைக்கவும்:", + "@setTheme": {}, + "invite": "அழைக்கவும்", + "@invite": {}, + "wrongPinEntered": "தவறான முள் நுழைந்தது! {seconds} விநாடிகளில் மீண்டும் முயற்சிக்கவும் ...", + "@wrongPinEntered": { + "type": "text", + "placeholders": { + "seconds": {} + } + }, + "pleaseEnterANumber": "தயவுசெய்து 0 ஐ விட அதிகமான எண்ணை உள்ளிடவும்", + "@pleaseEnterANumber": {}, + "kickUserDescription": "பயனர் அரட்டையிலிருந்து வெளியேற்றப்படுகிறார், ஆனால் தடை செய்யப்படவில்லை. பொது அரட்டைகளில், பயனர் எந்த நேரத்திலும் மீண்டும் சேரலாம்.", + "@kickUserDescription": {}, + "learnMore": "மேலும் அறிக", + "@learnMore": {}, + "chatCanBeDiscoveredViaSearchOnServer": "{server}", + "@chatCanBeDiscoveredViaSearchOnServer": { + "type": "text", + "placeholders": { + "server": {} + } + }, + "knockRestricted": "நாக் தடை", + "@knockRestricted": {}, + "bannedUser": "{username} தடைசெய்யப்பட்ட {targetName}", + "@bannedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "blockDevice": "தொகுதி சாதனம்", + "@blockDevice": { + "type": "text", + "placeholders": {} + }, + "blocked": "தடுக்கப்பட்டது", + "@blocked": { + "type": "text", + "placeholders": {} + }, + "changeDeviceName": "சாதனத்தின் பெயரை மாற்றவும்", + "@changeDeviceName": { + "type": "text", + "placeholders": {} + }, + "changedTheDisplaynameTo": "{username} அவற்றின் காட்சி பெயர்: '{displayname}'", + "@changedTheDisplaynameTo": { + "type": "text", + "placeholders": { + "username": {}, + "displayname": {} + } + }, + "changedTheGuestAccessRules": "{username} விருந்தினர் அணுகல் விதிகளை மாற்றியது", + "@changedTheGuestAccessRules": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheProfileAvatar": "{username} அவர்களின் அவதாரத்தை மாற்றியது", + "@changedTheProfileAvatar": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheRoomAliases": "{username} அறை மாற்றுப்பெயர்களை மாற்றியது", + "@changedTheRoomAliases": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changeTheNameOfTheGroup": "குழுவின் பெயரை மாற்றவும்", + "@changeTheNameOfTheGroup": { + "type": "text", + "placeholders": {} + }, + "chats": "அரட்டைகள்", + "@chats": { + "type": "text", + "placeholders": {} + }, + "clearArchive": "தெளிவான காப்பகம்", + "@clearArchive": {}, + "close": "மூடு", + "@close": { + "type": "text", + "placeholders": {} + }, + "commandHint_create": "வெற்று குழு அரட்டையை உருவாக்கவும்\n குறியாக்கத்தை முடக்க-இல்லை-குறியாக்கத்தைப் பயன்படுத்தவும்", + "@commandHint_create": { + "type": "text", + "description": "Usage hint for the command /create" + }, + "compareNumbersMatch": "எண்களை ஒப்பிடுக", + "@compareNumbersMatch": { + "type": "text", + "placeholders": {} + }, + "connect": "இணை", + "@connect": { + "type": "text", + "placeholders": {} + }, + "containsUserName": "பயனர்பெயர் உள்ளது", + "@containsUserName": { + "type": "text", + "placeholders": {} + }, + "copiedToClipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது", + "@copiedToClipboard": { + "type": "text", + "placeholders": {} + }, + "copy": "நகலெடு", + "@copy": { + "type": "text", + "placeholders": {} + }, + "couldNotDecryptMessage": "செய்தியை மறைகுறியாக்க முடியவில்லை: {error}", + "@couldNotDecryptMessage": { + "type": "text", + "placeholders": { + "error": {} + } + }, + "create": "உருவாக்கு", + "@create": { + "type": "text", + "placeholders": {} + }, + "chatPermissions": "அரட்டை அனுமதிகள்", + "@chatPermissions": {}, + "editRoomAliases": "அறை மாற்றுப்பெயர்களைத் திருத்து", + "@editRoomAliases": { + "type": "text", + "placeholders": {} + }, + "editRoomAvatar": "அறை அவதார் திருத்து", + "@editRoomAvatar": { + "type": "text", + "placeholders": {} + }, + "forward": "முன்னோக்கி", + "@forward": { + "type": "text", + "placeholders": {} + }, + "fromJoining": "சேருவதிலிருந்து", + "@fromJoining": { + "type": "text", + "placeholders": {} + }, + "groupWith": "{displayname} உடன் குழு", + "@groupWith": { + "type": "text", + "placeholders": { + "displayname": {} + } + }, + "hideRedactedMessages": "சரிசெய்யப்பட்ட செய்திகளை மறைக்கவும்", + "@hideRedactedMessages": {}, + "hideRedactedMessagesBody": "யாராவது ஒரு செய்தியை மாற்றியமைத்தால், இந்த செய்தி இனி அரட்டையில் காணப்படாது.", + "@hideRedactedMessagesBody": {}, + "howOffensiveIsThisContent": "இந்த உள்ளடக்கம் எவ்வளவு ஆபத்தானது?", + "@howOffensiveIsThisContent": { + "type": "text", + "placeholders": {} + }, + "inoffensive": "செயலற்றது", + "@inoffensive": { + "type": "text", + "placeholders": {} + }, + "inviteContact": "தொடர்பை அழைக்கவும்", + "@inviteContact": { + "type": "text", + "placeholders": {} + }, + "kicked": "👞 {username} {targetName} ஐ உதைத்தார்", + "@kicked": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "lastActiveAgo": "கடைசியாக செயலில்: {localizedTimeShort}", + "@lastActiveAgo": { + "type": "text", + "placeholders": { + "localizedTimeShort": {} + } + }, + "leftTheChat": "அரட்டையை விட்டு வெளியேறினார்", + "@leftTheChat": { + "type": "text", + "placeholders": {} + }, + "loadCountMoreParticipants": "ஏற்றவும் {count} மேலும் பங்கேற்பாளர்கள்", + "@loadCountMoreParticipants": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "dehydrateWarning": "இந்த செயலை செயல்தவிர்க்க முடியாது. காப்புப்பிரதி கோப்பை பாதுகாப்பாக சேமித்து வைக்கவும்.", + "@dehydrateWarning": {}, + "dehydrateTor": "டோர் பயனர்கள்: ஏற்றுமதி அமர்வு", + "@dehydrateTor": {}, + "hydrateTor": "டோர் பயனர்கள்: இறக்குமதி அமர்வு ஏற்றுமதி", + "@hydrateTor": {}, + "loadMore": "மேலும் ஏற்றவும்…", + "@loadMore": { + "type": "text", + "placeholders": {} + }, + "logout": "வெளியேற்றம்", + "@logout": { + "type": "text", + "placeholders": {} + }, + "memberChanges": "உறுப்பினர் மாற்றங்கள்", + "@memberChanges": { + "type": "text", + "placeholders": {} + }, + "messagesStyle": "செய்திகள்:", + "@messagesStyle": {}, + "needPantalaimonWarning": "இப்போதைக்கு இறுதி முதல் இறுதி குறியாக்கத்தைப் பயன்படுத்த உங்களுக்கு பாண்டலாயமன் தேவை என்பதை நினைவில் கொள்க.", + "@needPantalaimonWarning": { + "type": "text", + "placeholders": {} + }, + "newMessageInFluffyChat": "Fuf பஞ்சுபோன்ற புதிய செய்தி", + "@newMessageInFluffyChat": { + "type": "text", + "placeholders": {} + }, + "noEncryptionForPublicRooms": "அறை இனி பகிரங்கமாக அணுக முடியாதவுடன் மட்டுமே நீங்கள் குறியாக்கத்தை செயல்படுத்த முடியும்.", + "@noEncryptionForPublicRooms": { + "type": "text", + "placeholders": {} + }, + "noMatrixServer": "{server1} என்பது மேட்ரிக்ஸ் சர்வர் இல்லை, அதற்கு பதிலாக {server2} ஐ பயன்படுத்தவா?", + "@noMatrixServer": { + "type": "text", + "placeholders": { + "server1": {}, + "server2": {} + } + }, + "noPermission": "இசைவு இல்லை", + "@noPermission": { + "type": "text", + "placeholders": {} + }, + "noRoomsFound": "அறைகள் எதுவும் கிடைக்கவில்லை…", + "@noRoomsFound": { + "type": "text", + "placeholders": {} + }, + "notificationsEnabledForThisAccount": "இந்த கணக்கிற்கு அறிவிப்புகள் இயக்கப்பட்டன", + "@notificationsEnabledForThisAccount": { + "type": "text", + "placeholders": {} + }, + "numUsersTyping": "{count} பயனர்கள் தட்டச்சு செய்கிறார்கள்…", + "@numUsersTyping": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "offensive": "தாக்குதல்", + "@offensive": { + "type": "text", + "placeholders": {} + }, + "openCamera": "திறந்த கேமரா", + "@openCamera": { + "type": "text", + "placeholders": {} + }, + "openVideoCamera": "வீடியோவுக்கு கேமரா திறக்கவும்", + "@openVideoCamera": { + "type": "text", + "placeholders": {} + }, + "addAccount": "கணக்கைச் சேர்க்கவும்", + "@addAccount": {}, + "openInMaps": "வரைபடங்களில் திறந்திருக்கும்", + "@openInMaps": { + "type": "text", + "placeholders": {} + }, + "link": "இணைப்பு", + "@link": {}, + "serverRequiresEmail": "இந்த சேவையகம் பதிவுக்கு உங்கள் மின்னஞ்சல் முகவரியை சரிபார்க்க வேண்டும்.", + "@serverRequiresEmail": {}, + "passwordForgotten": "கடவுச்சொல் மறந்துவிட்டது", + "@passwordForgotten": { + "type": "text", + "placeholders": {} + }, + "passwordHasBeenChanged": "கடவுச்சொல் மாற்றப்பட்டுள்ளது", + "@passwordHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "pleaseChooseAPasscode": "பாச் குறியீட்டைத் தேர்வுசெய்க", + "@pleaseChooseAPasscode": { + "type": "text", + "placeholders": {} + }, + "publicRooms": "பொது அறைகள்", + "@publicRooms": { + "type": "text", + "placeholders": {} + }, + "removeAllOtherDevices": "மற்ற எல்லா சாதனங்களையும் அகற்றவும்", + "@removeAllOtherDevices": { + "type": "text", + "placeholders": {} + }, + "roomHasBeenUpgraded": "அறை மேம்படுத்தப்பட்டுள்ளது", + "@roomHasBeenUpgraded": { + "type": "text", + "placeholders": {} + }, + "roomVersion": "அறை பதிப்பு", + "@roomVersion": { + "type": "text", + "placeholders": {} + }, + "saveFile": "கோப்பை சேமி", + "@saveFile": { + "type": "text", + "placeholders": {} + }, + "sentAnAudio": "🎤 {username} ஆடியோவை அனுப்பியுள்ளார்", + "@sentAnAudio": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentAPicture": "🖼️ {username} படத்தை அனுப்பியுள்ளார்", + "@sentAPicture": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentAVideo": "🎥 {username} ஒரு வீடியோவை அனுப்பியுள்ளார்", + "@sentAVideo": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "setPermissionsLevel": "இசைவு அளவை அமைக்கவும்", + "@setPermissionsLevel": { + "type": "text", + "placeholders": {} + }, + "settings": "அமைப்புகள்", + "@settings": { + "type": "text", + "placeholders": {} + }, + "sharedTheLocation": "{username} அவற்றின் இருப்பிடத்தைப் பகிர்ந்து கொண்டது", + "@sharedTheLocation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "showPassword": "கடவுச்சொல்லைக் காட்டு", + "@showPassword": { + "type": "text", + "placeholders": {} + }, + "submit": "சமர்ப்பிக்கவும்", + "@submit": { + "type": "text", + "placeholders": {} + }, + "tryToSendAgain": "மீண்டும் அனுப்ப முயற்சிக்கவும்", + "@tryToSendAgain": { + "type": "text", + "placeholders": {} + }, + "sendTypingNotifications": "தட்டச்சு அறிவிப்புகளை அனுப்பவும்", + "@sendTypingNotifications": {}, + "enterAnEmailAddress": "மின்னஞ்சல் முகவரியை உள்ளிடவும்", + "@enterAnEmailAddress": { + "type": "text", + "placeholders": {} + }, + "blockListDescription": "உங்களை தொந்தரவு செய்யும் பயனர்களைத் தடுக்கலாம். உங்கள் தனிப்பட்ட தொகுதி பட்டியலில் பயனர்களிடமிருந்து எந்த செய்திகளையும் அல்லது அறை அழைப்புகளையும் நீங்கள் பெற முடியாது.", + "@blockListDescription": {}, + "incorrectPassphraseOrKey": "தவறான கடவுச்சொல் அல்லது மீட்பு விசை", + "@incorrectPassphraseOrKey": { + "type": "text", + "placeholders": {} + }, + "loadingPleaseWait": "ஏற்றுகிறது… தயவுசெய்து காத்திருங்கள்.", + "@loadingPleaseWait": { + "type": "text", + "placeholders": {} + }, + "no": "இல்லை", + "@no": { + "type": "text", + "placeholders": {} + }, + "people": "மக்கள்", + "@people": { + "type": "text", + "placeholders": {} + }, + "youHaveBeenBannedFromThisChat": "இந்த அரட்டையிலிருந்து உங்களுக்கு தடை விதிக்கப்பட்டுள்ளது", + "@youHaveBeenBannedFromThisChat": { + "type": "text", + "placeholders": {} + }, + "messageType": "செய்தி வகை", + "@messageType": {}, + "databaseMigrationTitle": "தரவுத்தளம் உகந்ததாக உள்ளது", + "@databaseMigrationTitle": {}, + "usersMustKnock": "பயனர்கள் தட்ட வேண்டும்", + "@usersMustKnock": {}, + "allSpaces": "அனைத்து இடங்களும்", + "@allSpaces": {}, + "importFromZipFile": ".Zip கோப்பிலிருந்து இறக்குமதி செய்யுங்கள்", + "@importFromZipFile": {}, + "activatedEndToEndEncryption": "{username} இறுதி குறியாக்கத்திற்கு செயல்படுத்தப்பட்ட முடிவு", + "@activatedEndToEndEncryption": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "addChatDescription": "அரட்டை விளக்கத்தைச் சேர்க்கவும் ...", + "@addChatDescription": {}, + "addToSpace": "விண்வெளியில் சேர்க்கவும்", + "@addToSpace": {}, + "commandHint_hug": "கட்டிப்பிடிக்கவும்", + "@commandHint_hug": {}, + "cuddleContent": "{senderName} பெயர் you உங்களை கசக்குகிறது", + "@cuddleContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "hugContent": "{senderName} உங்களை அணைத்துக்கொள்கிறது", + "@hugContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "appLock": "பயன்பாட்டு பூட்டு", + "@appLock": { + "type": "text", + "placeholders": {} + }, + "appLockDescription": "முள் குறியீட்டைக் கொண்டு பயன்படுத்தாதபோது பயன்பாட்டைப் பூட்டவும்", + "@appLockDescription": {}, + "googlyEyesContent": "{senderName} உங்களுக்கு கூகிள் கண்களை அனுப்புகிறது", + "@googlyEyesContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "areYouSure": "நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "@areYouSure": { + "type": "text", + "placeholders": {} + }, + "askVerificationRequest": "{username}பயனர்பெயர் இருந்து இலிருந்து இந்த சரிபார்ப்பு கோரிக்கையை ஏற்றுக்கொள்ளவா?", + "@askVerificationRequest": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "autoplayImages": "அனிமேசன் செய்யப்பட்ட ச்டிக்கர்கள் மற்றும் உணர்ச்சிகளை தானாக இயக்கவும்", + "@autoplayImages": { + "type": "text", + "placeholder": {} + }, + "badServerLoginTypesException": "உள்நாட்டு வகைகளை ஓம்சர்வர் ஆதரிக்கிறது:\n {serverVersions}\n ஆனால் இந்த பயன்பாடு மட்டுமே ஆதரிக்கிறது:\n {supportedVersions}", + "@badServerLoginTypesException": { + "type": "text", + "placeholders": { + "serverVersions": {}, + "supportedVersions": {} + } + }, + "badServerVersionsException": "ஓம்சர்வர் ச்பெக் பதிப்புகளை ஆதரிக்கிறது:\n {serverVersions}\n ஆனால் இந்த பயன்பாடு {supportedVersions} மட்டுமே ஆதரிக்கிறது", + "@badServerVersionsException": { + "type": "text", + "placeholders": { + "serverVersions": {}, + "supportedVersions": {} + } + }, + "noChatsFoundHere": "இங்கே அரட்டைகள் எதுவும் காணப்படவில்லை. கீழே உள்ள பொத்தானைப் பயன்படுத்தி ஒருவருடன் புதிய அரட்டையைத் தொடங்கவும். .", + "@noChatsFoundHere": {}, + "space": "இடைவெளி", + "@space": {}, + "banned": "தடைசெய்யப்பட்டது", + "@banned": { + "type": "text", + "placeholders": {} + }, + "botMessages": "போட் செய்திகள்", + "@botMessages": { + "type": "text", + "placeholders": {} + }, + "changedTheChatNameTo": "{username} அரட்டை பெயரை மாற்றியது: '{chatname}'", + "@changedTheChatNameTo": { + "type": "text", + "placeholders": { + "username": {}, + "chatname": {} + } + }, + "changedTheHistoryVisibilityTo": "{username} வரலாற்று தெரிவுநிலையை மாற்றியது: {rules}", + "@changedTheHistoryVisibilityTo": { + "type": "text", + "placeholders": { + "username": {}, + "rules": {} + } + }, + "changedTheJoinRules": "{username} சேர விதிகளை மாற்றியது", + "@changedTheJoinRules": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheJoinRulesTo": "{username} சேர விதிகளை மாற்றியது: {joinRules}", + "@changedTheJoinRulesTo": { + "type": "text", + "placeholders": { + "username": {}, + "joinRules": {} + } + }, + "changedTheRoomInvitationLink": "{username} அழைப்பிதழ் இணைப்பை மாற்றியது", + "@changedTheRoomInvitationLink": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changePassword": "கடவுச்சொல்லை மாற்றவும்", + "@changePassword": { + "type": "text", + "placeholders": {} + }, + "changeTheHomeserver": "ஓம்சர்வரை மாற்றவும்", + "@changeTheHomeserver": { + "type": "text", + "placeholders": {} + }, + "changeYourAvatar": "உங்கள் அவதாரத்தை மாற்றவும்", + "@changeYourAvatar": { + "type": "text", + "placeholders": {} + }, + "channelCorruptedDecryptError": "குறியாக்கம் சிதைந்துள்ளது", + "@channelCorruptedDecryptError": { + "type": "text", + "placeholders": {} + }, + "chat": "அரட்டை", + "@chat": { + "type": "text", + "placeholders": {} + }, + "yourChatBackupHasBeenSetUp": "உங்கள் அரட்டை காப்புப்பிரதி அமைக்கப்பட்டுள்ளது.", + "@yourChatBackupHasBeenSetUp": {}, + "chatBackupDescription": "உங்கள் பழைய செய்திகள் மீட்பு விசையுடன் பாதுகாக்கப்படுகின்றன. நீங்கள் அதை இழக்கவில்லை என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்.", + "@chatBackupDescription": { + "type": "text", + "placeholders": {} + }, + "chatHasBeenAddedToThisSpace": "இந்த இடத்தில் அரட்டை சேர்க்கப்பட்டுள்ளது", + "@chatHasBeenAddedToThisSpace": {}, + "commandHint_markasdm": "கொடுக்கப்பட்ட மேட்ரிக்சிற்கான நேரடி செய்தி அறையாக குறிக்கவும்", + "@commandHint_markasdm": {}, + "commandHint_ban": "கொடுக்கப்பட்ட பயனரை இந்த அறையிலிருந்து தடை செய்யுங்கள்", + "@commandHint_ban": { + "type": "text", + "description": "Usage hint for the command /ban" + }, + "commandHint_clearcache": "தெளிவான தற்காலிக சேமிப்பு", + "@commandHint_clearcache": { + "type": "text", + "description": "Usage hint for the command /clearcache" + }, + "commandHint_discardsession": "அமர்வை நிராகரிக்கவும்", + "@commandHint_discardsession": { + "type": "text", + "description": "Usage hint for the command /discardsession" + }, + "commandHint_myroomavatar": "இந்த அறைக்கு உங்கள் படத்தை அமைக்கவும் (MXC-URI எழுதியது)", + "@commandHint_myroomavatar": { + "type": "text", + "description": "Usage hint for the command /myroomavatar" + }, + "commandHint_myroomnick": "இந்த அறைக்கு உங்கள் காட்சி பெயரை அமைக்கவும்", + "@commandHint_myroomnick": { + "type": "text", + "description": "Usage hint for the command /myroomnick" + }, + "commandInvalid": "கட்டளை தவறானது", + "@commandInvalid": { + "type": "text" + }, + "confirm": "உறுதிப்படுத்தவும்", + "@confirm": { + "type": "text", + "placeholders": {} + }, + "contentHasBeenReported": "உள்ளடக்கம் சேவையக நிர்வாகிகளுக்கு தெரிவிக்கப்பட்டுள்ளது", + "@contentHasBeenReported": { + "type": "text", + "placeholders": {} + }, + "createdTheChat": "💬 {username} அரட்டையை உருவாக்கினார்", + "@createdTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "currentlyActive": "தற்போது செயலில் உள்ளது", + "@currentlyActive": { + "type": "text", + "placeholders": {} + }, + "dateWithYear": "{year}-{month}-{day}", + "@dateWithYear": { + "type": "text", + "placeholders": { + "year": {}, + "month": {}, + "day": {} + } + }, + "deactivateAccountWarning": "இது உங்கள் பயனர் கணக்கை செயலிழக்கச் செய்யும். இதை செயல்தவிர்க்க முடியாது! நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "@deactivateAccountWarning": { + "type": "text", + "placeholders": {} + }, + "defaultPermissionLevel": "புதிய பயனர்களுக்கான இயல்புநிலை இசைவு நிலை", + "@defaultPermissionLevel": { + "type": "text", + "placeholders": {} + }, + "delete": "நீக்கு", + "@delete": { + "type": "text", + "placeholders": {} + }, + "devices": "சாதனங்கள்", + "@devices": { + "type": "text", + "placeholders": {} + }, + "directChats": "நேரடி அரட்டைகள்", + "@directChats": { + "type": "text", + "placeholders": {} + }, + "allRooms": "அனைத்து குழு அரட்டைகளும்", + "@allRooms": { + "type": "text", + "placeholders": {} + }, + "displaynameHasBeenChanged": "காட்சி பெயர் மாற்றப்பட்டுள்ளது", + "@displaynameHasBeenChanged": { + "type": "text", + "placeholders": {} + }, + "editBlockedServers": "தடுக்கப்பட்ட சேவையகங்களைத் திருத்து", + "@editBlockedServers": { + "type": "text", + "placeholders": {} + }, + "emoteExists": "எமோட் ஏற்கனவே உள்ளது!", + "@emoteExists": { + "type": "text", + "placeholders": {} + }, + "emoteInvalid": "தவறான எமோட் சார்ட்கோட்!", + "@emoteInvalid": { + "type": "text", + "placeholders": {} + }, + "emoteKeyboardNoRecents": "அண்மைக் காலத்தில் பயன்படுத்தப்பட்ட உணர்ச்சிகள் இங்கே தோன்றும் ...", + "@emoteKeyboardNoRecents": { + "type": "text", + "placeholders": {} + }, + "emotePacks": "அறைக்கு எமோட் பொதிகள்", + "@emotePacks": { + "type": "text", + "placeholders": {} + }, + "emoteSettings": "எமோட் அமைப்புகள்", + "@emoteSettings": { + "type": "text", + "placeholders": {} + }, + "accessAndVisibilityDescription": "இந்த அரட்டையில் யார் சேர அனுமதிக்கப்படுகிறார்கள், அரட்டையை எவ்வாறு கண்டுபிடிப்பது.", + "@accessAndVisibilityDescription": {}, + "calls": "அழைப்புகள்", + "@calls": {}, + "customEmojisAndStickers": "தனிப்பயன் ஈமோசிகள் மற்றும் ச்டிக்கர்கள்", + "@customEmojisAndStickers": {}, + "customEmojisAndStickersBody": "எந்தவொரு அரட்டையிலும் பயன்படுத்தக்கூடிய தனிப்பயன் ஈமோசிகள் அல்லது ச்டிக்கர்களைச் சேர்க்கவும் அல்லது பகிரவும்.", + "@customEmojisAndStickersBody": {}, + "emoteShortcode": "சார்ட்கோட் எமோட்", + "@emoteShortcode": { + "type": "text", + "placeholders": {} + }, + "emoteWarnNeedToPick": "நீங்கள் ஒரு எமோட் சார்ட்கோட் மற்றும் ஒரு படத்தை எடுக்க வேண்டும்!", + "@emoteWarnNeedToPick": { + "type": "text", + "placeholders": {} + }, + "enableEmotesGlobally": "உலகளவில் எமோட் பேக்கை இயக்கவும்", + "@enableEmotesGlobally": { + "type": "text", + "placeholders": {} + }, + "enableEncryption": "குறியாக்கத்தை இயக்கவும்", + "@enableEncryption": { + "type": "text", + "placeholders": {} + }, + "enableEncryptionWarning": "நீங்கள் இனி குறியாக்கத்தை முடக்க முடியாது. நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "@enableEncryptionWarning": { + "type": "text", + "placeholders": {} + }, + "encryptionNotEnabled": "குறியாக்கம் இயக்கப்படவில்லை", + "@encryptionNotEnabled": { + "type": "text", + "placeholders": {} + }, + "encryption": "குறியாக்கம்", + "@encryption": { + "type": "text", + "placeholders": {} + }, + "endedTheCall": "{senderName} அழைப்பை முடித்தார்", + "@endedTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "homeserver": "ஓம்சர்வர்", + "@homeserver": {}, + "errorObtainingLocation": "இருப்பிடத்தைப் பெறுவதில் பிழை: {error}", + "@errorObtainingLocation": { + "type": "text", + "placeholders": { + "error": {} + } + }, + "everythingReady": "எல்லாம் தயாராக!", + "@everythingReady": { + "type": "text", + "placeholders": {} + }, + "extremeOffensive": "மிகவும் தாக்குதல்", + "@extremeOffensive": { + "type": "text", + "placeholders": {} + }, + "goToTheNewRoom": "புதிய அறைக்குச் செல்லுங்கள்", + "@goToTheNewRoom": { + "type": "text", + "placeholders": {} + }, + "group": "குழு", + "@group": { + "type": "text", + "placeholders": {} + }, + "chatDescription": "அரட்டை விளக்கம்", + "@chatDescription": {}, + "groupIsPublic": "குழு பொது", + "@groupIsPublic": { + "type": "text", + "placeholders": {} + }, + "hideRedactedEvents": "திருத்தப்பட்ட நிகழ்வுகளை மறைக்கவும்", + "@hideRedactedEvents": { + "type": "text", + "placeholders": {} + }, + "hideInvalidOrUnknownMessageFormats": "தவறான அல்லது அறியப்படாத செய்தி வடிவங்களை மறைக்கவும்", + "@hideInvalidOrUnknownMessageFormats": {}, + "id": "ஐடி", + "@id": { + "type": "text", + "placeholders": {} + }, + "block": "தொகுதி", + "@block": {}, + "inviteContactToGroupQuestion": "\"{groupName}\" அரட்டைக்கு {contact} ஐ அழைக்க விரும்புகிறீர்களா?", + "@inviteContactToGroupQuestion": {}, + "inviteContactToGroup": "{groupName} க்கு தொடர்பை அழை", + "@inviteContactToGroup": { + "type": "text", + "placeholders": { + "groupName": {} + } + }, + "noChatDescriptionYet": "அரட்டை விளக்கம் இதுவரை உருவாக்கப்படவில்லை.", + "@noChatDescriptionYet": {}, + "invalidServerName": "தவறான சேவையக பெயர்", + "@invalidServerName": {}, + "redactMessageDescription": "இந்த உரையாடலில் பங்கேற்பாளர்கள் அனைவருக்கும் செய்தி திருத்தப்படும். இதை செயல்தவிர்க்க முடியாது.", + "@redactMessageDescription": {}, + "invitedUser": "📩 {username} {targetName} அழைக்கப்பட்டார்", + "@invitedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "inviteText": "{username} உங்களை பஞ்சுபோன்றதாக அழைத்தது.\n 1. FulufyChat.im ஐப் பார்வையிட்டு பயன்பாட்டை நிறுவவும்\n 2. பதிவு செய்யுங்கள் அல்லது உள்நுழைக\n 3. அழைப்பிதழ் இணைப்பைத் திறக்கவும்:\n {link}", + "@inviteText": { + "type": "text", + "placeholders": { + "username": {}, + "link": {} + } + }, + "joinRoom": "அறையில் சேரவும்", + "@joinRoom": { + "type": "text", + "placeholders": {} + }, + "leave": "விடுப்பு", + "@leave": { + "type": "text", + "placeholders": {} + }, + "dehydrateTorLong": "TOR பயனர்களுக்கு, சாளரத்தை மூடுவதற்கு முன் அமர்வை ஏற்றுமதி செய்ய பரிந்துரைக்கப்படுகிறது.", + "@dehydrateTorLong": {}, + "hydrateTorLong": "உங்கள் அமர்வை கடைசியாக டோரில் ஏற்றுமதி செய்தீர்களா? விரைவாக அதை இறக்குமதி செய்து அரட்டையடிக்கவும்.", + "@hydrateTorLong": {}, + "hydrate": "காப்பு கோப்பிலிருந்து மீட்டமைக்கவும்", + "@hydrate": {}, + "locationDisabledNotice": "இருப்பிட சேவைகள் முடக்கப்பட்டுள்ளன. தயவுசெய்து உங்கள் இருப்பிடத்தைப் பகிர்ந்து கொள்ள அவர்களுக்கு உதவவும்.", + "@locationDisabledNotice": { + "type": "text", + "placeholders": {} + }, + "locationPermissionDeniedNotice": "இருப்பிட இசைவு மறுக்கப்பட்டது. உங்கள் இருப்பிடத்தைப் பகிர்ந்து கொள்ள தயவுசெய்து அவர்களுக்கு வழங்குங்கள்.", + "@locationPermissionDeniedNotice": { + "type": "text", + "placeholders": {} + }, + "login": "புகுபதிவு", + "@login": { + "type": "text", + "placeholders": {} + }, + "logInTo": "{homeserver} இல் உள்நுழைக", + "@logInTo": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "messages": "செய்திகள்", + "@messages": { + "type": "text", + "placeholders": {} + }, + "muteChat": "முடக்கு அரட்டை", + "@muteChat": { + "type": "text", + "placeholders": {} + }, + "newChat": "புதிய அரட்டை", + "@newChat": { + "type": "text", + "placeholders": {} + }, + "next": "அடுத்தது", + "@next": { + "type": "text", + "placeholders": {} + }, + "none": "எதுவுமில்லை", + "@none": { + "type": "text", + "placeholders": {} + }, + "noPasswordRecoveryDescription": "உங்கள் கடவுச்சொல்லை மீட்டெடுப்பதற்கான வழியை நீங்கள் இன்னும் சேர்க்கவில்லை.", + "@noPasswordRecoveryDescription": { + "type": "text", + "placeholders": {} + }, + "openAppToReadMessages": "செய்திகளைப் படிக்க பயன்பாட்டைத் திறக்கவும்", + "@openAppToReadMessages": { + "type": "text", + "placeholders": {} + }, + "oneClientLoggedOut": "உங்கள் வாடிக்கையாளர்களில் ஒருவர் உள்நுழைந்துள்ளார்", + "@oneClientLoggedOut": {}, + "addToBundle": "மூட்டையில் சேர்க்கவும்", + "@addToBundle": {}, + "or": "அல்லது", + "@or": { + "type": "text", + "placeholders": {} + }, + "hideMemberChangesInPublicChats": "பொது அரட்டைகளில் உறுப்பினர் மாற்றங்களை மறைக்கவும்", + "@hideMemberChangesInPublicChats": {}, + "hideMemberChangesInPublicChatsBody": "வாசிப்புத்திறனை மேம்படுத்த யாராவது ஒரு பொது அரட்டையில் சேர்ந்தால் அல்லது விட்டுவிட்டால் அரட்டை காலவரிசையில் காட்ட வேண்டாம்.", + "@hideMemberChangesInPublicChatsBody": {}, + "overview": "கண்ணோட்டம்", + "@overview": {}, + "notifyMeFor": "எனக்கு அறிவிக்கவும்", + "@notifyMeFor": {}, + "passwordRecoverySettings": "கடவுச்சொல் மீட்பு அமைப்புகள்", + "@passwordRecoverySettings": {}, + "passwordRecovery": "கடவுச்சொல் மீட்பு", + "@passwordRecovery": { + "type": "text", + "placeholders": {} + }, + "pleaseChoose": "தயவுசெய்து தேர்வு செய்யவும்", + "@pleaseChoose": { + "type": "text", + "placeholders": {} + }, + "play": "Play {fileName}", + "@play": { + "type": "text", + "placeholders": { + "fileName": {} + } + }, + "pleaseEnter4Digits": "பயன்பாட்டு பூட்டை முடக்க 4 இலக்கங்களை உள்ளிடவும் அல்லது காலியாக விடவும்.", + "@pleaseEnter4Digits": { + "type": "text", + "placeholders": {} + }, + "pleaseEnterYourPassword": "உங்கள் கடவுச்சொல்லை உள்ளிடவும்", + "@pleaseEnterYourPassword": { + "type": "text", + "placeholders": {} + }, + "pleaseEnterYourPin": "உங்கள் முள் உள்ளிடவும்", + "@pleaseEnterYourPin": { + "type": "text", + "placeholders": {} + }, + "pleaseEnterYourUsername": "உங்கள் பயனர்பெயரை உள்ளிடவும்", + "@pleaseEnterYourUsername": { + "type": "text", + "placeholders": {} + }, + "pleaseFollowInstructionsOnWeb": "வலைத்தளத்தின் வழிமுறைகளைப் பின்பற்றி அடுத்து தட்டவும்.", + "@pleaseFollowInstructionsOnWeb": { + "type": "text", + "placeholders": {} + }, + "privacy": "தனியுரிமை", + "@privacy": { + "type": "text", + "placeholders": {} + }, + "reason": "காரணம்", + "@reason": { + "type": "text", + "placeholders": {} + }, + "redactedByBecause": "{username} ஆல் திருத்தப்பட்டது ஏனெனில்: \"{reason}\"", + "@redactedByBecause": { + "type": "text", + "placeholders": { + "username": {}, + "reason": {} + } + }, + "register": "பதிவு செய்யுங்கள்", + "@register": { + "type": "text", + "placeholders": {} + }, + "rejectedTheInvitation": "{username} அழைப்பை நிராகரித்தது", + "@rejectedTheInvitation": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "rejoin": "மீண்டும் சேரவும்", + "@rejoin": { + "type": "text", + "placeholders": {} + }, + "unbanFromChat": "அரட்டையிலிருந்து தடையின்றி", + "@unbanFromChat": { + "type": "text", + "placeholders": {} + }, + "removeYourAvatar": "உங்கள் அவதாரத்தை அகற்று", + "@removeYourAvatar": { + "type": "text", + "placeholders": {} + }, + "replaceRoomWithNewerVersion": "புதிய பதிப்போடு அறையை மாற்றவும்", + "@replaceRoomWithNewerVersion": { + "type": "text", + "placeholders": {} + }, + "sendAsText": "உரையாக அனுப்பவும்", + "@sendAsText": { + "type": "text" + }, + "sendAudio": "ஆடியோ அனுப்பவும்", + "@sendAudio": { + "type": "text", + "placeholders": {} + }, + "sendImage": "படத்தை அனுப்பு", + "@sendImage": { + "type": "text", + "placeholders": {} + }, + "sendImages": "{count} படத்தை அனுப்பு", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "sendOriginal": "அசல் அனுப்பு", + "@sendOriginal": { + "type": "text", + "placeholders": {} + }, + "sendSticker": "ச்டிக்கரை அனுப்பவும்", + "@sendSticker": { + "type": "text", + "placeholders": {} + }, + "sendVideo": "வீடியோ அனுப்பவும்", + "@sendVideo": { + "type": "text", + "placeholders": {} + }, + "sentASticker": "😊 {username} ஒரு ச்டிக்கரை அனுப்பியது", + "@sentASticker": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "sentCallInformations": "{senderName} அனுப்பப்பட்ட அழைப்பு செய்தி", + "@sentCallInformations": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "separateChatTypes": "நேரடி அரட்டைகள் மற்றும் குழுக்களை பிரிக்கவும்", + "@separateChatTypes": { + "type": "text", + "placeholders": {} + }, + "setAsCanonicalAlias": "முதன்மையான மாற்றுப்பெயராக அமைக்கவும்", + "@setAsCanonicalAlias": { + "type": "text", + "placeholders": {} + }, + "setCustomEmotes": "தனிப்பயன் உணர்ச்சிகளை அமைக்கவும்", + "@setCustomEmotes": { + "type": "text", + "placeholders": {} + }, + "setChatDescription": "அரட்டை விளக்கத்தை அமைக்கவும்", + "@setChatDescription": {}, + "setInvitationLink": "அழைப்பிதழ் இணைப்பை அமைக்கவும்", + "@setInvitationLink": { + "type": "text", + "placeholders": {} + }, + "share": "பங்கு", + "@share": { + "type": "text", + "placeholders": {} + }, + "presenceStyle": "இருப்பு:", + "@presenceStyle": { + "type": "text", + "placeholders": {} + }, + "presencesToggle": "பிற பயனர்களிடமிருந்து நிலை செய்திகளைக் காட்டு", + "@presencesToggle": { + "type": "text", + "placeholders": {} + }, + "singlesignon": "ஒற்றை அடையாளம்", + "@singlesignon": { + "type": "text", + "placeholders": {} + }, + "skip": "தவிர்", + "@skip": { + "type": "text", + "placeholders": {} + }, + "sourceCode": "மூலக் குறியீடு", + "@sourceCode": { + "type": "text", + "placeholders": {} + }, + "spaceName": "விண்வெளி பெயர்", + "@spaceName": { + "type": "text", + "placeholders": {} + }, + "status": "நிலை", + "@status": { + "type": "text", + "placeholders": {} + }, + "systemTheme": "மண்டலம்", + "@systemTheme": { + "type": "text", + "placeholders": {} + }, + "theyMatch": "அவர்கள் பொருந்துகிறார்கள்", + "@theyMatch": { + "type": "text", + "placeholders": {} + }, + "unbannedUser": "{username} தடைசெய்யப்படாத {targetName}", + "@unbannedUser": { + "type": "text", + "placeholders": { + "username": {}, + "targetName": {} + } + }, + "unknownEvent": "அறியப்படாத நிகழ்வு '{type}'", + "@unknownEvent": { + "type": "text", + "placeholders": { + "type": {} + } + }, + "userAndOthersAreTyping": "{username} மற்றும் {count} மற்றவர்கள் தட்டச்சு செய்கிறார்கள்…", + "@userAndOthersAreTyping": { + "type": "text", + "placeholders": { + "username": {}, + "count": {} + } + }, + "userIsTyping": "{username} தட்டச்சு செய்கிறது…", + "@userIsTyping": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "waitingPartnerAcceptRequest": "கூட்டாளர் கோரிக்கையை ஏற்றுக்கொள்வதற்காக காத்திருக்கிறார்…", + "@waitingPartnerAcceptRequest": { + "type": "text", + "placeholders": {} + }, + "waitingPartnerEmoji": "பங்குதாரர் ஈமோசியை ஏற்றுக்கொள்வதற்காக காத்திருக்கிறார்…", + "@waitingPartnerEmoji": { + "type": "text", + "placeholders": {} + }, + "sender": "அனுப்புநர்", + "@sender": {}, + "openGallery": "திறந்த கேலரி", + "@openGallery": {}, + "wallpaper": "வால்பேப்பர்:", + "@wallpaper": { + "type": "text", + "placeholders": {} + }, + "whyDoYouWantToReportThis": "இதை ஏன் புகாரளிக்க விரும்புகிறீர்கள்?", + "@whyDoYouWantToReportThis": { + "type": "text", + "placeholders": {} + }, + "wipeChatBackup": "புதிய மீட்பு விசையை உருவாக்க உங்கள் அரட்டை காப்புப்பிரதியைத் துடைக்கவா?", + "@wipeChatBackup": { + "type": "text", + "placeholders": {} + }, + "withTheseAddressesRecoveryDescription": "இந்த முகவரிகள் மூலம் உங்கள் கடவுச்சொல்லை மீட்டெடுக்கலாம்.", + "@withTheseAddressesRecoveryDescription": { + "type": "text", + "placeholders": {} + }, + "writeAMessage": "ஒரு செய்தியை எழுதுங்கள்…", + "@writeAMessage": { + "type": "text", + "placeholders": {} + }, + "yes": "ஆம்", + "@yes": { + "type": "text", + "placeholders": {} + }, + "removeFromSpace": "இடத்திலிருந்து அகற்று", + "@removeFromSpace": {}, + "pleaseEnterRecoveryKeyDescription": "உங்கள் பழைய செய்திகளைத் திறக்க, முந்தைய அமர்வில் உருவாக்கப்பட்ட உங்கள் மீட்பு விசையை உள்ளிடவும். உங்கள் மீட்பு விசை உங்கள் கடவுச்சொல் அல்ல.", + "@pleaseEnterRecoveryKeyDescription": {}, + "confirmEventUnpin": "நிகழ்வை நிரந்தரமாக அவிழ்ப்பது உறுதி?", + "@confirmEventUnpin": {}, + "switchToAccount": "கணக்குக்கு மாறவும் {number}", + "@switchToAccount": { + "type": "number", + "placeholders": { + "number": {} + } + }, + "nextAccount": "அடுத்த கணக்கு", + "@nextAccount": {}, + "youJoinedTheChat": "நீங்கள் அரட்டையில் சேர்ந்தீர்கள்", + "@youJoinedTheChat": {}, + "indexedDbErrorLong": "செய்தி சேமிப்பு துரதிர்ச்டவசமாக இயல்புநிலையாக தனிப்பட்ட பயன்முறையில் இயக்கப்படவில்லை.\n தயவுசெய்து பார்வையிடவும்\n - பற்றி: கட்டமைப்பு\n - கணம் dom.indexedDB.privateBrowsing.enabled பெறுநர் true\n இல்லையெனில், பஞ்சுபோன்றவை இயக்க முடியாது.", + "@indexedDbErrorLong": {}, + "youAcceptedTheInvitation": "👍 நீங்கள் அழைப்பை ஏற்றுக்கொண்டீர்கள்", + "@youAcceptedTheInvitation": {}, + "youBannedUser": "நீங்கள் {user} தடை செய்தீர்கள்", + "@youBannedUser": { + "placeholders": { + "user": {} + } + }, + "youHaveWithdrawnTheInvitationFor": "{user}க்கான அழைப்பை திரும்பப் பெற்றுவிட்டீர்கள்", + "@youHaveWithdrawnTheInvitationFor": { + "placeholders": { + "user": {} + } + }, + "youInvitedBy": "📩 நீங்கள் {user} ஆல் அழைக்கப்பட்டுள்ளீர்கள்", + "@youInvitedBy": { + "placeholders": { + "user": {} + } + }, + "youInvitedUser": "📩 {user} ஐ அழைத்தீர்கள்", + "@youInvitedUser": { + "placeholders": { + "user": {} + } + }, + "youUnbannedUser": "நீங்கள் {user} தடைசெய்யவில்லை", + "@youUnbannedUser": { + "placeholders": { + "user": {} + } + }, + "hasKnocked": "🚪 {user} தட்டியது", + "@hasKnocked": { + "placeholders": { + "user": {} + } + }, + "noPublicLinkHasBeenCreatedYet": "பொது இணைப்பு இதுவரை உருவாக்கப்படவில்லை", + "@noPublicLinkHasBeenCreatedYet": {}, + "knock": "தட்டவும்", + "@knock": {}, + "users": "பயனர்கள்", + "@users": {}, + "storeInSecureStorageDescription": "மீட்பு விசையை இந்த சாதனத்தின் பாதுகாப்பான சேமிப்பகத்தில் சேமிக்கவும்.", + "@storeInSecureStorageDescription": {}, + "saveKeyManuallyDescription": "கணினி பகிர்வு உரையாடல் அல்லது கிளிப்போர்டைத் தூண்டுவதன் மூலம் இந்த விசையை கைமுறையாக சேமிக்கவும்.", + "@saveKeyManuallyDescription": {}, + "storeInAndroidKeystore": "ஆண்ட்ராய்டு கீச்டோரில் சேமிக்கவும்", + "@storeInAndroidKeystore": {}, + "countFiles": "{count} கோப்புகள்", + "@countFiles": { + "placeholders": { + "count": {} + } + }, + "custom": "தனிப்பயன்", + "@custom": {}, + "foregroundServiceRunning": "முன்புற பணி இயங்கும்போது இந்த அறிவிப்பு தோன்றும்.", + "@foregroundServiceRunning": {}, + "screenSharingDetail": "உங்கள் திரையை FUFFYCHAT இல் பகிர்கிறீர்கள்", + "@screenSharingDetail": {}, + "callingPermissions": "அழைப்பு அனுமதிகள்", + "@callingPermissions": {}, + "callingAccount": "அழைப்பு கணக்கு", + "@callingAccount": {}, + "callingAccountDetails": "சொந்த ஆண்ட்ராய்டு டயலர் பயன்பாட்டைப் பயன்படுத்த பஞ்சுபோன்றது அனுமதிக்கிறது.", + "@callingAccountDetails": {}, + "otherCallingPermissions": "மைக்ரோஃபோன், கேமரா மற்றும் பிற பஞ்சுபோன்ற அனுமதிகள்", + "@otherCallingPermissions": {}, + "numChats": "{number} அரட்டைகள்", + "@numChats": { + "type": "number", + "placeholders": { + "number": {} + } + }, + "hideUnimportantStateEvents": "முக்கியமற்ற மாநில நிகழ்வுகளை மறைக்கவும்", + "@hideUnimportantStateEvents": {}, + "whyIsThisMessageEncrypted": "இந்த செய்தி ஏன் படிக்க முடியாதது?", + "@whyIsThisMessageEncrypted": {}, + "noKeyForThisMessage": "இந்த சாதனத்தில் உங்கள் கணக்கில் கையொப்பமிடுவதற்கு முன்பு செய்தி அனுப்பப்பட்டால் இது நிகழலாம்.\n\n அனுப்புநர் உங்கள் சாதனத்தைத் தடுத்துள்ளார் அல்லது இணைய இணைப்பில் ஏதேனும் தவறு ஏற்பட்டுள்ளது.\n\n மற்றொரு அமர்வில் செய்தியைப் படிக்க முடியுமா? அதிலிருந்து செய்தியை மாற்றலாம்! அமைப்புகள்> சாதனங்களுக்குச் சென்று, உங்கள் சாதனங்கள் ஒருவருக்கொருவர் சரிபார்த்துள்ளன என்பதை உறுதிப்படுத்தவும். அடுத்த முறை நீங்கள் அறையைத் திறக்கும்போது, இரண்டு அமர்வுகளும் முன்னணியில் இருக்கும்போது, விசைகள் தானாகவே அனுப்பப்படும்.\n\n வெளியேறும்போது அல்லது சாதனங்களை மாற்றும்போது விசைகளை இழக்க நீங்கள் விரும்பவில்லையா? அமைப்புகளில் அரட்டை காப்புப்பிரதியை நீங்கள் இயக்கியுள்ளீர்கள் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்.", + "@noKeyForThisMessage": {}, + "hidePresences": "நிலை பட்டியலை மறைக்கவா?", + "@hidePresences": {}, + "doNotShowAgain": "மீண்டும் காட்ட வேண்டாம்", + "@doNotShowAgain": {}, + "newSpaceDescription": "உங்கள் அரட்டைகளை ஒருங்கிணைத்து தனியார் அல்லது பொது சமூகங்களை உருவாக்க இடைவெளிகள் உங்களை அனுமதிக்கிறது.", + "@newSpaceDescription": {}, + "disableEncryptionWarning": "பாதுகாப்பு காரணங்களுக்காக நீங்கள் ஒரு அரட்டையில் குறியாக்கத்தை முடக்க முடியாது, அது இதற்கு முன்பு இயக்கப்பட்டிருக்கிறது.", + "@disableEncryptionWarning": {}, + "sorryThatsNotPossible": "மன்னிக்கவும் ... அது சாத்தியமில்லை", + "@sorryThatsNotPossible": {}, + "noBackupWarning": "எச்சரிக்கை! அரட்டை காப்புப்பிரதியை இயக்காமல், உங்கள் மறைகுறியாக்கப்பட்ட செய்திகளுக்கான அணுகலை இழப்பீர்கள். வெளியேறுவதற்கு முன் முதலில் அரட்டை காப்புப்பிரதியை இயக்க மிகவும் பரிந்துரைக்கப்படுகிறது.", + "@noBackupWarning": {}, + "fileIsTooBigForServer": "அனுப்ப முடியவில்லை! சேவையகம் {max} வரை இணைப்புகளை மட்டுமே ஆதரிக்கிறது.", + "@fileIsTooBigForServer": { + "type": "text", + "placeholders": { + "max": {} + } + }, + "fileHasBeenSavedAt": "கோப்பு {path}", + "@fileHasBeenSavedAt": { + "type": "text", + "placeholders": { + "path": {} + } + }, + "pleaseTryAgainLaterOrChooseDifferentServer": "தயவுசெய்து பின்னர் மீண்டும் முயற்சிக்கவும் அல்லது வேறு சேவையகத்தைத் தேர்வுசெய்க.", + "@pleaseTryAgainLaterOrChooseDifferentServer": {}, + "signInWith": "{provider} மூலம் உள்நுழையவும்", + "@signInWith": { + "type": "text", + "placeholders": { + "provider": {} + } + }, + "profileNotFound": "பயனரை சேவையகத்தில் காண முடியவில்லை. ஒரு இணைப்பு சிக்கல் இருக்கலாம் அல்லது பயனர் இல்லை.", + "@profileNotFound": {}, + "inviteGroupChat": "Compect குழு அரட்டையை அழைக்கவும்", + "@inviteGroupChat": {}, + "invitePrivateChat": "தனிப்பட்ட தனியார் அரட்டையை அழைக்கவும்", + "@invitePrivateChat": {}, + "invalidInput": "தவறான உள்ளீடு!", + "@invalidInput": {}, + "archiveRoomDescription": "அரட்டை காப்பகத்திற்கு நகர்த்தப்படும். மற்ற பயனர்கள் நீங்கள் அரட்டையை விட்டுவிட்டீர்கள் என்பதைக் காண முடியும்.", + "@archiveRoomDescription": {}, + "removeDevicesDescription": "நீங்கள் இந்த சாதனத்திலிருந்து வெளியேறுவீர்கள், இனி செய்திகளைப் பெற முடியாது.", + "@removeDevicesDescription": {}, + "banUserDescription": "பயனர் அரட்டையிலிருந்து தடைசெய்யப்படுவார், மேலும் அவை தடைசெய்யப்படாத வரை மீண்டும் அரட்டையில் நுழைய முடியாது.", + "@banUserDescription": {}, + "makeAdminDescription": "இந்த பயனர் நிர்வாகியை நீங்கள் செய்தவுடன், இதை நீங்கள் செயல்தவிர்க்க முடியாமல் போகலாம், ஏனெனில் அவை உங்களைப் போன்ற அதே அனுமதிகளைக் கொண்டிருக்கும்.", + "@makeAdminDescription": {}, + "knocking": "தட்டுதல்", + "@knocking": {}, + "searchChatsRooms": "#Chats, Us பயனர்களைத் தேடுங்கள் ...", + "@searchChatsRooms": {}, + "nothingFound": "எதுவும் கிடைக்கவில்லை ...", + "@nothingFound": {}, + "groupName": "குழு பெயர்", + "@groupName": {}, + "createGroupAndInviteUsers": "ஒரு குழுவை உருவாக்கி பயனர்களை அழைக்கவும்", + "@createGroupAndInviteUsers": {}, + "groupCanBeFoundViaSearch": "தேடல் வழியாக குழுவை காணலாம்", + "@groupCanBeFoundViaSearch": {}, + "wrongRecoveryKey": "மன்னிக்கவும் ... இது சரியான மீட்பு விசையாகத் தெரியவில்லை.", + "@wrongRecoveryKey": {}, + "databaseMigrationBody": "தயவுசெய்து காத்திருங்கள். இது ஒரு கணம் ஆகலாம்.", + "@databaseMigrationBody": {}, + "newPassword": "புதிய கடவுச்சொல்", + "@newPassword": {}, + "pleaseChooseAStrongPassword": "வலுவான கடவுச்சொல்லைத் தேர்வுசெய்க", + "@pleaseChooseAStrongPassword": {}, + "passwordsDoNotMatch": "கடவுச்சொற்கள் பொருந்தவில்லை", + "@passwordsDoNotMatch": {}, + "joinSpace": "விண்வெளியில் சேரவும்", + "@joinSpace": {}, + "addChatOrSubSpace": "அரட்டை அல்லது துணை இடத்தைச் சேர்க்கவும்", + "@addChatOrSubSpace": {}, + "initAppError": "பயன்பாட்டைத் தொடங்கும்போது பிழை ஏற்பட்டது", + "@initAppError": {}, + "databaseBuildErrorBody": "SQlite தரவுத்தளத்தை உருவாக்க முடியவில்லை. ஆப்ஸ் தற்போதைக்கு மரபு தரவுத்தளத்தைப் பயன்படுத்த முயற்சிக்கிறது. {url} இல் டெவலப்பர்களிடம் இந்தப் பிழையைப் புகாரளிக்கவும். பிழை செய்தி: {error}", + "@databaseBuildErrorBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "sessionLostBody": "உங்கள் அமர்வு தொலைந்துவிட்டது. {url} இல் டெவலப்பர்களிடம் இந்தப் பிழையைப் புகாரளிக்கவும். பிழை செய்தி: {error}", + "@sessionLostBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "sendTypingNotificationsDescription": "அரட்டையில் பங்கேற்பாளர்கள் நீங்கள் ஒரு புதிய செய்தியைத் தட்டச்சு செய்யும் போது காணலாம்.", + "@sendTypingNotificationsDescription": {}, + "sendReadReceiptsDescription": "அரட்டையில் பங்கேற்பாளர்கள் நீங்கள் ஒரு செய்தியைப் படிக்கும்போது பார்க்கலாம்.", + "@sendReadReceiptsDescription": {}, + "formattedMessagesDescription": "மார்க் டவுனைப் பயன்படுத்தி தைரியமான உரை போன்ற பணக்கார செய்தி உள்ளடக்கத்தைக் காண்பி.", + "@formattedMessagesDescription": {}, + "verifyOtherUser": "Poser மற்ற பயனரை சரிபார்க்கவும்", + "@verifyOtherUser": {}, + "verifyOtherUserDescription": "நீங்கள் மற்றொரு பயனரைச் சரிபார்த்தால், நீங்கள் உண்மையில் யாருக்கு எழுதுகிறீர்கள் என்பது உங்களுக்குத் தெரியும் என்பதை நீங்கள் உறுதியாக நம்பலாம். .\n\n நீங்கள் ஒரு சரிபார்ப்பைத் தொடங்கும்போது, நீங்களும் மற்ற பயனரும் பயன்பாட்டில் ஒரு பாப்அப்பைக் காண்பீர்கள். நீங்கள் ஒருவருக்கொருவர் ஒப்பிட வேண்டிய தொடர்ச்சியான ஈமோசிகள் அல்லது எண்களைக் காண்பீர்கள்.\n\n இதைச் செய்வதற்கான சிறந்த வழி வீடியோ அழைப்பைச் சந்திப்பது அல்லது தொடங்குவது. .", + "@verifyOtherUserDescription": {}, + "verifyOtherDeviceDescription": "நீங்கள் மற்றொரு சாதனத்தை சரிபார்க்கும்போது, அந்த சாதனங்கள் விசைகளை பரிமாறிக்கொள்ளலாம், உங்கள் ஒட்டுமொத்த பாதுகாப்பை அதிகரிக்கும். So நீங்கள் ஒரு சரிபார்ப்பைத் தொடங்கும்போது, இரண்டு சாதனங்களிலும் பயன்பாட்டில் ஒரு பாப்அப் தோன்றும். நீங்கள் ஒருவருக்கொருவர் ஒப்பிட வேண்டிய தொடர்ச்சியான ஈமோசிகள் அல்லது எண்களைக் காண்பீர்கள். நீங்கள் சரிபார்ப்பைத் தொடங்குவதற்கு முன்பு இரண்டு சாதனங்களையும் எளிதில் வைத்திருப்பது நல்லது. .", + "@verifyOtherDeviceDescription": {}, + "canceledKeyVerification": "{sender} ரத்து செய்யப்பட்ட விசை சரிபார்ப்பு", + "@canceledKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "isReadyForKeyVerification": "விசை சரிபார்ப்பிற்கு {sender} தயாராக உள்ளார்", + "@isReadyForKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "requestedKeyVerification": "{sender} கோரப்பட்ட விசை சரிபார்ப்பு", + "@requestedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "transparent": "வெளிப்படையானது", + "@transparent": {}, + "stickers": "ச்டிக்கர்கள்", + "@stickers": {}, + "commandHint_ignore": "கொடுக்கப்பட்ட மேட்ரிக்ச் ஐடியை புறக்கணிக்கவும்", + "@commandHint_ignore": {}, + "unreadChatsInApp": "{appname}: {unread} படிக்காத அரட்டைகள்", + "@unreadChatsInApp": { + "type": "text", + "placeholders": { + "appname": {}, + "unread": {} + } + }, + "thereAreCountUsersBlocked": "இப்போது {count} பயனர்கள் தடுக்கப்பட்டுள்ளனர்.", + "@thereAreCountUsersBlocked": { + "type": "text", + "count": {} + }, + "restricted": "தடைசெய்யப்பட்டது", + "@restricted": {}, + "moderatorLevel": "{level} - மதிப்பீட்டாளர்", + "@moderatorLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "adminLevel": "{level} - நிர்வாகி", + "@adminLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "changeGeneralChatSettings": "பொது அரட்டை அமைப்புகளை மாற்றவும்", + "@changeGeneralChatSettings": {}, + "inviteOtherUsers": "இந்த அரட்டைக்கு மற்ற பயனர்களை அழைக்கவும்", + "@inviteOtherUsers": {}, + "changeTheChatPermissions": "அரட்டை அனுமதிகளை மாற்றவும்", + "@changeTheChatPermissions": {}, + "changeTheVisibilityOfChatHistory": "அரட்டை வரலாற்றின் தெரிவுநிலையை மாற்றவும்", + "@changeTheVisibilityOfChatHistory": {}, + "changeTheCanonicalRoomAlias": "முக்கிய பொது அரட்டை முகவரியை மாற்றவும்", + "@changeTheCanonicalRoomAlias": {}, + "changeTheDescriptionOfTheGroup": "அரட்டையின் விளக்கத்தை மாற்றவும்", + "@changeTheDescriptionOfTheGroup": {}, + "chatPermissionsDescription": "இந்த அரட்டையில் சில செயல்களுக்கு எந்த ஆற்றல் நிலை தேவை என்பதை வரையறுக்கவும். 0, 50 மற்றும் 100 ஆற்றல் நிலைகள் பொதுவாக பயனர்கள், மதிப்பீட்டாளர்கள் மற்றும் நிர்வாகிகளைக் குறிக்கின்றன, ஆனால் எந்த தரமும் சாத்தியமாகும்.", + "@chatPermissionsDescription": {}, + "updateInstalled": "🎉 புதுப்பிப்பு {version} நிறுவப்பட்டது!", + "@updateInstalled": { + "type": "text", + "placeholders": { + "version": {} + } + }, + "changelog": "மாற்றபதிவு", + "@changelog": {}, + "homeserverDescription": "உங்கள் எல்லா தரவுகளும் ஒரு மின்னஞ்சல் வழங்குநரைப் போலவே ஓம்சர்வரில் சேமிக்கப்படுகின்றன. நீங்கள் எந்த ஓம்சர்வரை பயன்படுத்த விரும்புகிறீர்கள் என்பதை நீங்கள் தேர்வு செய்யலாம், அதே நேரத்தில் நீங்கள் எல்லோரிடமும் தொடர்பு கொள்ளலாம். Https://matrix.org இல் மேலும் அறிக.", + "@homeserverDescription": {}, + "calculatingFileSize": "கோப்பு அளவைக் கணக்கிடுகிறது ...", + "@calculatingFileSize": {}, + "compressVideo": "அமைக்கும் வீடியோ ...", + "@compressVideo": {}, + "sendingAttachmentCountOfCount": "{length} இன் இணைப்பு {index}ஐ அனுப்புகிறது...", + "@sendingAttachmentCountOfCount": { + "type": "integer", + "placeholders": { + "index": {}, + "length": {} + } + }, + "oneOfYourDevicesIsNotVerified": "உங்கள் சாதனங்களில் ஒன்று சரிபார்க்கப்படவில்லை", + "@oneOfYourDevicesIsNotVerified": {}, + "noticeChatBackupDeviceVerification": "குறிப்பு: உங்கள் எல்லா சாதனங்களையும் அரட்டை காப்புப்பிரதியுடன் இணைக்கும்போது, அவை தானாகவே சரிபார்க்கப்படும்.", + "@noticeChatBackupDeviceVerification": {}, + "manageAccount": "கணக்கை நிர்வகிக்கவும்", + "@manageAccount": {}, + "noContactInformationProvided": "சேவையகம் எந்த சரியான தொடர்பு தகவலையும் வழங்காது", + "@noContactInformationProvided": {}, + "contactServerAdmin": "சேவையக நிர்வாகி தொடர்பு", + "@contactServerAdmin": {}, + "contactServerSecurity": "சேவையக பாதுகாப்பைத் தொடர்பு கொள்ளுங்கள்", + "@contactServerSecurity": {}, + "supportPage": "உதவி பக்கம்", + "@supportPage": {}, + "serverInformation": "சேவையக தகவல்:", + "@serverInformation": {}, + "version": "பதிப்பு", + "@version": {}, + "website": "வலைத்தளம்", + "@website": {}, + "compress": "சுருக்க", + "@compress": {}, + "alwaysUse24HourFormat": "தவறு", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "admin": "நிர்வாகி", + "@admin": { + "type": "text", + "placeholders": {} + }, + "alias": "மாற்றுப்பெயர்", + "@alias": { + "type": "text", + "placeholders": {} + }, + "answeredTheCall": "{senderName} அழைப்புக்கு பதிலளித்தார்", + "@answeredTheCall": { + "type": "text", + "placeholders": { + "senderName": {} + } + }, + "commandHint_react": "ஒரு எதிர்வினையாக பதிலை அனுப்பவும்", + "@commandHint_react": { + "type": "text", + "description": "Usage hint for the command /react" + }, + "commandHint_send": "உரையை அனுப்பவும்", + "@commandHint_send": { + "type": "text", + "description": "Usage hint for the command /send" + }, + "containsDisplayName": "காட்சி பெயரைக் கொண்டுள்ளது", + "@containsDisplayName": { + "type": "text", + "placeholders": {} + }, + "removeFromBundle": "இந்த மூட்டையிலிருந்து அகற்றவும்", + "@removeFromBundle": {}, + "pushRules": "தள்ளி விதிகள்", + "@pushRules": { + "type": "text", + "placeholders": {} + }, + "recording": "பதிவு", + "@recording": { + "type": "text", + "placeholders": {} + }, + "redactedBy": "{username} ஆல் திருத்தப்பட்டது", + "@redactedBy": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "directChat": "நேரடி அரட்டை", + "@directChat": {}, + "redactMessage": "திருத்தும் செய்தி", + "@redactMessage": { + "type": "text", + "placeholders": {} + }, + "userAndUserAreTyping": "{username} மற்றும் {username2} தட்டச்சு செய்கின்றன…", + "@userAndUserAreTyping": { + "type": "text", + "placeholders": { + "username": {}, + "username2": {} + } + }, + "userLeftTheChat": "🚪 {username} அரட்டையை விட்டு வெளியேறினார்", + "@userLeftTheChat": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "visibilityOfTheChatHistory": "அரட்டை வரலாற்றின் தெரிவுநிலை", + "@visibilityOfTheChatHistory": { + "type": "text", + "placeholders": {} + }, + "you": "நீங்கள்", + "@you": { + "type": "text", + "placeholders": {} + }, + "unsupportedAndroidVersionLong": "இந்த அம்சத்திற்கு புதிய ஆண்ட்ராய்டு பதிப்பு தேவைப்படுகிறது. புதுப்பிப்புகள் அல்லது பரம்பரை OS ஆதரவை சரிபார்க்கவும்.", + "@unsupportedAndroidVersionLong": {}, + "widgetJitsi": "சிட்சி சந்திக்கிறார்", + "@widgetJitsi": {}, + "signInWithPassword": "கடவுச்சொல்லுடன் உள்நுழைக", + "@signInWithPassword": {}, + "setColorTheme": "வண்ண கருப்பொருள் அமைக்கவும்:", + "@setColorTheme": {}, + "roomUpgradeDescription": "அரட்டை பின்னர் புதிய அறை பதிப்பில் மீண்டும் உருவாக்கப்படும். பங்கேற்பாளர்கள் அனைவருக்கும் புதிய அரட்டைக்கு மாற வேண்டும் என்று அறிவிக்கப்படும். அறை பதிப்புகள் பற்றி மேலும் அறிய https://spec.matrix.org/latest/rooms/", + "@roomUpgradeDescription": {}, + "account": "கணக்கு", + "@account": { + "type": "text", + "placeholders": {} + }, + "groups": "குழுக்கள்", + "@groups": { + "type": "text", + "placeholders": {} + }, + "blockedUsers": "தடுக்கப்பட்ட பயனர்கள்", + "@blockedUsers": {}, + "leaveEmptyToClearStatus": "உங்கள் நிலையை அழிக்க காலியாக விடவும்.", + "@leaveEmptyToClearStatus": {}, + "subspace": "துணை", + "@subspace": {}, + "decline": "வீழ்ச்சி", + "@decline": {}, + "thisDevice": "இந்த சாதனம்:", + "@thisDevice": {}, + "minimumPowerLevel": "{level} என்பது குறைந்தபட்ச ஆற்றல் நிலை.", + "@minimumPowerLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "gallery": "கேலரி", + "@gallery": {}, + "files": "கோப்புகள்", + "@files": {}, + "noDatabaseEncryption": "இந்த மேடையில் தரவுத்தள குறியாக்கம் ஆதரிக்கப்படவில்லை", + "@noDatabaseEncryption": {}, + "goToSpace": "விண்வெளிக்குச் செல்லுங்கள்: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "markAsUnread": "படிக்காத எனக் குறிக்கவும்", + "@markAsUnread": {}, + "userLevel": "{level} - பயனர்", + "@userLevel": { + "type": "text", + "placeholders": { + "level": {} + } + }, + "notAnImage": "படக் கோப்பு அல்ல.", + "@notAnImage": {}, + "encryptThisChat": "இந்த அரட்டையை குறியாக்கவும்", + "@encryptThisChat": {}, + "userRole": "பயனர் பங்கு", + "@userRole": {}, + "publicChatAddresses": "பொது அரட்டை முகவரிகள்", + "@publicChatAddresses": {}, + "createNewAddress": "புதிய முகவரியை உருவாக்கவும்", + "@createNewAddress": {}, + "boldText": "தைரியமான உரை", + "@boldText": {}, + "italicText": "சாய்வு உரை", + "@italicText": {}, + "strikeThrough": "ச்ட்ரைகெத்ரோ", + "@strikeThrough": {}, + "pleaseFillOut": "தயவுசெய்து நிரப்பவும்", + "@pleaseFillOut": {}, + "invalidUrl": "தவறான முகவரி", + "@invalidUrl": {}, + "addLink": "இணைப்பைச் சேர்க்கவும்", + "@addLink": {}, + "searchIn": "அரட்டையில் தேடு \"{chat}\" ...", + "@searchIn": { + "type": "text", + "placeholders": { + "chat": {} + } + }, + "searchMore": "மேலும் தேடுங்கள் ...", + "@searchMore": {}, + "startedKeyVerification": "{sender} விசை சரிபார்ப்பைத் தொடங்கினார்", + "@startedKeyVerification": { + "type": "text", + "placeholders": { + "sender": {} + } + }, + "loginWithMatrixId": "மேட்ரிக்ச்-ஐடியுடன் உள்நுழைக", + "@loginWithMatrixId": {}, + "discoverHomeservers": "ஓம்சர்சர்களைக் கண்டறியவும்", + "@discoverHomeservers": {}, + "whatIsAHomeserver": "ஓம்சர்வர் என்றால் என்ன?", + "@whatIsAHomeserver": {}, + "doesNotSeemToBeAValidHomeserver": "இணக்கமான ஓம்சர்வர் என்று தெரியவில்லை. தவறான URL?", + "@doesNotSeemToBeAValidHomeserver": {}, + "countChatsAndCountParticipants": "{chats} அரட்டைகள் மற்றும் {participants} பங்கேற்பாளர்கள்", + "@countChatsAndCountParticipants": { + "type": "text", + "placeholders": { + "chats": {}, + "participants": {} + } + }, + "noMoreChatsFound": "இனி அரட்டைகள் கிடைக்கவில்லை ...", + "@noMoreChatsFound": {}, + "joinedChats": "இணைந்த அரட்டைகள்", + "@joinedChats": {}, + "spaces": "இடங்கள்", + "@spaces": {}, + "changedTheChatPermissions": "{username} அரட்டை அனுமதிகளை மாற்றியுள்ளார்", + "@changedTheChatPermissions": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "changedTheGuestAccessRulesTo": "{username} விருந்தினர் அணுகல் விதிகளை மாற்றியது: {rules}", + "@changedTheGuestAccessRulesTo": { + "type": "text", + "placeholders": { + "username": {}, + "rules": {} + } + }, + "changedTheHistoryVisibility": "{username} வரலாற்று தெரிவுநிலையை மாற்றியது", + "@changedTheHistoryVisibility": { + "type": "text", + "placeholders": { + "username": {} + } + }, + "commandHint_join": "கொடுக்கப்பட்ட அறையில் சேரவும்", + "@commandHint_join": { + "type": "text", + "description": "Usage hint for the command /join" + }, + "commandHint_dm": "நேரடி அரட்டையைத் தொடங்கவும்\n குறியாக்கத்தை முடக்க-இல்லை-குறியாக்கத்தைப் பயன்படுத்தவும்", + "@commandHint_dm": { + "type": "text", + "description": "Usage hint for the command /dm" + }, + "commandHint_leave": "இந்த அறையை விட்டு விடுங்கள்", + "@commandHint_leave": { + "type": "text", + "description": "Usage hint for the command /leave" + }, + "toggleMuted": "முடக்கியது", + "@toggleMuted": { + "type": "text", + "placeholders": {} + }, + "unbanUserDescription": "அவர்கள் முயற்சித்தால் பயனர் மீண்டும் அரட்டையை உள்ளிட முடியும்.", + "@unbanUserDescription": {}, + "restoreSessionBody": "ஆப்ஸ் இப்போது உங்கள் அமர்வை காப்புப்பிரதியிலிருந்து மீட்டெடுக்க முயற்சிக்கிறது. {url} இல் டெவலப்பர்களிடம் இந்தப் பிழையைப் புகாரளிக்கவும். பிழை செய்தி: {error}", + "@restoreSessionBody": { + "type": "text", + "placeholders": { + "url": {}, + "error": {} + } + }, + "sendReadReceipts": "வாசிப்பு ரசீதுகளை அனுப்பவும்", + "@sendReadReceipts": {}, + "unableToJoinChat": "அரட்டையில் சேர முடியவில்லை. ஒருவேளை மற்ற கட்சி ஏற்கனவே உரையாடலை மூடியிருக்கலாம்.", + "@unableToJoinChat": {}, + "noGoogleServicesWarning": "ஃபயர்பேச் முகில் செய்தி உங்கள் சாதனத்தில் கிடைக்கவில்லை. இன்னும் புச் அறிவிப்புகளைப் பெற, NTFY ஐ நிறுவ பரிந்துரைக்கிறோம். NTFY அல்லது மற்றொரு ஒருங்கிணைந்த புச் வழங்குநருடன் நீங்கள் தரவு பாதுகாப்பான வழியில் புச் அறிவிப்புகளைப் பெறலாம். நீங்கள் பிளேச்டோரிலிருந்து அல்லது எஃப்-டிராய்டிலிருந்து NTFY ஐ பதிவிறக்கம் செய்யலாம்.", + "@noGoogleServicesWarning": { + "type": "text", + "placeholders": {} + }, + "scanQrCode": "QR குறியீட்டை ச்கேன் செய்யுங்கள்", + "@scanQrCode": {}, + "obtainingLocation": "இருப்பிடத்தைப் பெறுதல்…", + "@obtainingLocation": { + "type": "text", + "placeholders": {} + }, + "offline": "இணையமில்லாமல்", + "@offline": { + "type": "text", + "placeholders": {} + }, + "online": "ஆன்லைனில்", + "@online": { + "type": "text", + "placeholders": {} + }, + "participant": "பங்கேற்பாளர்", + "@participant": { + "type": "text", + "placeholders": {} + }, + "removeDevice": "சாதனத்தை அகற்று", + "@removeDevice": { + "type": "text", + "placeholders": {} + }, + "search": "தேடல்", + "@search": { + "type": "text", + "placeholders": {} + }, + "security": "பாதுகாப்பு", + "@security": { + "type": "text", + "placeholders": {} + }, + "sendFile": "கோப்பு அனுப்பவும்", + "@sendFile": { + "type": "text", + "placeholders": {} + } +} diff --git a/assets/l10n/intl_tr.arb b/assets/l10n/intl_tr.arb index ccafdf7a5..48d185027 100644 --- a/assets/l10n/intl_tr.arb +++ b/assets/l10n/intl_tr.arb @@ -2832,5 +2832,63 @@ "oneOfYourDevicesIsNotVerified": "Aygıtlarınızdan biri doğrulanmadı", "@oneOfYourDevicesIsNotVerified": {}, "noticeChatBackupDeviceVerification": "Not: Tüm aygıtlarınızı sohbet yedeklemesine bağladığınızda, otomatik olarak doğrulanırlar.", - "@noticeChatBackupDeviceVerification": {} + "@noticeChatBackupDeviceVerification": {}, + "blur": "Blur:", + "@blur": {}, + "opacity": "Şeffaflık:", + "@opacity": {}, + "setWallpaper": "Duvar kağıdı seç", + "@setWallpaper": {}, + "manageAccount": "Hesabı yönet", + "@manageAccount": {}, + "noContactInformationProvided": "Sunucu geçerli bir iletişim bilgisi sunmadı", + "@noContactInformationProvided": {}, + "contactServerAdmin": "Sunucu yöneticisiyle iletişime geçin", + "@contactServerAdmin": {}, + "contactServerSecurity": "Sunucu güvenliğiyle iletişime geçin", + "@contactServerSecurity": {}, + "supportPage": "Destek sayfası", + "@supportPage": {}, + "name": "İsim", + "@name": {}, + "version": "Versiyon", + "@version": {}, + "serverInformation": "Sunucu bilgisi:", + "@serverInformation": {}, + "website": "Web sitesi", + "@website": {}, + "compress": "Sıkıştırma", + "@compress": {}, + "boldText": "Kalın metin", + "@boldText": {}, + "italicText": "İtalik metin", + "@italicText": {}, + "strikeThrough": "Üstü çizili", + "@strikeThrough": {}, + "pleaseFillOut": "Lütfen doldurun", + "@pleaseFillOut": {}, + "aboutHomeserver": "{homeserver} Hakkında", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "invalidUrl": "Geçersiz url", + "@invalidUrl": {}, + "addLink": "Link ekle", + "@addLink": {}, + "unableToJoinChat": "Sohbete girilemiyor. Belki başka birileri konuşmayı kapatmış olabilir.", + "@unableToJoinChat": {}, + "continueText": "Devam et", + "@continueText": {}, + "sendImages": "{count} görsel gönder", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "welcomeText": "Hey Hey 👋 Karşınızda FluffyChat. https://matrix.org ile uyumlu herhangi bir homeserver'a giriş yapabilirsiniz. Ve herkesle konuşabilirsiniz. Bu koca bir merkeziyetsiz mesajlaşma ağı!", + "@welcomeText": {} } diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index 3bc8dcda4..23720d263 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -2885,5 +2885,14 @@ "addLink": "Додати посилання", "@addLink": {}, "unableToJoinChat": "Неможливо приєднатися до чату. Можливо, інша сторона вже закрила розмову.", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "Надіслати {count} зображення", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "Стиснути", + "@compress": {} } diff --git a/assets/l10n/intl_vi.arb b/assets/l10n/intl_vi.arb index 0dc49651a..354370226 100644 --- a/assets/l10n/intl_vi.arb +++ b/assets/l10n/intl_vi.arb @@ -629,5 +629,25 @@ "stickers": "Nhãn dán", "@stickers": {}, "roomUpgradeDescription": "Cuộc trò chuyện sẽ được tạo lại với phiên bản phòng mới. Tất cả những người tham gia sẽ được thông báo rằng họ cần chuyển sang cuộc trò chuyện mới. Bạn có thể tìm hiểu thêm về các phiên bản phòng tại https://spec.matrix.org/latest/rooms/", - "@roomUpgradeDescription": {} + "@roomUpgradeDescription": {}, + "commandHint_hug": "Gửi một cái ôm", + "@commandHint_hug": {}, + "aboutHomeserver": "Về {homeserver}", + "@aboutHomeserver": { + "type": "text", + "placeholders": { + "homeserver": {} + } + }, + "alwaysUse24HourFormat": "Không", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + }, + "hugContent": "{senderName} ôm bạn", + "@hugContent": { + "type": "text", + "placeholders": { + "senderName": {} + } + } } diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index c91a6d666..0e93c1fba 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -2885,5 +2885,14 @@ "invalidUrl": "无效 url", "@invalidUrl": {}, "unableToJoinChat": "无法加入聊天。可能其他方面已经关闭了对话。", - "@unableToJoinChat": {} + "@unableToJoinChat": {}, + "sendImages": "发送 {count} 张图片", + "@sendImages": { + "type": "text", + "placeholders": { + "count": {} + } + }, + "compress": "压缩", + "@compress": {} } diff --git a/fonts/Roboto/LICENSE.txt b/fonts/Roboto/LICENSE.txt deleted file mode 100644 index 75b52484e..000000000 --- a/fonts/Roboto/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/fonts/Roboto/Roboto-Black.ttf b/fonts/Roboto/Roboto-Black.ttf deleted file mode 100644 index 0112e7da6..000000000 Binary files a/fonts/Roboto/Roboto-Black.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-BlackItalic.ttf b/fonts/Roboto/Roboto-BlackItalic.ttf deleted file mode 100644 index b2c6aca57..000000000 Binary files a/fonts/Roboto/Roboto-BlackItalic.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-Bold.ttf b/fonts/Roboto/Roboto-Bold.ttf deleted file mode 100644 index 43da14d84..000000000 Binary files a/fonts/Roboto/Roboto-Bold.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-BoldItalic.ttf b/fonts/Roboto/Roboto-BoldItalic.ttf deleted file mode 100644 index bcfdab431..000000000 Binary files a/fonts/Roboto/Roboto-BoldItalic.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-Italic.ttf b/fonts/Roboto/Roboto-Italic.ttf deleted file mode 100644 index 1b5eaa361..000000000 Binary files a/fonts/Roboto/Roboto-Italic.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-Light.ttf b/fonts/Roboto/Roboto-Light.ttf deleted file mode 100644 index e7307e72c..000000000 Binary files a/fonts/Roboto/Roboto-Light.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-LightItalic.ttf b/fonts/Roboto/Roboto-LightItalic.ttf deleted file mode 100644 index 2d277afb2..000000000 Binary files a/fonts/Roboto/Roboto-LightItalic.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-Medium.ttf b/fonts/Roboto/Roboto-Medium.ttf deleted file mode 100644 index ac0f908b9..000000000 Binary files a/fonts/Roboto/Roboto-Medium.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-MediumItalic.ttf b/fonts/Roboto/Roboto-MediumItalic.ttf deleted file mode 100644 index fc36a4785..000000000 Binary files a/fonts/Roboto/Roboto-MediumItalic.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-Regular.ttf b/fonts/Roboto/Roboto-Regular.ttf deleted file mode 100644 index ddf4bfacb..000000000 Binary files a/fonts/Roboto/Roboto-Regular.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-Thin.ttf b/fonts/Roboto/Roboto-Thin.ttf deleted file mode 100644 index 2e0dee6a8..000000000 Binary files a/fonts/Roboto/Roboto-Thin.ttf and /dev/null differ diff --git a/fonts/Roboto/Roboto-ThinItalic.ttf b/fonts/Roboto/Roboto-ThinItalic.ttf deleted file mode 100644 index 084f9c0f5..000000000 Binary files a/fonts/Roboto/Roboto-ThinItalic.ttf and /dev/null differ diff --git a/fonts/Roboto/RobotoMono-Regular.ttf b/fonts/Roboto/RobotoMono-Regular.ttf deleted file mode 100644 index d9371a1bd..000000000 Binary files a/fonts/Roboto/RobotoMono-Regular.ttf and /dev/null differ diff --git a/fonts/Ubuntu/UFL.txt b/fonts/Ubuntu/UFL.txt new file mode 100644 index 000000000..6e722c88d --- /dev/null +++ b/fonts/Ubuntu/UFL.txt @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/Ubuntu/Ubuntu-Bold.ttf b/fonts/Ubuntu/Ubuntu-Bold.ttf new file mode 100644 index 000000000..c2293d5c8 Binary files /dev/null and b/fonts/Ubuntu/Ubuntu-Bold.ttf differ diff --git a/fonts/Ubuntu/Ubuntu-BoldItalic.ttf b/fonts/Ubuntu/Ubuntu-BoldItalic.ttf new file mode 100644 index 000000000..ce6e784df Binary files /dev/null and b/fonts/Ubuntu/Ubuntu-BoldItalic.ttf differ diff --git a/fonts/Ubuntu/Ubuntu-Italic.ttf b/fonts/Ubuntu/Ubuntu-Italic.ttf new file mode 100644 index 000000000..a599244e7 Binary files /dev/null and b/fonts/Ubuntu/Ubuntu-Italic.ttf differ diff --git a/fonts/Ubuntu/Ubuntu-Regular.ttf b/fonts/Ubuntu/Ubuntu-Regular.ttf new file mode 100644 index 000000000..f98a2dab8 Binary files /dev/null and b/fonts/Ubuntu/Ubuntu-Regular.ttf differ diff --git a/fonts/Ubuntu/UbuntuMono-Regular.ttf b/fonts/Ubuntu/UbuntuMono-Regular.ttf new file mode 100644 index 000000000..4977028d1 Binary files /dev/null and b/fonts/Ubuntu/UbuntuMono-Regular.ttf differ diff --git a/ios/FluffyChat Share/Info.plist b/ios/FluffyChat Share/Info.plist index d3a136ec5..876e55957 100644 --- a/ios/FluffyChat Share/Info.plist +++ b/ios/FluffyChat Share/Info.plist @@ -27,11 +27,11 @@ NSExtensionActivationRule NSExtensionActivationSupportsFileWithMaxCount - 1 + 10 NSExtensionActivationSupportsImageWithMaxCount - 1 + 10 NSExtensionActivationSupportsMovieWithMaxCount - 1 + 10 NSExtensionActivationSupportsText NSExtensionActivationSupportsWebURLWithMaxCount diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 7d65b14fd..4557f1a4b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A10584DF00E2CBE024A7FEB1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F30C00BA233E7CA67AFBED5 /* Pods_Runner.framework */; }; + BCFA6E528F0B53B71B652C77 /* Pods_FluffyChat_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B1F89C23F73F2B8E7922A37 /* Pods_FluffyChat_Share.framework */; }; C1005C45261071B5002F4F32 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1005C44261071B5002F4F32 /* ShareViewController.swift */; }; C1005C48261071B5002F4F32 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1005C46261071B5002F4F32 /* MainInterface.storyboard */; }; C1005C4C261071B5002F4F32 /* FluffyChat Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = C1005C42261071B5002F4F32 /* FluffyChat Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -60,6 +62,7 @@ /* Begin PBXFileReference section */ 09545B0C8C397F94966EA956 /* Pods-FluffyChat Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FluffyChat Share.debug.xcconfig"; path = "Target Support Files/Pods-FluffyChat Share/Pods-FluffyChat Share.debug.xcconfig"; sourceTree = ""; }; + 0BDDCB1746F84339AF1A5F40 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 23120B990D2B5081843FB313 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -78,6 +81,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9B1F89C23F73F2B8E7922A37 /* Pods_FluffyChat_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FluffyChat_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F30C00BA233E7CA67AFBED5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C1005C42261071B5002F4F32 /* FluffyChat Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FluffyChat Share.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; C1005C44261071B5002F4F32 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; C1005C47261071B5002F4F32 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 292e10831..9f392e088 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -41,6 +41,7 @@ import 'package:fluffychat/widgets/layouts/empty_page.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/log_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; abstract class AppRoutes { static FutureOr loggedInRedirect( @@ -461,15 +462,25 @@ abstract class AppRoutes { ), GoRoute( path: ':roomid', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - ChatPage( - roomId: state.pathParameters['roomid']!, - shareText: state.uri.queryParameters['body'], - eventId: state.uri.queryParameters['event'], - ), - ), + pageBuilder: (context, state) { + final body = state.uri.queryParameters['body']; + var shareItems = state.extra is List + ? state.extra as List + : null; + if (body != null && body.isNotEmpty) { + shareItems ??= []; + shareItems.add(TextShareItem(body)); + } + return defaultPageBuilder( + context, + state, + ChatPage( + roomId: state.pathParameters['roomid']!, + shareItems: shareItems, + eventId: state.uri.queryParameters['event'], + ), + ); + }, redirect: loggedOutRedirect, routes: [ GoRoute( diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 6d612b69f..2ebb14dc6 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -2,11 +2,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'app_config.dart'; abstract class FluffyThemes { - static const double columnWidth = 360.0; + static const double columnWidth = 380.0; static const double navRailWidth = 64.0; @@ -20,7 +19,7 @@ abstract class FluffyThemes { MediaQuery.of(context).size.width > FluffyThemes.columnWidth * 3.5; static const fallbackTextStyle = TextStyle( - fontFamily: 'Roboto', + fontFamily: 'Ubuntu', fontFamilyFallback: ['NotoEmoji'], ); @@ -68,22 +67,25 @@ abstract class FluffyThemes { brightness: brightness, seedColor: seed ?? AppConfig.colorSchemeSeed ?? AppConfig.primaryColor, ); + final isColumnMode = FluffyThemes.isColumnMode(context); return ThemeData( visualDensity: VisualDensity.standard, useMaterial3: true, brightness: brightness, colorScheme: colorScheme, - textTheme: PlatformInfos.isDesktop - ? brightness == Brightness.light - ? Typography.material2018().black.merge(fallbackTextTheme) - : Typography.material2018().white.merge(fallbackTextTheme) - : null, + textTheme: fallbackTextTheme, dividerColor: colorScheme.surfaceContainer, popupMenuTheme: PopupMenuThemeData( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: SegmentedButton.styleFrom( + iconColor: colorScheme.onSurface, + disabledIconColor: colorScheme.onSurface, + ), + ), textSelectionTheme: TextSelectionThemeData( selectionColor: colorScheme.onSurface.withAlpha(128), selectionHandleColor: colorScheme.secondary, @@ -96,14 +98,11 @@ abstract class FluffyThemes { filled: false, ), appBarTheme: AppBarTheme( - toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56, - shadowColor: FluffyThemes.isColumnMode(context) - ? colorScheme.surfaceContainer.withAlpha(128) - : null, - surfaceTintColor: - FluffyThemes.isColumnMode(context) ? colorScheme.surface : null, - backgroundColor: - FluffyThemes.isColumnMode(context) ? colorScheme.surface : null, + toolbarHeight: isColumnMode ? 72 : 56, + shadowColor: + isColumnMode ? colorScheme.surfaceContainer.withAlpha(128) : null, + surfaceTintColor: isColumnMode ? colorScheme.surface : null, + backgroundColor: isColumnMode ? colorScheme.surface : null, systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: brightness.reversed, @@ -124,14 +123,12 @@ abstract class FluffyThemes { ), ), ), - dialogTheme: DialogTheme( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - ), - snackBarTheme: const SnackBarThemeData( - behavior: SnackBarBehavior.floating, - ), + snackBarTheme: isColumnMode + ? const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + width: FluffyThemes.columnWidth * 1.5, + ) + : const SnackBarThemeData(behavior: SnackBarBehavior.floating), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: colorScheme.secondaryContainer, diff --git a/lib/pages/archive/archive.dart b/lib/pages/archive/archive.dart index bfa2e3238..1613200a9 100644 --- a/lib/pages/archive/archive.dart +++ b/lib/pages/archive/archive.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/archive/archive_view.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.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'; diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 19ddfb1e8..c2f35fc78 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:matrix/encryption.dart'; @@ -11,6 +10,7 @@ import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../utils/adaptive_bottom_sheet.dart'; import '../key_verification/key_verification_dialog.dart'; @@ -27,7 +27,6 @@ class BootstrapDialog extends StatefulWidget { Future show(BuildContext context) => showAdaptiveBottomSheet( context: context, builder: (context) => this, - maxHeight: 600, ); @override @@ -133,7 +132,7 @@ class BootstrapDialogState extends State { minLines: 2, maxLines: 4, readOnly: true, - style: const TextStyle(fontFamily: 'RobotoMono'), + style: const TextStyle(fontFamily: 'UbuntuMono'), controller: TextEditingController(text: key), decoration: const InputDecoration( contentPadding: EdgeInsets.all(16), @@ -258,7 +257,7 @@ class BootstrapDialogState extends State { ? null : [AutofillHints.password], controller: _recoveryKeyTextEditingController, - style: const TextStyle(fontFamily: 'RobotoMono'), + style: const TextStyle(fontFamily: 'UbuntuMono'), decoration: InputDecoration( contentPadding: const EdgeInsets.all(16), hintStyle: TextStyle( @@ -275,6 +274,7 @@ class BootstrapDialogState extends State { ElevatedButton.icon( style: ElevatedButton.styleFrom( foregroundColor: theme.colorScheme.onPrimary, + iconColor: theme.colorScheme.onPrimary, backgroundColor: theme.colorScheme.primary, ), icon: _recoveryKeyInputLoading @@ -366,7 +366,6 @@ class BootstrapDialogState extends State { .verifyOtherDeviceDescription, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - fullyCapitalizedForMaterial: false, ); if (consent != OkCancelResult.ok) return; final req = await showFutureLoadingDialog( @@ -390,6 +389,7 @@ class BootstrapDialogState extends State { style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.errorContainer, foregroundColor: theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, ), icon: const Icon(Icons.delete_outlined), label: Text(L10n.of(context).recoveryKeyLost), @@ -404,7 +404,7 @@ class BootstrapDialogState extends State { message: L10n.of(context).wipeChatBackup, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - isDestructiveAction: true, + isDestructive: true, )) { setState(() => _createBootstrap(true)); } @@ -444,7 +444,7 @@ class BootstrapDialogState extends State { titleText = L10n.of(context).oopsSomethingWentWrong; body = const Icon(Icons.error_outline, color: Colors.red, size: 80); buttons.add( - OutlinedButton( + ElevatedButton( onPressed: () => Navigator.of(context, rootNavigator: false).pop(false), child: Text(L10n.of(context).close), @@ -470,7 +470,7 @@ class BootstrapDialogState extends State { ], ); buttons.add( - OutlinedButton( + ElevatedButton( onPressed: () => Navigator.of(context, rootNavigator: false).pop(false), child: Text(L10n.of(context).close), @@ -491,13 +491,17 @@ class BootstrapDialogState extends State { title: Text(titleText ?? L10n.of(context).loadingPleaseWait), ), body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - body, - const SizedBox(height: 8), - ...buttons, - ], + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + body, + const SizedBox(height: 8), + ...buttons, + ], + ), ), ), ); diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index cbb724f85..804845434 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1,7 +1,6 @@ // ignore_for_file: depend_on_referenced_packages, implementation_imports import 'dart:async'; -import 'dart:core'; import 'dart:developer'; import 'dart:io'; @@ -9,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; @@ -54,9 +52,15 @@ import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/other_party_can_receive.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/show_scaffold_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import 'send_file_dialog.dart'; @@ -64,14 +68,14 @@ import 'send_location_dialog.dart'; class ChatPage extends StatelessWidget { final String roomId; - final String? shareText; + final List? shareItems; final String? eventId; const ChatPage({ super.key, required this.roomId, this.eventId, - this.shareText, + this.shareItems, }); @override @@ -95,7 +99,7 @@ class ChatPage extends StatelessWidget { return ChatPageWithRoom( key: Key('chat_page_${roomId}_$eventId'), room: room, - shareText: shareText, + shareItems: shareItems, eventId: eventId, ); } @@ -103,13 +107,13 @@ class ChatPage extends StatelessWidget { class ChatPageWithRoom extends StatefulWidget { final Room room; - final String? shareText; + final List? shareItems; final String? eventId; const ChatPageWithRoom({ super.key, required this.room, - this.shareText, + this.shareItems, this.eventId, }); @@ -143,11 +147,12 @@ class ChatController extends State Timer? typingTimeout; bool currentlyTyping = false; // #Pangea - // bool dragging = false; // void onDragEntered(_) => setState(() => dragging = true); + // void onDragExited(_) => setState(() => dragging = false); + // void onDragDone(DropDoneDetails details) async { // setState(() => dragging = false); // if (details.files.isEmpty) return; @@ -161,16 +166,8 @@ class ChatController extends State // ), // ); // } - - // await showAdaptiveDialog( - // context: context, - // builder: (c) => SendFileDialog( - // files: matrixFiles, - // room: room, - // ), - // ); - // } // Pangea# + bool get canSaveSelectedEvent => selectedEvents.length == 1 && { @@ -248,7 +245,7 @@ class ChatController extends State setReadMarker(eventId: mostRecentEventId); } - void updateScrollController() { + void _updateScrollController() { if (!mounted) { return; } @@ -267,22 +264,63 @@ class ChatController extends State } } - void loadDraft() async { + void _loadDraft() async { final prefs = await SharedPreferences.getInstance(); - final draft = widget.shareText ?? prefs.getString('draft_$roomId'); + final draft = prefs.getString('draft_$roomId'); if (draft != null && draft.isNotEmpty) { sendController.text = draft; } } + void _shareItems([_]) { + final shareItems = widget.shareItems; + if (shareItems == null || shareItems.isEmpty) return; + if (!room.otherPartyCanReceiveMessages) { + final theme = Theme.of(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: theme.colorScheme.errorContainer, + closeIconColor: theme.colorScheme.onErrorContainer, + content: Text( + L10n.of(context).otherPartyNotLoggedIn, + style: TextStyle( + color: theme.colorScheme.onErrorContainer, + ), + ), + showCloseIcon: true, + ), + ); + return; + } + for (final item in shareItems) { + if (item is FileShareItem) continue; + if (item is TextShareItem) room.sendTextEvent(item.value); + if (item is ContentShareItem) room.sendEvent(item.value); + } + final files = shareItems + .whereType() + .map((item) => item.value) + .toList(); + if (files.isEmpty) return; + showAdaptiveDialog( + context: context, + builder: (c) => SendFileDialog( + files: files, + room: room, + outerContext: context, + ), + ); + } + @override void initState() { - scrollController.addListener(updateScrollController); - inputFocus.addListener(inputFocusListener); + scrollController.addListener(_updateScrollController); + inputFocus.addListener(_inputFocusListener); - loadDraft(); + _loadDraft(); + WidgetsBinding.instance.addPostFrameCallback(_shareItems); super.initState(); - displayChatDetailsColumn = ValueNotifier( + _displayChatDetailsColumn = ValueNotifier( Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? false, ); @@ -320,13 +358,13 @@ class ChatController extends State ), ); // Pangea# - tryLoadTimeline(); + _tryLoadTimeline(); if (kIsWeb) { onFocusSub = html.window.onFocus.listen((_) => setReadMarker()); } } - void tryLoadTimeline() async { + void _tryLoadTimeline() async { final initialEventId = widget.eventId; loadTimelineFuture = _getTimeline(); try { @@ -353,7 +391,7 @@ class ChatController extends State scrollToEventId(readMarkerEventId, highlightEvent: false); return; } else if (readMarkerEventId.isNotEmpty && readMarkerEventIndex == -1) { - showScrollUpMaterialBanner(readMarkerEventId); + _showScrollUpMaterialBanner(readMarkerEventId); } // Mark room as read on first visit if requirements are fulfilled @@ -372,7 +410,7 @@ class ChatController extends State scrollUpBannerEventId = null; }); - void showScrollUpMaterialBanner(String eventId) => setState(() { + void _showScrollUpMaterialBanner(String eventId) => setState(() { scrollUpBannerEventId = eventId; }); @@ -448,7 +486,7 @@ class ChatController extends State ); if (!mounted) return; if (e is TimeoutException || e is IOException) { - showScrollUpMaterialBanner(eventContextId!); + _showScrollUpMaterialBanner(eventContextId!); } } timeline!.requestKeys(onlineKeyBackupOnly: false); @@ -473,7 +511,7 @@ class ChatController extends State setReadMarker(); } - Future? setReadMarkerFuture; + Future? _setReadMarkerFuture; void setReadMarker({String? eventId}) { // #Pangea @@ -485,7 +523,7 @@ class ChatController extends State return; } // Pangea# - if (setReadMarkerFuture != null) return; + if (_setReadMarkerFuture != null) return; if (_scrolledUp) return; if (scrollUpBannerEventId != null) return; @@ -511,19 +549,17 @@ class ChatController extends State } final timeline = this.timeline; - if (timeline == null || timeline.events.isEmpty) { - return; - } + if (timeline == null || timeline.events.isEmpty) return; Logs().d('Set read marker...', eventId); // ignore: unawaited_futures - setReadMarkerFuture = timeline + _setReadMarkerFuture = timeline .setReadMarker( eventId: eventId, public: AppConfig.sendPublicReadReceipts, ) .then((_) { - setReadMarkerFuture = null; + _setReadMarkerFuture = null; }) // #Pangea .catchError((e, s) { @@ -544,7 +580,6 @@ class ChatController extends State ); }); // Pangea# - if (eventId == null || eventId == timeline.room.lastEvent?.eventId) { Matrix.of(context).backgroundPush?.cancelNotification(roomId); } @@ -554,7 +589,7 @@ class ChatController extends State void dispose() { timeline?.cancelSubscriptions(); timeline = null; - inputFocus.removeListener(inputFocusListener); + inputFocus.removeListener(_inputFocusListener); onFocusSub?.cancel(); //#Pangea choreographer.stateListener.close(); @@ -645,7 +680,7 @@ class ChatController extends State }) async { // Pangea# if (sendController.text.trim().isEmpty) return; - storeInputTimeoutTimer?.cancel(); + _storeInputTimeoutTimer?.cancel(); final prefs = await SharedPreferences.getInstance(); prefs.remove('draft_$roomId'); var parseCommands = true; @@ -677,7 +712,7 @@ class ChatController extends State // wait for the next event to come through before clearing any fake event, // to make the replacement look smooth - room.client.onEvent.stream.first.then((_) => clearFakeEvent()); + room.client.onTimelineEvent.stream.first.then((_) => clearFakeEvent()); room .pangeaSendTextEvent( @@ -762,7 +797,7 @@ class ChatController extends State setState(() { sendController.text = pendingText; - inputTextIsEmpty = pendingText.isEmpty; + _inputTextIsEmpty = pendingText.isEmpty; replyEvent = null; editEvent = null; pendingText = ''; @@ -945,7 +980,7 @@ class ChatController extends State setState(() => showEmojiPicker = !showEmojiPicker); } - void inputFocusListener() { + void _inputFocusListener() { if (showEmojiPicker && inputFocus.hasFocus) { emojiPickerType = EmojiPickerType.keyboard; setState(() => showEmojiPicker = false); @@ -959,7 +994,7 @@ class ChatController extends State ); } - String getSelectedEventString() { + String _getSelectedEventString() { var copyString = ''; if (selectedEvents.length == 1) { return selectedEvents.first @@ -977,7 +1012,7 @@ class ChatController extends State } void copyEventsAction() { - Clipboard.setData(ClipboardData(text: getSelectedEventString())); + Clipboard.setData(ClipboardData(text: _getSelectedEventString())); setState(() { showEmojiPicker = false; // #Pangea @@ -992,23 +1027,22 @@ class ChatController extends State // #Pangea clearSelectedEvents(); // Pangea# - final score = await showConfirmationDialog( + final score = await showModalActionPopup( context: context, title: L10n.of(context).reportMessage, message: L10n.of(context).howOffensiveIsThisContent, cancelLabel: L10n.of(context).cancel, - okLabel: L10n.of(context).ok, actions: [ - AlertDialogAction( - key: -100, + AdaptiveModalAction( + value: -100, label: L10n.of(context).extremeOffensive, ), - AlertDialogAction( - key: -50, + AdaptiveModalAction( + value: -50, label: L10n.of(context).offensive, ), - AlertDialogAction( - key: 0, + AdaptiveModalAction( + value: 0, label: L10n.of(context).inoffensive, ), ], @@ -1019,18 +1053,18 @@ class ChatController extends State title: L10n.of(context).whyDoYouWantToReportThis, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [DialogTextField(hintText: L10n.of(context).reason)], + hintText: L10n.of(context).reason, // #Pangea autoSubmit: true, // Pangea# ); - if (reason == null || reason.single.isEmpty) return; + if (reason == null || reason.isEmpty) return; // #Pangea try { await reportMessage( context, roomId, - reason.single, + reason, event.senderId, event.content['body'].toString(), ); @@ -1040,7 +1074,7 @@ class ChatController extends State s: StackTrace.current, data: { 'roomId': roomId, - 'reason': reason.single, + 'reason': reason, 'senderId': event.senderId, 'content': event.content['body'].toString(), }, @@ -1055,10 +1089,10 @@ class ChatController extends State } // final result = await showFutureLoadingDialog( // context: context, - // future: () => Matrix.of(context).client.reportContent( + // future: () => Matrix.of(context).client.reportEvent( // event.roomId!, // event.eventId, - // reason: reason.single, + // reason: reason, // score: score, // ), // ); @@ -1098,26 +1132,22 @@ class ChatController extends State context: context, title: L10n.of(context).redactMessage, message: L10n.of(context).redactMessageDescription, - isDestructiveAction: true, - textFields: [ - DialogTextField( - hintText: L10n.of(context).optionalRedactReason, - ), - ], + isDestructive: true, + hintText: L10n.of(context).optionalRedactReason, okLabel: L10n.of(context).remove, cancelLabel: L10n.of(context).cancel, // #Pangea autoSubmit: true, // Pangea# ) - : []; + : null; if (reasonInput == null) { // #Pangea clearSelectedEvents(); // Pangea# return; } - final reason = reasonInput.single.isEmpty ? null : reasonInput.single; + final reason = reasonInput.isEmpty ? null : reasonInput; for (final event in selectedEvents) { await showFutureLoadingDialog( context: context, @@ -1196,17 +1226,17 @@ class ChatController extends State } void forwardEventsAction() async { - if (selectedEvents.length == 1) { - Matrix.of(context).shareContent = - selectedEvents.first.getDisplayEvent(timeline!).content; - } else { - Matrix.of(context).shareContent = { - 'msgtype': 'm.text', - 'body': getSelectedEventString(), - }; - } + if (selectedEvents.isEmpty) return; + await showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: selectedEvents + .map((event) => ContentShareItem(event.content)) + .toList(), + ), + ); + if (!mounted) return; setState(() => selectedEvents.clear()); - context.go('/rooms'); } void sendAgainAction() { @@ -1283,7 +1313,7 @@ class ChatController extends State duration: FluffyThemes.animationDuration, preferPosition: AutoScrollPosition.middle, ); - updateScrollController(); + _updateScrollController(); } void scrollDown() async { @@ -1397,18 +1427,20 @@ class ChatController extends State } // Pangea# + // #Pangea + // void clearSelectedEvents() => setState(() { + // selectedEvents.clear(); + // showEmojiPicker = false; + // }); void clearSelectedEvents() { - // #Pangea if (!mounted) return; - // Pangea# setState(() { - // #Pangea closeSelectionOverlay(); - // Pangea# selectedEvents.clear(); showEmojiPicker = false; }); } + // Pangea# void clearSingleSelectedEvent() { if (selectedEvents.length <= 1) { @@ -1455,16 +1487,16 @@ class ChatController extends State } final result = await showFutureLoadingDialog( context: context, - future: () => room.client.joinRoom( - room - .getState(EventTypes.RoomTombstone)! - .parsedTombstoneContent - .replacementRoom, - ), - ); - await showFutureLoadingDialog( - context: context, - future: room.leave, + future: () async { + final roomId = room.client.joinRoom( + room + .getState(EventTypes.RoomTombstone)! + .parsedTombstoneContent + .replacementRoom, + ); + await room.leave(); + return roomId; + }, ); if (result.error == null) { context.go('/rooms/${result.result!}'); @@ -1587,18 +1619,18 @@ class ChatController extends State ); } - Timer? storeInputTimeoutTimer; - static const storeInputTimeout = Duration(milliseconds: 500); + Timer? _storeInputTimeoutTimer; + Duration storeInputTimeout = const Duration(milliseconds: 500); void onInputBarChanged(String text) { - if (inputTextIsEmpty != text.isEmpty) { + if (_inputTextIsEmpty != text.isEmpty) { setState(() { - inputTextIsEmpty = text.isEmpty; + _inputTextIsEmpty = text.isEmpty; }); } - storeInputTimeoutTimer?.cancel(); - storeInputTimeoutTimer = Timer(storeInputTimeout, () async { + _storeInputTimeoutTimer?.cancel(); + _storeInputTimeoutTimer = Timer(storeInputTimeout, () async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('draft_$roomId', text); }); @@ -1637,7 +1669,7 @@ class ChatController extends State } } - var inputTextIsEmpty = true; + bool _inputTextIsEmpty = true; bool get isArchived => {Membership.leave, Membership.ban}.contains(room.membership); @@ -1664,21 +1696,21 @@ class ChatController extends State } }); } - final callType = await showModalActionSheet( + final callType = await showModalActionPopup( context: context, title: L10n.of(context).warning, message: L10n.of(context).videoCallsBetaWarning, cancelLabel: L10n.of(context).cancel, actions: [ - SheetAction( + AdaptiveModalAction( label: L10n.of(context).voiceCall, - icon: Icons.phone_outlined, - key: CallType.kVoice, + icon: const Icon(Icons.phone_outlined), + value: CallType.kVoice, ), - SheetAction( + AdaptiveModalAction( label: L10n.of(context).videoCall, - icon: Icons.video_call_outlined, - key: CallType.kVideo, + icon: const Icon(Icons.video_call_outlined), + value: CallType.kVideo, ), ], ); @@ -1787,48 +1819,16 @@ class ChatController extends State onSelectMessage(event); }); } + // Pangea# - // final List selectedTokenIndicies = []; - // void onClickOverlayMessageToken( - // PangeaMessageEvent pangeaMessageEvent, - // int tokenIndex, - // ) { - // if (pangeaMessageEvent.originalSent?.tokens == null || - // tokenIndex < 0 || - // tokenIndex >= pangeaMessageEvent.originalSent!.tokens!.length) { - // selectedTokenIndicies.clear(); - // return; - // } - - // // if there's stuff that's already selected, then we already ahve a sentence deselect - // if (selectedTokenIndicies.isNotEmpty) { - // final bool listContainedIndex = - // selectedTokenIndicies.contains(tokenIndex); - - // selectedTokenIndicies.clear(); - // if (!listContainedIndex) { - // selectedTokenIndicies.add(tokenIndex); - // } - // } - - // // TODO - // // if this is already selected, see if there's sentnence and selelct that - - // // if nothing is select, select one token - // else { - // selectedTokenIndicies.add(tokenIndex); - // } - // } - // // Pangea# - - late final ValueNotifier displayChatDetailsColumn; + late final ValueNotifier _displayChatDetailsColumn; void toggleDisplayChatDetailsColumn() async { await Matrix.of(context).store.setBool( SettingKeys.displayChatDetailsColumn, - !displayChatDetailsColumn.value, + !_displayChatDetailsColumn.value, ); - displayChatDetailsColumn.value = !displayChatDetailsColumn.value; + _displayChatDetailsColumn.value = !_displayChatDetailsColumn.value; } @override @@ -1843,7 +1843,7 @@ class ChatController extends State duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, child: ValueListenableBuilder( - valueListenable: displayChatDetailsColumn, + valueListenable: _displayChatDetailsColumn, builder: (context, displayChatDetailsColumn, _) { if (!FluffyThemes.isThreeColumnMode(context) || room.membership != Membership.join || diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index 6124049cb..66027192b 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/sync_status_localization.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/presence_builder.dart'; @@ -57,30 +59,68 @@ class ChatAppBarTitle extends StatelessWidget { fontSize: 16, ), ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: PresenceBuilder( - userId: room.directChatMatrixID, - builder: (context, presence) { - final lastActiveTimestamp = presence?.lastActiveTimestamp; - final style = Theme.of(context).textTheme.bodySmall; - if (presence?.currentlyActive == true) { - return Text( - L10n.of(context).currentlyActive, - style: style, - ); - } - if (lastActiveTimestamp != null) { - return Text( - L10n.of(context).lastActiveAgo( - lastActiveTimestamp.localizedTimeShort(context), - ), - style: style, - ); - } - return const SizedBox.shrink(); - }, - ), + StreamBuilder( + stream: room.client.onSyncStatus.stream, + builder: (context, snapshot) { + final status = room.client.onSyncStatus.value ?? + const SyncStatusUpdate(SyncStatus.waitingForResponse); + final hide = FluffyThemes.isColumnMode(context) || + (room.client.onSync.value != null && + status.status != SyncStatus.error && + room.client.prevBatch != null); + return AnimatedSize( + duration: FluffyThemes.animationDuration, + child: hide + ? PresenceBuilder( + userId: room.directChatMatrixID, + builder: (context, presence) { + final lastActiveTimestamp = + presence?.lastActiveTimestamp; + final style = + Theme.of(context).textTheme.bodySmall; + if (presence?.currentlyActive == true) { + return Text( + L10n.of(context).currentlyActive, + style: style, + ); + } + if (lastActiveTimestamp != null) { + return Text( + L10n.of(context).lastActiveAgo( + lastActiveTimestamp + .localizedTimeShort(context), + ), + style: style, + ); + } + return const SizedBox.shrink(); + }, + ) + : Row( + children: [ + Icon( + status.icon, + size: 12, + color: status.error != null + ? Theme.of(context).colorScheme.error + : null, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + status.calcLocalizedString(context), + style: TextStyle( + fontSize: 12, + color: status.error != null + ? Theme.of(context).colorScheme.error + : null, + ), + ), + ), + ], + ), + ); + }, ), ], ), diff --git a/lib/pages/chat/chat_emoji_picker.dart b/lib/pages/chat/chat_emoji_picker.dart index 41534eaaa..63fa0403c 100644 --- a/lib/pages/chat/chat_emoji_picker.dart +++ b/lib/pages/chat/chat_emoji_picker.dart @@ -55,6 +55,7 @@ class ChatEmojiPicker extends StatelessWidget { theme.colorScheme.primary.withAlpha(128), iconColorSelected: theme.colorScheme.primary, indicatorColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.surface, ), skinToneConfig: SkinToneConfig( dialogBackgroundColor: Color.lerp( @@ -107,9 +108,15 @@ class NoRecent extends StatelessWidget { @override Widget build(BuildContext context) { - return Text( - L10n.of(context).emoteKeyboardNoRecents, - style: Theme.of(context).textTheme.bodyLarge, + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context).emoteKeyboardNoRecents, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ), ); } } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 1c094dfc6..8bb7f2c43 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/pangea_reaction_picker.dart'; +import 'package:fluffychat/utils/other_party_can_receive.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import '../../config/themes.dart'; import 'chat.dart'; @@ -38,6 +39,20 @@ class ChatInputRow extends StatelessWidget { controller.emojiPickerType == EmojiPickerType.reaction) { return const SizedBox.shrink(); } + + if (!controller.room.otherPartyCanReceiveMessages) { + return Center( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + L10n.of(context).otherPartyNotLoggedIn, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + ); + } + // #Pangea // const height = 48.0; const height = AppConfig.defaultFooterHeight; @@ -129,7 +144,7 @@ class ChatInputRow extends StatelessWidget { child: Row( children: [ // #Pangea - // Text(L10n.of(context)!.reply), + // Text(L10n.of(context).reply), // const Icon(Icons.keyboard_arrow_right), const Icon(Symbols.reply), const SizedBox(width: 6), @@ -148,12 +163,11 @@ class ChatInputRow extends StatelessWidget { // children: [ // Text(L10n.of(context).tryToSendAgain), // const SizedBox(width: 4), - // const Icon(Icons.send_outlined, - // size: 16), + // const Icon(Icons.send_outlined, size: 16), // ], // ), // ), - // ) + // ), // Pangea# : const SizedBox.shrink(), // #Pangea @@ -194,44 +208,31 @@ class ChatInputRow extends StatelessWidget { onSelected: controller.onAddPopupMenuButtonSelected, itemBuilder: (BuildContext context) => >[ - //#Pangea - if (controller.pangeaController.permissionsController - .canShareFile(controller.roomId)) - //Pangea# - PopupMenuItem( - value: 'file', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment_outlined), - ), - title: Text(L10n.of(context).sendFile), - contentPadding: const EdgeInsets.all(0), + PopupMenuItem( + value: 'file', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment_outlined), ), + title: Text(L10n.of(context).sendFile), + contentPadding: const EdgeInsets.all(0), ), - //#Pangea - if (controller.pangeaController.permissionsController - .canSharePhoto(controller.roomId)) - //Pangea# - PopupMenuItem( - value: 'image', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image_outlined), - ), - title: Text(L10n.of(context).sendImage), - contentPadding: const EdgeInsets.all(0), + ), + PopupMenuItem( + value: 'image', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image_outlined), ), + title: Text(L10n.of(context).sendImage), + contentPadding: const EdgeInsets.all(0), ), - //#Pangea - // if (PlatformInfos.isMobile) - if (PlatformInfos.isMobile && - controller.pangeaController.permissionsController - .canSharePhoto(controller.roomId)) - //Pangea# + ), + if (PlatformInfos.isMobile) PopupMenuItem( value: 'camera', child: ListTile( @@ -244,12 +245,7 @@ class ChatInputRow extends StatelessWidget { contentPadding: const EdgeInsets.all(0), ), ), - //#Pangea - // if (PlatformInfos.isMobile) - if (PlatformInfos.isMobile && - controller.pangeaController.permissionsController - .canShareVideo(controller.roomId)) - //Pangea# + if (PlatformInfos.isMobile) PopupMenuItem( value: 'camera-video', child: ListTile( @@ -262,12 +258,7 @@ class ChatInputRow extends StatelessWidget { contentPadding: const EdgeInsets.all(0), ), ), - //#Pangea - // if (PlatformInfos.isMobile) - if (PlatformInfos.isMobile && - controller.pangeaController.permissionsController - .canShareLocation(controller.roomId)) - //Pangea# + if (PlatformInfos.isMobile) PopupMenuItem( value: 'location', child: ListTile( @@ -319,7 +310,7 @@ class ChatInputRow extends StatelessWidget { ), ) // #Pangea - : const SizedBox(width: 10), + : const SizedBox.shrink(), // if (Matrix.of(context).isMultiAccount && // Matrix.of(context).hasComplexBundles && // Matrix.of(context).currentBundle!.length > 1) @@ -465,9 +456,6 @@ class ChatInputRow extends StatelessWidget { // mxContent: snapshot.data?.avatarUrl, // name: snapshot.data?.displayName ?? // client.userID!.localpart, -// // #Pangea -// presenceUserId: client.userID!, -// // Pangea# // size: 20, // ), // title: Text(snapshot.data?.displayName ?? client.userID!), @@ -481,9 +469,6 @@ class ChatInputRow extends StatelessWidget { // mxContent: snapshot.data?.avatarUrl, // name: snapshot.data?.displayName ?? // Matrix.of(context).client.userID!.localpart, -// // #Pangea -// presenceUserId: Matrix.of(context).client.userID!, -// // Pangea# // size: 20, // ), // ), diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 08f6fa473..9ef3a62d5 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -12,7 +12,6 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; -import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; @@ -26,12 +25,12 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import '../../utils/stream_extension.dart'; +import 'chat_emoji_picker.dart'; enum _EventContextAction { info, report } @@ -144,7 +143,8 @@ class ChatView extends StatelessWidget { } // } else if (!controller.room.isArchived) { // return [ - // if (Matrix.of(context).voipPlugin != null && + // if (AppConfig.experimentalVoip && + // Matrix.of(context).voipPlugin != null && // controller.room.isDirectChat) // IconButton( // onPressed: controller.onPhoneButtonTap, @@ -287,6 +287,8 @@ class ChatView extends StatelessWidget { ), ), // #Pangea + // floatingActionButtonLocation: + // FloatingActionButtonLocation.miniCenterFloat, // floatingActionButton: controller.showScrollDownButton && // controller.selectedEvents.isEmpty // ? Padding( @@ -295,20 +297,19 @@ class ChatView extends StatelessWidget { // onPressed: controller.scrollDown, // heroTag: null, // mini: true, + // backgroundColor: theme.colorScheme.surface, + // foregroundColor: theme.colorScheme.onSurface, // child: const Icon(Icons.arrow_downward_outlined), // ), // ) // : null, - // Pangea# - body: - // #Pangea - // DropTarget( - // onDragDone: controller.onDragDone, - // onDragEntered: controller.onDragEntered, - // onDragExited: controller.onDragExited, - // child: - // Pangea# - Stack( + // body: DropTarget( + // onDragDone: controller.onDragDone, + // onDragEntered: controller.onDragEntered, + // onDragExited: controller.onDragExited, + // child: Stack( + body: Stack( + // Pangea# children: [ if (accountConfig.wallpaperUrl != null) Opacity( @@ -405,7 +406,6 @@ class ChatView extends StatelessWidget { // : Column( // mainAxisSize: MainAxisSize.min, // children: [ - // const ConnectionStatusHeader(), // ReactionsPicker(controller), // ReplyDisplay(controller), // ChatInputRow(controller), @@ -424,9 +424,7 @@ class ChatView extends StatelessWidget { ], ), // #Pangea - ChatViewBackground( - choreographer: controller.choreographer, - ), + ChatViewBackground(controller.choreographer), Positioned( left: 0, right: 0, @@ -484,7 +482,6 @@ class ChatView extends StatelessWidget { child: Column( children: [ - const ConnectionStatusHeader(), ITBar( choreographer: controller.choreographer, ), @@ -518,7 +515,7 @@ class ChatView extends StatelessWidget { // #Pangea // if (controller.dragging) // Container( - // color: theme.scaffoldBackgroundColor.withOpacity(0.9), + // color: theme.scaffoldBackgroundColor.withAlpha(230), // alignment: Alignment.center, // child: const Icon( // Icons.upload_outlined, diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 2f22bd3f3..769ccce9c 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -11,17 +11,18 @@ import 'package:matrix/matrix.dart'; import 'package:opus_caf_converter_dart/opus_caf_converter_dart.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import '../../../utils/matrix_sdk_extensions/event_extension.dart'; class AudioPlayerWidget extends StatefulWidget { final Color color; + final Color linkColor; final double fontSize; // #Pangea // final Event event; @@ -30,8 +31,9 @@ class AudioPlayerWidget extends StatefulWidget { final bool autoplay; final Function(bool)? setIsPlayingAudio; final double padding; + final ChatController chatController; + final bool isOverlay; final MessageOverlayController? overlayController; - final ChatController? chatController; // Pangea# static String? currentId; @@ -45,7 +47,8 @@ class AudioPlayerWidget extends StatefulWidget { const AudioPlayerWidget( this.event, { - this.color = Colors.black, + required this.color, + required this.linkColor, required this.fontSize, // #Pangea this.matrixFile, @@ -54,8 +57,9 @@ class AudioPlayerWidget extends StatefulWidget { this.sectionEndMS, this.setIsPlayingAudio, this.padding = 12.0, + required this.chatController, + required this.isOverlay, this.overlayController, - this.chatController, // Pangea# super.key, }); @@ -356,7 +360,7 @@ class AudioPlayerState extends State { : _downloadAction(); } - _onShowToolbar = widget.chatController?.showToolbarStream.stream + _onShowToolbar = widget.chatController.showToolbarStream.stream .where((eventID) => eventID == widget.event?.eventId) .listen((eventID) { audioPlayer?.pause(); @@ -373,17 +377,14 @@ class AudioPlayerState extends State { final statusText = this.statusText ??= _durationString ?? '00:00'; final audioPlayer = this.audioPlayer; - final body = widget.event?.content.tryGet('body') ?? - widget.event?.content.tryGet('filename'); - final displayBody = body != null && - body.isNotEmpty && - widget.event?.content['org.matrix.msc1767.audio'] == null; + // #Pangea + // final fileDescription = widget.event.fileDescription; + final fileDescription = widget.event?.fileDescription; + // Pangea# final wavePosition = (currentPosition / maxPosition) * AudioPlayerWidget.wavesCount; - final fontSize = 12 * AppConfig.fontSizeFactor; - return Padding( // #Pangea // padding: const EdgeInsets.all(12.0), @@ -508,46 +509,66 @@ class AudioPlayerState extends State { ), ), // #Pangea - // ), // const SizedBox(width: 8), - // Badge( - // isLabelVisible: audioPlayer != null, - // label: audioPlayer == null - // ? null - // : Text( - // '${audioPlayer.speed.toString()}x', - // ), - // backgroundColor: theme.colorScheme.secondary, - // textColor: theme.colorScheme.onSecondary, - // child: InkWell( - // splashColor: widget.color.withAlpha(128), - // borderRadius: BorderRadius.circular(64), - // onTap: audioPlayer == null ? null : _toggleSpeed, + // AnimatedCrossFade( + // firstChild: Padding( + // padding: const EdgeInsets.only(right: 8.0), // child: Icon( // Icons.mic_none_outlined, // color: widget.color, // ), // ), + // secondChild: Material( + // color: widget.color.withAlpha(64), + // borderRadius: BorderRadius.circular(AppConfig.borderRadius), + // child: InkWell( + // borderRadius: + // BorderRadius.circular(AppConfig.borderRadius), + // onTap: _toggleSpeed, + // child: SizedBox( + // width: 32, + // height: 20, + // child: Center( + // child: Text( + // '${audioPlayer?.speed.toString()}x', + // style: TextStyle( + // color: widget.color, + // fontSize: 9, + // ), + // ), + // ), + // ), + // ), + // ), + // alignment: Alignment.center, + // crossFadeState: audioPlayer == null + // ? CrossFadeState.showFirst + // : CrossFadeState.showSecond, + // duration: FluffyThemes.animationDuration, // ), - // const SizedBox(width: 8), // Pangea# ], ), ), - if (displayBody) ...[ + if (fileDescription != null + // #Pangea + && + widget.event != null + // Pangea# + ) ...[ const SizedBox(height: 8), Linkify( - text: body, + text: fileDescription, style: TextStyle( color: widget.color, - fontSize: fontSize, + fontSize: widget.fontSize, ), options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( - color: widget.color.withAlpha(150), - fontSize: fontSize, + color: widget.linkColor, + fontSize: widget.fontSize, decoration: TextDecoration.underline, - decorationColor: widget.color.withAlpha(150), + decorationColor: widget.linkColor, ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ), diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 361b4ab4e..a2953d768 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -4,14 +4,13 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; -import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:html/dom.dart' as dom; -import 'package:linkify/linkify.dart'; +import 'package:html/parser.dart' as parser; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart'; @@ -23,6 +22,9 @@ class HtmlMessage extends StatelessWidget { final String html; final Room room; final Color textColor; + final double fontSize; + final TextStyle linkStyle; + final void Function(LinkableElement) onOpen; // #Pangea final bool isOverlay; final PangeaMessageEvent? pangeaMessageEvent; @@ -39,7 +41,10 @@ class HtmlMessage extends StatelessWidget { super.key, required this.html, required this.room, + required this.fontSize, + required this.linkStyle, this.textColor = Colors.black, + required this.onOpen, // #Pangea required this.isOverlay, required this.event, @@ -52,35 +57,78 @@ class HtmlMessage extends StatelessWidget { // Pangea# }); - dom.Node _linkifyHtml(dom.Node element) { - for (final node in element.nodes) { - if (node is! dom.Text || - (element is dom.Element && element.localName == 'code')) { - node.replaceWith(_linkifyHtml(node)); - continue; - } + /// Keep in sync with: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes + static const Set allowedHtmlTags = { + 'font', + 'del', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'p', + 'a', + 'ul', + 'ol', + 'sup', + 'sub', + 'li', + 'b', + 'i', + 'u', + 'strong', + 'em', + 'strike', + 'code', + 'hr', + 'br', + 'div', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 'caption', + 'pre', + 'span', + 'img', + 'details', + 'summary', + // Not in the allowlist of the matrix spec yet but should be harmless: + 'ruby', + 'rp', + 'rt', + 'html', + 'body', + // Workaround for https://github.com/krille-chan/fluffychat/issues/507 + 'tg-forward', + // #Pangea + 'token', + // Pangea# + }; - final parts = linkify( - node.text, - options: const LinkifyOptions(humanize: false), - ); - - if (!parts.any((part) => part is UrlElement)) { - continue; - } - - final newHtml = parts - .map( - (linkifyElement) => linkifyElement is! UrlElement - ? linkifyElement.text.replaceAll('<', '<') - : '${linkifyElement.text}', - ) - .join(' '); - - node.replaceWith(dom.Element.html('

$newHtml

')); - } - return element; - } + /// We add line breaks before these tags: + static const Set blockHtmlTags = { + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ul', + 'ol', + 'li', + 'pre', + 'br', + 'div', + 'table', + 'blockquote', + 'details', + }; // #Pangea List? get tokens => @@ -149,43 +197,385 @@ class HtmlMessage extends StatelessWidget { } // Pangea# + /// Adding line breaks before block elements. + List _renderWithLineBreaks( + dom.NodeList nodes, + BuildContext context, { + int depth = 1, + }) => + [ + for (var i = 0; i < nodes.length; i++) ...[ + if (i > 0 && + nodes[i] is dom.Element && + blockHtmlTags.contains((nodes[i] as dom.Element).localName)) + const TextSpan(text: '\n'), // Add linebreak + // Actually render the node child: + _renderHtml(nodes[i], context, depth: depth + 1), + ], + ]; + + /// Transforms a Node to an InlineSpan. + InlineSpan _renderHtml( + dom.Node node, + BuildContext context, { + int depth = 1, + }) { + // We must not render elements nested more than 100 elements deep: + if (depth >= 100) return const TextSpan(); + + // This is a text node, so we render it as text: + if (node is! dom.Element) { + var text = node.text ?? ''; + // Single linebreak nodes between Elements are ignored: + if (text == '\n') text = ''; + + return LinkifySpan( + text: text, + options: const LinkifyOptions(humanize: false), + linkStyle: linkStyle, + onOpen: onOpen, + ); + } + + // We must not render tags which are not in the allow list: + if (!allowedHtmlTags.contains(node.localName)) return const TextSpan(); + + switch (node.localName) { + // #Pangea + case 'token': + final token = getToken( + node.attributes['offset'] ?? '', + int.tryParse(node.attributes['offset'] ?? '') ?? 0, + int.tryParse(node.attributes['length'] ?? '') ?? 0, + ); + + final selected = token != null && isSelected != null + ? isSelected!.call(token) + : false; + + final shouldDo = token?.shouldDoActivity( + a: ActivityTypeEnum.wordMeaning, + feature: null, + tag: null, + ) ?? + false; + + final didMeaningActivity = token?.didActivitySuccessfully( + ActivityTypeEnum.wordMeaning, + ) ?? + true; + + Color backgroundColor = Colors.transparent; + if (selected) { + backgroundColor = AppConfig.primaryColor.withAlpha(80); + } else if (isSelected != null && shouldDo) { + backgroundColor = !didMeaningActivity + ? AppConfig.success.withAlpha(60) + : AppConfig.gold.withAlpha(60); + } + + return TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = onClick != null && token != null + ? () => onClick?.call(token) + : null, + text: node.innerHtml, + style: AppConfig.messageTextStyle( + pangeaMessageEvent!.event, + textColor, + ).merge(TextStyle(backgroundColor: backgroundColor)), + ); + // Pangea# + case 'a': + final href = node.attributes['href']; + if (href == null) continue block; + final matrixId = node.attributes['href'] + ?.parseIdentifierIntoParts() + ?.primaryIdentifier; + if (matrixId != null) { + if (matrixId.sigil == '@') { + final user = room.unsafeGetUserFromMemoryOrFallback(matrixId); + return WidgetSpan( + child: MatrixPill( + key: Key('user_pill_$matrixId'), + name: user.calcDisplayname(), + avatar: user.avatarUrl, + uri: href, + outerContext: context, + fontSize: fontSize, + color: linkStyle.color, + ), + ); + } + if (matrixId.sigil == '#' || matrixId.sigil == '!') { + final room = matrixId.sigil == '!' + ? this.room.client.getRoomById(matrixId) + : this.room.client.getRoomByAlias(matrixId); + return WidgetSpan( + child: MatrixPill( + name: room?.getLocalizedDisplayname() ?? matrixId, + avatar: room?.avatar, + uri: href, + outerContext: context, + fontSize: fontSize, + color: linkStyle.color, + ), + ); + } + } + return WidgetSpan( + child: Tooltip( + message: href, + child: InkWell( + splashColor: Colors.transparent, + onTap: () => UrlLauncher(context, href, node.text).launchUrl(), + child: Text.rich( + TextSpan( + children: _renderWithLineBreaks( + node.nodes, + context, + depth: depth, + ), + style: linkStyle, + ), + style: const TextStyle(height: 1.25), + ), + ), + ), + ); + case 'li': + if (!{'ol', 'ul'}.contains(node.parent?.localName)) { + continue block; + } + return WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: fontSize), + child: Text.rich( + TextSpan( + children: [ + if (node.parent?.localName == 'ul') + const TextSpan(text: '• '), + if (node.parent?.localName == 'ol') + TextSpan( + text: + '${(node.parent?.nodes.indexOf(node) ?? 0) + (int.tryParse(node.parent?.attributes['start'] ?? '1') ?? 1)}. ', + ), + ..._renderWithLineBreaks( + node.nodes, + context, + depth: depth, + ), + ], + style: TextStyle(fontSize: fontSize, color: textColor), + ), + ), + ), + ); + case 'blockquote': + return WidgetSpan( + child: Container( + padding: const EdgeInsets.only(left: 8.0), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: textColor, + width: 3, + ), + ), + ), + child: Text.rich( + TextSpan( + children: _renderWithLineBreaks( + node.nodes, + context, + depth: depth, + ), + ), + style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: fontSize, + color: textColor, + ), + ), + ), + ); + case 'code': + final isInline = node.parent?.localName != 'pre'; + return WidgetSpan( + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: BorderRadius.circular(4), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: HighlightView( + node.text, + language: node.className + .split(' ') + .singleWhereOrNull( + (className) => className.startsWith('language-'), + ) + ?.split('language-') + .last ?? + 'md', + theme: shadesOfPurpleTheme, + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: isInline ? 0 : 8, + ), + textStyle: TextStyle(fontSize: fontSize), + ), + ), + ), + ); + case 'img': + final mxcUrl = Uri.tryParse(node.attributes['src'] ?? ''); + if (mxcUrl == null || mxcUrl.scheme != 'mxc') { + return TextSpan(text: node.attributes['alt']); + } + + final width = double.tryParse(node.attributes['width'] ?? ''); + final height = double.tryParse(node.attributes['height'] ?? ''); + const defaultDimension = 64.0; + final actualWidth = width ?? height ?? defaultDimension; + final actualHeight = height ?? width ?? defaultDimension; + + return WidgetSpan( + child: SizedBox( + width: actualWidth, + height: actualHeight, + child: MxcImage( + uri: mxcUrl, + width: actualWidth, + height: actualHeight, + isThumbnail: (actualWidth * actualHeight) > (256 * 256), + ), + ), + ); + case 'hr': + return const WidgetSpan(child: Divider()); + case 'details': + var obscure = true; + return WidgetSpan( + child: StatefulBuilder( + builder: (context, setState) => InkWell( + splashColor: Colors.transparent, + onTap: () => setState(() { + obscure = !obscure; + }), + child: Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Icon( + obscure ? Icons.arrow_right : Icons.arrow_drop_down, + size: fontSize * 1.2, + color: textColor, + ), + ), + if (obscure) + ...node.nodes + .where( + (node) => + node is dom.Element && + node.localName == 'summary', + ) + .map( + (node) => _renderHtml(node, context, depth: depth), + ) + else + ..._renderWithLineBreaks( + node.nodes, + context, + depth: depth, + ), + ], + ), + style: TextStyle( + fontSize: fontSize, + color: textColor, + ), + ), + ), + ), + ); + case 'span': + if (!node.attributes.containsKey('data-mx-spoiler')) { + continue block; + } + var obscure = true; + return WidgetSpan( + child: StatefulBuilder( + builder: (context, setState) => InkWell( + splashColor: Colors.transparent, + onTap: () => setState(() { + obscure = !obscure; + }), + child: Text.rich( + TextSpan( + children: _renderWithLineBreaks( + node.nodes, + context, + depth: depth, + ), + ), + style: TextStyle( + fontSize: fontSize, + color: textColor, + backgroundColor: obscure ? textColor : null, + ), + ), + ), + ), + ); + block: + default: + return TextSpan( + style: switch (node.localName) { + 'body' => TextStyle( + fontSize: fontSize, + color: textColor, + ), + 'a' => linkStyle, + 'strong' => const TextStyle(fontWeight: FontWeight.bold), + 'em' || 'i' => const TextStyle(fontStyle: FontStyle.italic), + 'del' || + 'strikethrough' => + const TextStyle(decoration: TextDecoration.lineThrough), + 'u' => const TextStyle(decoration: TextDecoration.underline), + 'h1' => TextStyle(fontSize: fontSize * 1.6, height: 2), + 'h2' => TextStyle(fontSize: fontSize * 1.5, height: 2), + 'h3' => TextStyle(fontSize: fontSize * 1.4, height: 2), + 'h4' => TextStyle(fontSize: fontSize * 1.3, height: 1.75), + 'h5' => TextStyle(fontSize: fontSize * 1.2, height: 1.75), + 'h6' => TextStyle(fontSize: fontSize * 1.1, height: 1.5), + 'span' => TextStyle( + color: node.attributes['color']?.hexToColor ?? + node.attributes['data-mx-color']?.hexToColor ?? + textColor, + backgroundColor: + node.attributes['data-mx-bg-color']?.hexToColor, + ), + 'sup' => + const TextStyle(fontFeatures: [FontFeature.superscripts()]), + 'sub' => const TextStyle(fontFeatures: [FontFeature.subscripts()]), + _ => null, + }, + children: _renderWithLineBreaks( + node.nodes, + context, + depth: depth, + ), + ); + } + } + @override Widget build(BuildContext context) { - final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; - - final linkColor = textColor.withAlpha(150); - - final blockquoteStyle = Style( - border: Border( - left: BorderSide( - width: 3, - color: textColor, - ), - ), - padding: HtmlPaddings.only(left: 6, bottom: 0), - ); - // #Pangea - // final element = _linkifyHtml(HtmlParser.parseHTML(html)); - dom.Node element = _linkifyHtml(HtmlParser.parseHTML(html)); - if (tokens != null && element is dom.Element) { - try { - element = _tokenizeHtml(element, element.innerHtml, List.from(tokens!)); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - 'html': html, - 'tokens': tokens, - }, - ); - } + dom.Node parsed = parser.parse(html).body ?? dom.Element.html(''); + if (tokens != null) { + parsed = _tokenizeHtml(parsed, html, List.from(tokens!)); } - // Pangea# - - // there is no need to pre-validate the html, as we validate it while rendering - // #Pangea return SelectionArea( child: GestureDetector( onTap: () { @@ -199,458 +589,21 @@ class HtmlMessage extends StatelessWidget { } }, // Pangea# - child: Html.fromElement( - documentElement: element as dom.Element, - style: { - '*': Style( - color: textColor, - margin: Margins.all(0), - fontSize: FontSize(fontSize), - ), - 'a': Style(color: linkColor, textDecorationColor: linkColor), - 'h1': Style( - fontSize: FontSize(fontSize * 2), - lineHeight: LineHeight.number(1.5), - fontWeight: FontWeight.w600, - ), - 'h2': Style( - fontSize: FontSize(fontSize * 1.75), - lineHeight: LineHeight.number(1.5), - fontWeight: FontWeight.w500, - ), - 'h3': Style( - fontSize: FontSize(fontSize * 1.5), - lineHeight: LineHeight.number(1.5), - ), - 'h4': Style( - fontSize: FontSize(fontSize * 1.25), - lineHeight: LineHeight.number(1.5), - ), - 'h5': Style( - fontSize: FontSize(fontSize * 1.25), - lineHeight: LineHeight.number(1.5), - ), - 'h6': Style( - fontSize: FontSize(fontSize), - lineHeight: LineHeight.number(1.5), - ), - 'blockquote': blockquoteStyle, - 'tg-forward': blockquoteStyle, - 'hr': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'table': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'tr': Style( - border: Border.all(color: textColor, width: 0.5), - ), - 'td': Style( - border: Border.all(color: textColor, width: 0.5), - padding: HtmlPaddings.all(2), - ), - 'th': Style( - border: Border.all(color: textColor, width: 0.5), - ), - }, - extensions: [ - RoomPillExtension(context, room, fontSize, linkColor), - CodeExtension(fontSize: fontSize), + child: Text.rich( + _renderHtml( // #Pangea - // const TableHtmlExtension(), + // parser.parse(html).body ?? dom.Element.html(''), + parsed, // Pangea# - SpoilerExtension(textColor: textColor), - const ImageExtension(), - FontColorExtension(), - FallbackTextExtension(fontSize: fontSize), - // #Pangea - if (pangeaMessageEvent != null) - TokenExtension( - style: AppConfig.messageTextStyle( - pangeaMessageEvent!.event, - textColor, - ), - getToken: getToken, - isSelected: isSelected, - onClick: onClick, - ), - // Pangea# - ], - onLinkTap: (url, _, element) => UrlLauncher( context, - url, - element?.text, - ).launchUrl(), - onlyRenderTheseTags: const { - ...allowedHtmlTags, - // Needed to make it work properly - 'body', - 'html', - }, - shrinkWrap: true, - ), - ), - ); - } - - static const Set fallbackTextTags = {'tg-forward'}; - - /// Keep in sync with: https://spec.matrix.org/v1.6/client-server-api/#mroommessage-msgtypes - static const Set allowedHtmlTags = { - 'font', - 'del', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'blockquote', - 'p', - 'a', - 'ul', - 'ol', - 'sup', - 'sub', - 'li', - 'b', - 'i', - 'u', - 'strong', - 'em', - 'strike', - 'code', - 'hr', - 'br', - 'div', - 'table', - 'thead', - 'tbody', - 'tr', - 'th', - 'td', - 'caption', - 'pre', - 'span', - 'img', - 'details', - 'summary', - // Not in the allowlist of the matrix spec yet but should be harmless: - 'ruby', - 'rp', - 'rt', - // Workaround for https://github.com/krille-chan/fluffychat/issues/507 - ...fallbackTextTags, - // #Pangea - 'token', - // Pangea# - }; -} - -// #Pangea -class TokenExtension extends HtmlExtension { - final TextStyle style; - final PangeaToken? Function(String, int, int) getToken; - final bool Function(PangeaToken)? isSelected; - final void Function(PangeaToken)? onClick; - - const TokenExtension({ - required this.style, - required this.getToken, - this.isSelected, - this.onClick, - }); - - @override - Set get supportedTags => {'token'}; - - @override - InlineSpan build(ExtensionContext context) { - final token = getToken( - context.attributes['offset'] ?? '', - int.tryParse(context.attributes['offset'] ?? '') ?? 0, - int.tryParse(context.attributes['length'] ?? '') ?? 0, - ); - - final selected = - token != null && isSelected != null ? isSelected!.call(token) : false; - - final shouldDo = token?.shouldDoActivity( - a: ActivityTypeEnum.wordMeaning, - feature: null, - tag: null, - ) ?? - false; - - final didMeaningActivity = token?.didActivitySuccessfully( - ActivityTypeEnum.wordMeaning, - ) ?? - true; - - Color backgroundColor = Colors.transparent; - if (selected) { - backgroundColor = AppConfig.primaryColor.withAlpha(80); - } else if (isSelected != null && shouldDo) { - backgroundColor = !didMeaningActivity - ? AppConfig.success.withAlpha(60) - : AppConfig.gold.withAlpha(60); - } - - return TextSpan( - recognizer: TapGestureRecognizer() - ..onTap = onClick != null && token != null - ? () => onClick?.call(token) - : null, - text: context.innerHtml, - style: style.merge(TextStyle(backgroundColor: backgroundColor)), - ); - } -} -// Pangea# - -class FontColorExtension extends HtmlExtension { - static const String colorAttribute = 'color'; - static const String mxColorAttribute = 'data-mx-color'; - static const String bgColorAttribute = 'data-mx-bg-color'; - - @override - Set get supportedTags => {'font', 'span'}; - - @override - bool matches(ExtensionContext context) { - if (!supportedTags.contains(context.elementName)) return false; - return context.element?.attributes.keys.any( - { - colorAttribute, - mxColorAttribute, - bgColorAttribute, - }.contains, - ) ?? - false; - } - - Color? hexToColor(String? hexCode) { - if (hexCode == null) return null; - if (hexCode.startsWith('#')) hexCode = hexCode.substring(1); - if (hexCode.length == 6) hexCode = 'FF$hexCode'; - final colorValue = int.tryParse(hexCode, radix: 16); - return colorValue == null ? null : Color(colorValue); - } - - @override - InlineSpan build( - ExtensionContext context, - ) { - final colorText = context.element?.attributes[colorAttribute] ?? - context.element?.attributes[mxColorAttribute]; - final bgColor = context.element?.attributes[bgColorAttribute]; - return TextSpan( - style: TextStyle( - color: hexToColor(colorText), - backgroundColor: hexToColor(bgColor), - ), - text: context.innerHtml, - ); - } -} - -class ImageExtension extends HtmlExtension { - final double defaultDimension; - - const ImageExtension({this.defaultDimension = 64}); - - @override - Set get supportedTags => {'img'}; - - @override - InlineSpan build(ExtensionContext context) { - final mxcUrl = Uri.tryParse(context.attributes['src'] ?? ''); - if (mxcUrl == null || mxcUrl.scheme != 'mxc') { - return TextSpan(text: context.attributes['alt']); - } - - final width = double.tryParse(context.attributes['width'] ?? ''); - final height = double.tryParse(context.attributes['height'] ?? ''); - - final actualWidth = width ?? height ?? defaultDimension; - final actualHeight = height ?? width ?? defaultDimension; - - return WidgetSpan( - child: SizedBox( - width: actualWidth, - height: actualHeight, - child: MxcImage( - uri: mxcUrl, - width: actualWidth, - height: actualHeight, - isThumbnail: (actualWidth * actualHeight) > (256 * 256), - ), - ), - ); - } -} - -class SpoilerExtension extends HtmlExtension { - final Color textColor; - - const SpoilerExtension({required this.textColor}); - - @override - Set get supportedTags => {'span'}; - - static const String customDataAttribute = 'data-mx-spoiler'; - - @override - bool matches(ExtensionContext context) { - if (context.elementName != 'span') return false; - return context.element?.attributes.containsKey(customDataAttribute) ?? - false; - } - - @override - InlineSpan build(ExtensionContext context) { - var obscure = true; - final children = context.inlineSpanChildren; - return WidgetSpan( - child: StatefulBuilder( - builder: (context, setState) { - return InkWell( - onTap: () => setState(() { - obscure = !obscure; - }), - child: RichText( - text: TextSpan( - style: obscure ? TextStyle(backgroundColor: textColor) : null, - children: children, - ), - ), - ); - }, - ), - ); - } -} - -class CodeExtension extends HtmlExtension { - final double fontSize; - - CodeExtension({required this.fontSize}); - @override - Set get supportedTags => {'code'}; - - @override - InlineSpan build(ExtensionContext context) => WidgetSpan( - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(4), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: HighlightView( - context.element?.text ?? '', - language: context.element?.className - .split(' ') - .singleWhereOrNull( - (className) => className.startsWith('language-'), - ) - ?.split('language-') - .last ?? - 'md', - theme: shadesOfPurpleTheme, - padding: EdgeInsets.symmetric( - horizontal: 6, - vertical: context.element?.parent?.localName == 'pre' ? 6 : 0, - ), - textStyle: TextStyle(fontSize: fontSize), - ), ), - ), - ); -} - -class FallbackTextExtension extends HtmlExtension { - final double fontSize; - - FallbackTextExtension({required this.fontSize}); - @override - Set get supportedTags => HtmlMessage.fallbackTextTags; - - @override - InlineSpan build(ExtensionContext context) => TextSpan( - text: context.element?.text ?? '', - style: TextStyle( - fontSize: fontSize, - ), - ); -} - -class RoomPillExtension extends HtmlExtension { - final Room room; - final BuildContext context; - final double fontSize; - final Color color; - - RoomPillExtension(this.context, this.room, this.fontSize, this.color); - @override - Set get supportedTags => {'a'}; - - @override - bool matches(ExtensionContext context) { - if (context.elementName != 'a') return false; - final userId = context.element?.attributes['href'] - ?.parseIdentifierIntoParts() - ?.primaryIdentifier; - return userId != null; - } - - static final _cachedUsers = {}; - - Future _fetchUser(String matrixId) async => - _cachedUsers[room.id + matrixId] ??= await room.requestUser(matrixId); - - @override - InlineSpan build(ExtensionContext context) { - final href = context.element?.attributes['href']; - final matrixId = href?.parseIdentifierIntoParts()?.primaryIdentifier; - if (href == null || matrixId == null) { - return TextSpan(text: context.innerHtml); - } - if (matrixId.sigil == '@') { - return WidgetSpan( - child: FutureBuilder( - future: _fetchUser(matrixId), - builder: (context, snapshot) => MatrixPill( - key: Key('user_pill_$matrixId'), - name: _cachedUsers[room.id + matrixId]?.calcDisplayname() ?? - matrixId.localpart ?? - matrixId, - avatar: _cachedUsers[room.id + matrixId]?.avatarUrl, - uri: href, - outerContext: this.context, + style: TextStyle( fontSize: fontSize, - color: color, + color: textColor, ), ), - ); - } - if (matrixId.sigil == '#' || matrixId.sigil == '!') { - final room = matrixId.sigil == '!' - ? this.room.client.getRoomById(matrixId) - : this.room.client.getRoomByAlias(matrixId); - if (room != null) { - return WidgetSpan( - child: MatrixPill( - name: room.getLocalizedDisplayname(), - avatar: room.avatar, - uri: href, - outerContext: this.context, - fontSize: fontSize, - color: color, - ), - ); - } - } - - return TextSpan(text: context.innerHtml); + ), + ); } } @@ -675,6 +628,7 @@ class MatrixPill extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( + splashColor: Colors.transparent, onTap: UrlLauncher(outerContext, uri).launchUrl, child: Row( mainAxisSize: MainAxisSize.min, @@ -700,3 +654,13 @@ class MatrixPill extends StatelessWidget { ); } } + +extension on String { + Color? get hexToColor { + var hexCode = this; + if (hexCode.startsWith('#')) hexCode = hexCode.substring(1); + if (hexCode.length == 6) hexCode = 'FF$hexCode'; + final colorValue = int.tryParse(hexCode, radix: 16); + return colorValue == null ? null : Color(colorValue); + } +} diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index a2fcf23b3..6c9a788ac 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; +import 'package:fluffychat/utils/file_description.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../widgets/blur_hash.dart'; @@ -13,12 +16,15 @@ class ImageBubble extends StatelessWidget { final BoxFit fit; final bool maxSize; final Color? backgroundColor; + final Color? textColor; + final Color? linkColor; final bool thumbnailOnly; final bool animated; final double width; final double height; final void Function()? onTap; final BorderRadius? borderRadius; + final Timeline? timeline; const ImageBubble( this.event, { @@ -32,6 +38,9 @@ class ImageBubble extends StatelessWidget { this.animated = false, this.onTap, this.borderRadius, + this.timeline, + this.textColor, + this.linkColor, super.key, }); @@ -62,6 +71,7 @@ class ImageBubble extends StatelessWidget { context: context, builder: (_) => ImageViewer( event, + timeline: timeline, outerContext: context, ), ); @@ -73,35 +83,64 @@ class ImageBubble extends StatelessWidget { final borderRadius = this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius); - return Material( - color: Colors.transparent, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: borderRadius, - side: BorderSide( - color: event.messageType == MessageTypes.Sticker - ? Colors.transparent - : theme.dividerColor, - ), - ), - child: InkWell( - onTap: () => _onTap(context), - borderRadius: borderRadius, - child: Hero( - tag: event.eventId, - child: MxcImage( - event: event, - width: width, - height: height, - fit: fit, - animated: animated, - isThumbnail: thumbnailOnly, - placeholder: event.messageType == MessageTypes.Sticker - ? null - : _buildPlaceholder, + + final fileDescription = event.fileDescription; + final textColor = this.textColor; + + return Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Material( + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + side: BorderSide( + color: event.messageType == MessageTypes.Sticker + ? Colors.transparent + : theme.dividerColor, + ), + ), + child: InkWell( + onTap: () => _onTap(context), + borderRadius: borderRadius, + child: Hero( + tag: event.eventId, + child: MxcImage( + event: event, + width: width, + height: height, + fit: fit, + animated: animated, + isThumbnail: thumbnailOnly, + placeholder: event.messageType == MessageTypes.Sticker + ? null + : _buildPlaceholder, + ), + ), ), ), - ), + if (fileDescription != null && textColor != null) + SizedBox( + width: width, + child: Linkify( + text: fileDescription, + style: TextStyle( + color: textColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: linkColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + ), + ], ); } } diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index f2215ce1b..8b517ff0b 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/toolbar/widgets/message_buttons.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -157,10 +158,19 @@ class Message extends StatelessWidget { final textColor = ownMessage ? // #Pangea - // theme.colorScheme.onPrimary + // theme.brightness == Brightness.light + // ? theme.colorScheme.onPrimary + // : theme.colorScheme.onPrimaryContainer ThemeData.dark().colorScheme.onPrimary // Pangea# : theme.colorScheme.onSurface; + + final linkColor = ownMessage + ? theme.brightness == Brightness.light + ? theme.colorScheme.primaryFixed + : theme.colorScheme.onTertiaryContainer + : theme.colorScheme.primary; + final rowMainAxisAlignment = ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start; @@ -180,6 +190,7 @@ class Message extends StatelessWidget { MessageTypes.Image, MessageTypes.Sticker, }.contains(event.messageType) && + event.fileDescription == null && !event.redacted) || (event.messageType == MessageTypes.Text && event.relationshipType == null && @@ -195,7 +206,9 @@ class Message extends StatelessWidget { color = displayEvent.status.isError ? Colors.redAccent // #Pangea - // : ThemeData.dark().colorScheme.primary; + // : theme.brightness == Brightness.light + // ? theme.colorScheme.primary + // : theme.colorScheme.primaryContainer; : Color.alphaBlend( Colors.white.withAlpha(180), ThemeData.dark().colorScheme.primary, @@ -212,10 +225,9 @@ class Message extends StatelessWidget { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { animateIn = false; // #Pangea - if (context.mounted) { - // Pangea# - setState(resetAnimateIn); - } + // setState(resetAnimateIn); + if (context.mounted) setState(resetAnimateIn); + // Pangea# }); } return AnimatedSize( @@ -248,13 +260,10 @@ class Message extends StatelessWidget { child: Material( borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), - color: selected + color: selected || highlightMarker ? theme.colorScheme.secondaryContainer - .withAlpha(100) - : highlightMarker - ? theme.colorScheme.tertiaryContainer - .withAlpha(100) - : Colors.transparent, + .withAlpha(128) + : Colors.transparent, ), ), ), @@ -520,8 +529,10 @@ class Message extends StatelessWidget { MessageContent( displayEvent, textColor: textColor, + linkColor: linkColor, onInfoTab: onInfoTab, borderRadius: borderRadius, + timeline: timeline, // #Pangea pangeaMessageEvent: pangeaMessageEvent, diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index e6805f39b..86fc68adf 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -28,6 +28,7 @@ import 'message_download_content.dart'; class MessageContent extends StatelessWidget { final Event event; final Color textColor; + final Color linkColor; final void Function(Event)? onInfoTab; final BorderRadius borderRadius; // #Pangea @@ -41,11 +42,13 @@ class MessageContent extends StatelessWidget { final Event? nextEvent; final Event? prevEvent; // Pangea# + final Timeline timeline; const MessageContent( this.event, { this.onInfoTab, super.key, + required this.timeline, required this.textColor, // #Pangea this.pangeaMessageEvent, @@ -55,6 +58,7 @@ class MessageContent extends StatelessWidget { this.nextEvent, this.prevEvent, // Pangea# + required this.linkColor, required this.borderRadius, }); @@ -194,6 +198,8 @@ class MessageContent extends StatelessWidget { height: height, fit: fit, borderRadius: borderRadius, + timeline: timeline, + textColor: textColor, ); case CuteEventContent.eventType: return CuteContent(event); @@ -208,17 +214,27 @@ class MessageContent extends StatelessWidget { return AudioPlayerWidget( event, color: textColor, + linkColor: linkColor, fontSize: fontSize, // #Pangea + isOverlay: overlayController != null, chatController: controller, // Pangea# ); } - return MessageDownloadContent(event, textColor); + return MessageDownloadContent( + event, + textColor: textColor, + linkColor: linkColor, + ); case MessageTypes.Video: - return EventVideoPlayer(event); + return EventVideoPlayer(event, textColor: textColor); case MessageTypes.File: - return MessageDownloadContent(event, textColor); + return MessageDownloadContent( + event, + textColor: textColor, + linkColor: linkColor, + ); case MessageTypes.Text: case MessageTypes.Notice: @@ -244,6 +260,15 @@ class MessageContent extends StatelessWidget { isSelected: overlayController != null ? isSelected : null, onClick: onClick, // Pangea# + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + linkStyle: TextStyle( + color: linkColor, + fontSize: + AppConfig.fontSizeFactor * AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ); } // else we fall through to the normal message rendering @@ -367,7 +392,6 @@ class MessageContent extends StatelessWidget { prevEvent: prevEvent, child: // Pangea# - Linkify( text: event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)), @@ -377,17 +401,16 @@ class MessageContent extends StatelessWidget { // style: TextStyle( // color: textColor, // fontSize: bigEmotes ? fontSize * 5 : fontSize, - // decoration: - // event.redacted ? TextDecoration.lineThrough : null, + // decoration: event.redacted ? TextDecoration.lineThrough : null, // ), style: messageTextStyle, // Pangea# options: const LinkifyOptions(humanize: false), linkStyle: TextStyle( - color: textColor.withAlpha(150), + color: linkColor, fontSize: fontSize, decoration: TextDecoration.underline, - decorationColor: textColor.withAlpha(150), + decorationColor: linkColor, ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), ), diff --git a/lib/pages/chat/events/message_download_content.dart b/lib/pages/chat/events/message_download_content.dart index 767ea8e7b..b759895ef 100644 --- a/lib/pages/chat/events/message_download_content.dart +++ b/lib/pages/chat/events/message_download_content.dart @@ -1,14 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; class MessageDownloadContent extends StatelessWidget { final Event event; final Color textColor; + final Color linkColor; - const MessageDownloadContent(this.event, this.textColor, {super.key}); + const MessageDownloadContent( + this.event, { + required this.textColor, + required this.linkColor, + super.key, + }); @override Widget build(BuildContext context) { @@ -21,59 +31,83 @@ class MessageDownloadContent extends StatelessWidget { ?.toUpperCase() ?? 'UNKNOWN'); final sizeString = event.sizeString; - return InkWell( - onTap: () => event.saveFile(context), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Icon( - Icons.file_download_outlined, - color: textColor, - ), - const SizedBox(width: 16), - Flexible( - child: Text( - filename, - maxLines: 1, - style: TextStyle( + final fileDescription = event.fileDescription; + return Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + InkWell( + onTap: () => event.saveFile(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + Icons.file_download_outlined, color: textColor, - fontWeight: FontWeight.bold, ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - const Divider(height: 1), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Row( - children: [ - Text( - filetype, - style: TextStyle( - color: textColor.withAlpha(150), - ), - ), - const Spacer(), - if (sizeString != null) - Text( - sizeString, - style: TextStyle( - color: textColor.withAlpha(150), + const SizedBox(width: 16), + Flexible( + child: Text( + filename, + maxLines: 1, + style: TextStyle( + color: textColor, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), ), - ), - ], - ), + ], + ), + ), + const Divider(height: 1), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Row( + children: [ + Text( + filetype, + style: TextStyle( + color: linkColor, + ), + ), + const Spacer(), + if (sizeString != null) + Text( + sizeString, + style: TextStyle( + color: linkColor, + ), + ), + ], + ), + ), + ], ), - ], - ), + ), + if (fileDescription != null) + Linkify( + text: fileDescription, + style: TextStyle( + color: textColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: linkColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + ], ); } } diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index de237f004..e5b7f2975 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -33,9 +33,11 @@ class ReplyContent extends StatelessWidget { final displayEvent = timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent; final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; - final color = ownMessage - ? theme.colorScheme.tertiaryContainer - : theme.colorScheme.tertiary; + final color = theme.brightness == Brightness.dark + ? theme.colorScheme.onTertiaryContainer + : ownMessage + ? theme.colorScheme.tertiaryContainer + : theme.colorScheme.tertiary; return Material( color: backgroundColor ?? @@ -80,9 +82,11 @@ class ReplyContent extends StatelessWidget { overflow: TextOverflow.ellipsis, maxLines: 1, style: TextStyle( - color: ownMessage - ? theme.colorScheme.onTertiary - : theme.colorScheme.onSurface, + color: theme.brightness == Brightness.dark + ? theme.colorScheme.onSurface + : ownMessage + ? theme.colorScheme.onTertiary + : theme.colorScheme.onSurface, fontSize: fontSize, ), ), diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index 3f2b3d0cc..ad3dfaf15 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:universal_html/html.dart' as html; @@ -12,15 +13,24 @@ import 'package:video_player/video_player.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/image_bubble.dart'; +import 'package:fluffychat/utils/file_description.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/blur_hash.dart'; import '../../../utils/error_reporter.dart'; class EventVideoPlayer extends StatefulWidget { final Event event; - const EventVideoPlayer(this.event, {super.key}); + final Color? textColor; + final Color? linkColor; + const EventVideoPlayer( + this.event, { + this.textColor, + this.linkColor, + super.key, + }); @override EventVideoPlayerState createState() => EventVideoPlayerState(); @@ -102,51 +112,86 @@ class EventVideoPlayerState extends State { final blurHash = (widget.event.infoMap as Map) .tryGet('xyz.amorgan.blurhash') ?? fallbackBlurHash; + final fileDescription = widget.event.fileDescription; + final textColor = widget.textColor; + final linkColor = widget.linkColor; + + const width = 300.0; final chewieManager = _chewieManager; - return Material( - color: Colors.black, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: SizedBox( - height: 300, - child: chewieManager != null - ? Center(child: Chewie(controller: chewieManager)) - : Stack( - children: [ - if (hasThumbnail) - Center( - child: ImageBubble( - widget.event, - tapToView: false, + return Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Material( + color: Colors.black, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: SizedBox( + height: width, + child: chewieManager != null + ? Center(child: Chewie(controller: chewieManager)) + : Stack( + children: [ + if (hasThumbnail) + Center( + child: ImageBubble( + widget.event, + tapToView: false, + textColor: widget.textColor, + ), + ) + else + BlurHash( + blurhash: blurHash, + width: width, + height: width, + ), + Center( + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.surface, + ), + icon: _isDownloading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ) + : const Icon(Icons.play_circle_outlined), + tooltip: _isDownloading + ? L10n.of(context).loadingPleaseWait + : L10n.of(context).videoWithSize( + widget.event.sizeString ?? '?MB', + ), + onPressed: _isDownloading ? null : _downloadAction, + ), ), - ) - else - BlurHash(blurhash: blurHash, width: 300, height: 300), - Center( - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.surface, - ), - icon: _isDownloading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ) - : const Icon(Icons.play_circle_outlined), - tooltip: _isDownloading - ? L10n.of(context).loadingPleaseWait - : L10n.of(context).videoWithSize( - widget.event.sizeString ?? '?MB', - ), - onPressed: _isDownloading ? null : _downloadAction, - ), + ], ), - ], + ), + ), + if (fileDescription != null && textColor != null && linkColor != null) + SizedBox( + width: width, + child: Linkify( + text: fileDescription, + style: TextStyle( + color: textColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, ), - ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: linkColor, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + decoration: TextDecoration.underline, + decorationColor: linkColor, + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + ), + ), + ], ); } } diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart index e56b34d48..c27f606fa 100644 --- a/lib/pages/chat/pinned_events.dart +++ b/lib/pages/chat/pinned_events.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; class PinnedEvents extends StatelessWidget { @@ -30,13 +30,15 @@ class PinnedEvents extends StatelessWidget { final eventId = events.length == 1 ? events.single?.eventId - : await showConfirmationDialog( + : await showModalActionPopup( context: context, - title: L10n.of(context).pinMessage, + title: L10n.of(context).pin, + cancelLabel: L10n.of(context).cancel, actions: events .map( - (event) => AlertDialogAction( - key: event?.eventId ?? '', + (event) => AdaptiveModalAction( + value: event?.eventId ?? '', + icon: const Icon(Icons.push_pin_outlined), label: event?.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)), withSenderNamePrefix: true, diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart index 077d903c0..03fe56922 100644 --- a/lib/pages/chat/recording_dialog.dart +++ b/lib/pages/chat/recording_dialog.dart @@ -206,7 +206,7 @@ class RecordingDialogState extends State { CupertinoDialogAction( onPressed: () => Navigator.of(context, rootNavigator: false).pop(), child: Text( - L10n.of(context).cancel.toUpperCase(), + L10n.of(context).cancel, style: TextStyle( color: theme.textTheme.bodyMedium?.color?.withAlpha(150), ), @@ -215,7 +215,7 @@ class RecordingDialogState extends State { if (error != true) CupertinoDialogAction( onPressed: _stopAndSend, - child: Text(L10n.of(context).send.toUpperCase()), + child: Text(L10n.of(context).send), ), ], ); @@ -226,23 +226,16 @@ class RecordingDialogState extends State { TextButton( onPressed: () => Navigator.of(context, rootNavigator: false).pop(), child: Text( - L10n.of(context).cancel.toUpperCase(), + L10n.of(context).cancel, style: TextStyle( - color: theme.textTheme.bodyMedium?.color?.withAlpha(150), + color: theme.colorScheme.error, ), ), ), if (error != true) TextButton( onPressed: _stopAndSend, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(L10n.of(context).send.toUpperCase()), - const SizedBox(width: 4), - const Icon(Icons.send_outlined, size: 15), - ], - ), + child: Text(L10n.of(context).send), ), ], ); diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index f54f84d27..43c16a0a6 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -12,9 +12,10 @@ import 'package:mime/mime.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/utils/other_party_can_receive.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/size_string.dart'; -import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import '../../utils/resize_video.dart'; class SendFileDialog extends StatefulWidget { @@ -37,17 +38,20 @@ class SendFileDialogState extends State { bool compress = true; /// Images smaller than 20kb don't need compression. - static const int minSizeToCompress = 20 * 1024; + static const int minSizeToCompress = 20 * 1000; Future _send() async { final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext); final l10n = L10n.of(context); try { + if (!widget.room.otherPartyCanReceiveMessages) { + throw OtherPartyCanNotReceiveMessages(); + } scaffoldMessenger.showLoadingSnackBar(l10n.prepareSendingAttachment); Navigator.of(context, rootNavigator: false).pop(); final clientConfig = await widget.room.client.getConfig(); - final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024; + final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1000 * 1000; for (final xfile in widget.files) { final MatrixFile file; @@ -66,6 +70,9 @@ class SendFileDialogState extends State { scaffoldMessenger.showLoadingSnackBar(l10n.generatingVideoThumbnail); thumbnail = await xfile.getVideoThumbnail(); } else { + if (length > maxUploadSize) { + throw FileTooBigMatrixException(length, maxUploadSize); + } // Else we just create a MatrixFile file = MatrixFile( bytes: await xfile.readAsBytes(), @@ -124,9 +131,15 @@ class SendFileDialogState extends State { scaffoldMessenger.clearSnackBars(); } catch (e) { scaffoldMessenger.clearSnackBars(); + final theme = Theme.of(context); scaffoldMessenger.showSnackBar( SnackBar( - content: Text(e.toLocalizedString(widget.outerContext)), + backgroundColor: theme.colorScheme.errorContainer, + closeIconColor: theme.colorScheme.onErrorContainer, + content: Text( + e.toLocalizedString(widget.outerContext), + style: TextStyle(color: theme.colorScheme.onErrorContainer), + ), duration: const Duration(seconds: 30), showCloseIcon: true, ), diff --git a/lib/pages/chat/send_location_dialog.dart b/lib/pages/chat/send_location_dialog.dart index bf14460ca..58c44db2d 100644 --- a/lib/pages/chat/send_location_dialog.dart +++ b/lib/pages/chat/send_location_dialog.dart @@ -8,7 +8,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/events/map_bubble.dart'; -import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; class SendLocationDialog extends StatefulWidget { diff --git a/lib/pages/chat_access_settings/chat_access_settings_controller.dart b/lib/pages/chat_access_settings/chat_access_settings_controller.dart index 6ea313d1e..bf6001772 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_controller.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_controller.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart' hide Visibility; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_page.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -149,14 +151,15 @@ class ChatAccessSettingsController extends State { ); final capabilities = capabilitiesResult.result; if (capabilities == null) return; - final newVersion = await showConfirmationDialog( + final newVersion = await showModalActionPopup( context: context, title: L10n.of(context).replaceRoomWithNewerVersion, + cancelLabel: L10n.of(context).cancel, actions: capabilities.mRoomVersions!.available.entries .where((r) => r.key != roomVersion) .map( - (version) => AlertDialogAction( - key: version.key, + (version) => AdaptiveModalAction( + value: version.key, label: '${version.key} (${version.value.toString().split('.').last})', ), @@ -172,7 +175,7 @@ class ChatAccessSettingsController extends State { cancelLabel: L10n.of(context).cancel, title: L10n.of(context).areYouSure, message: L10n.of(context).roomUpgradeDescription, - isDestructiveAction: true, + isDestructive: true, )) { return; } @@ -191,15 +194,11 @@ class ChatAccessSettingsController extends State { final input = await showTextInputDialog( context: context, title: L10n.of(context).editRoomAliases, - textFields: [ - DialogTextField( - prefixText: '#', - suffixText: domain, - hintText: L10n.of(context).alias, - ), - ], + prefixText: '#', + suffixText: domain, + hintText: L10n.of(context).alias, ); - final aliasLocalpart = input?.singleOrNull?.trim(); + final aliasLocalpart = input?.trim(); if (aliasLocalpart == null || aliasLocalpart.isEmpty) return; final alias = '#$aliasLocalpart:$domain'; diff --git a/lib/pages/chat_access_settings/chat_access_settings_page.dart b/lib/pages/chat_access_settings/chat_access_settings_page.dart index 34ee6c559..7bea621a7 100644 --- a/lib/pages/chat_access_settings/chat_access_settings_page.dart +++ b/lib/pages/chat_access_settings/chat_access_settings_page.dart @@ -238,8 +238,8 @@ class _AliasListTile extends StatelessWidget { 'https://matrix.to/#/$alias', context, ), - child: Text( - 'https://matrix.to/#/$alias', + child: SelectableText( + alias, style: TextStyle( decoration: TextDecoration.underline, decorationColor: theme.colorScheme.primary, diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index ebfee13b3..dca2f7db3 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; @@ -12,6 +11,8 @@ import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_details.dart'; import 'package:fluffychat/pangea/spaces/utils/set_class_name.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -54,20 +55,16 @@ class ChatDetailsController extends State { // title: L10n.of(context).changeTheNameOfTheGroup, // okLabel: L10n.of(context).ok, // cancelLabel: L10n.of(context).cancel, - // textFields: [ - // DialogTextField( - // initialText: room.getLocalizedDisplayname( - // MatrixLocals( - // L10n.of(context), - // ), - // ), + // initialText: room.getLocalizedDisplayname( + // MatrixLocals( + // L10n.of(context), // ), - // ], + // ), // ); // if (input == null) return; // final success = await showFutureLoadingDialog( // context: context, - // future: () => room.setName(input.single), + // future: () => room.setName(input), // ); // if (success.error == null) { // ScaffoldMessenger.of(context).showSnackBar( @@ -84,20 +81,16 @@ class ChatDetailsController extends State { title: L10n.of(context).setChatDescription, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - hintText: L10n.of(context).noChatDescriptionYet, - initialText: room.topic, - minLines: 4, - maxLines: 8, - ), - ], + hintText: L10n.of(context).noChatDescriptionYet, + initialText: room.topic, + minLines: 4, + maxLines: 8, ); if (input == null) return; // #Pangea await showFutureLoadingDialog( context: context, - future: () => room.setDescription(input.single), + future: () => room.setDescription(input), ); // final success = await showFutureLoadingDialog( // context: context, @@ -131,30 +124,31 @@ class ChatDetailsController extends State { final room = Matrix.of(context).client.getRoomById(roomId!); final actions = [ if (PlatformInfos.isMobile) - SheetAction( - key: AvatarAction.camera, + AdaptiveModalAction( + value: AvatarAction.camera, label: L10n.of(context).openCamera, isDefaultAction: true, - icon: Icons.camera_alt_outlined, + icon: const Icon(Icons.camera_alt_outlined), ), - SheetAction( - key: AvatarAction.file, + AdaptiveModalAction( + value: AvatarAction.file, label: L10n.of(context).openGallery, - icon: Icons.photo_outlined, + icon: const Icon(Icons.photo_outlined), ), if (room?.avatar != null) - SheetAction( - key: AvatarAction.remove, + AdaptiveModalAction( + value: AvatarAction.remove, label: L10n.of(context).delete, - isDestructiveAction: true, - icon: Icons.delete_outlined, + isDestructive: true, + icon: const Icon(Icons.delete_outlined), ), ]; final action = actions.length == 1 - ? actions.single.key - : await showModalActionSheet( + ? actions.single.value + : await showModalActionPopup( context: context, title: L10n.of(context).editRoomAvatar, + cancelLabel: L10n.of(context).cancel, actions: actions, ); if (action == null) return; diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 253c190dc..38ef2f2a5 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -5,7 +5,6 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_details/participant_list_item.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; @@ -15,6 +14,7 @@ import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/url_launcher.dart'; +import '../../widgets/qr_code_viewer.dart'; class ChatDetailsView extends StatelessWidget { final ChatDetailsController controller; @@ -57,15 +57,16 @@ class ChatDetailsView extends StatelessWidget { const Center(child: BackButton()), elevation: theme.appBarTheme.elevation, actions: [ - if (room.canonicalAlias.isNotEmpty) + if (room.canonicalAlias.isNotEmpty) ...[ IconButton( tooltip: L10n.of(context).share, - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - AppConfig.inviteLinkPrefix + room.canonicalAlias, + icon: const Icon(Icons.qr_code_rounded), + onPressed: () => showQrCodeViewer( context, + room.canonicalAlias, ), ), + ], if (controller.widget.embeddedCloseButton == null) ChatSettingsPopupMenu(room, false), ], @@ -150,6 +151,7 @@ class ChatDetailsView extends StatelessWidget { style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, + iconColor: theme.colorScheme.onSurface, ), label: Text( room.isDirectChat @@ -173,6 +175,7 @@ class ChatDetailsView extends StatelessWidget { style: TextButton.styleFrom( foregroundColor: theme.colorScheme.secondary, + iconColor: theme.colorScheme.secondary, ), label: Text( L10n.of(context).countParticipants( @@ -207,6 +210,8 @@ class ChatDetailsView extends StatelessWidget { label: Text(L10n.of(context).setChatDescription), icon: const Icon(Icons.edit_outlined), style: TextButton.styleFrom( + iconColor: + theme.colorScheme.onSecondaryContainer, backgroundColor: theme.colorScheme.secondaryContainer, foregroundColor: diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart index 35cabad91..8d0d53c76 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings_view.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'; import '../key_verification/key_verification_dialog.dart'; @@ -76,7 +76,6 @@ class ChatEncryptionSettingsController extends State { message: L10n.of(context).verifyOtherUserDescription, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - fullyCapitalizedForMaterial: false, ); if (consent != OkCancelResult.ok) return; final req = await room.client.userDeviceKeys[room.directChatMatrixID]! diff --git a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart index 238cf6f7b..0bf558958 100644 --- a/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart +++ b/lib/pages/chat_encryption_settings/chat_encryption_settings_view.dart @@ -170,7 +170,7 @@ class ChatEncryptionSettingsView extends StatelessWidget { deviceKeys[i].ed25519Key?.beautified ?? L10n.of(context).unknownEncryptionAlgorithm, style: TextStyle( - fontFamily: 'RobotoMono', + fontFamily: 'UbuntuMono', color: theme.colorScheme.secondary, ), ), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 175e614eb..13b0d8cf2 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart'; @@ -15,8 +14,6 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat/send_file_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pangea/chat_list/utils/app_version_util.dart'; import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart'; @@ -29,12 +26,17 @@ import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dar import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/show_scaffold_dialog.dart'; import 'package:fluffychat/utils/show_update_snackbar.dart'; -import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; +import '../../utils/url_launcher.dart'; import '../../utils/voip/callkeep_manager.dart'; import '../../widgets/fluffy_chat_app.dart'; import '../../widgets/matrix.dart'; @@ -42,11 +44,6 @@ import '../../widgets/matrix.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; -enum SelectMode { - normal, - share, -} - enum PopupMenuAction { settings, invite, @@ -117,12 +114,6 @@ class ChatListController extends State setState(() { _activeSpaceId = spaceId; }); - - // #Pangea - if (FluffyThemes.isColumnMode(context)) { - context.go('/rooms/$spaceId/details'); - } - // Pangea# } // #Pangea @@ -139,50 +130,6 @@ class ChatListController extends State void onChatTap(Room room) async { if (room.membership == Membership.invite) { - final inviterId = - room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId; - final inviteAction = await showModalActionSheet( - context: context, - message: room.isDirectChat - ? L10n.of(context).invitePrivateChat - // #Pangea - // : L10n.of(context).inviteGroupChat, - : L10n.of(context).inviteChat, - // Pangea# - title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - actions: [ - SheetAction( - key: InviteActions.accept, - label: L10n.of(context).accept, - icon: Icons.check_outlined, - isDefaultAction: true, - ), - SheetAction( - key: InviteActions.decline, - label: L10n.of(context).decline, - icon: Icons.close_outlined, - isDestructiveAction: true, - ), - SheetAction( - key: InviteActions.block, - label: L10n.of(context).block, - icon: Icons.block_outlined, - isDestructiveAction: true, - ), - ], - ); - if (inviteAction == null) return; - if (inviteAction == InviteActions.block) { - context.go('/rooms/settings/security/ignorelist', extra: inviterId); - return; - } - if (inviteAction == InviteActions.decline) { - await showFutureLoadingDialog( - context: context, - future: room.leave, - ); - return; - } final joinResult = await showFutureLoadingDialog( context: context, future: () async { @@ -216,42 +163,6 @@ class ChatListController extends State setActiveSpace(room.id); return; } - // Share content into this room - final shareContent = Matrix.of(context).shareContent; - if (shareContent != null) { - final shareFile = shareContent.tryGet('file'); - if (shareContent.tryGet('msgtype') == 'chat.fluffy.shared_file' && - shareFile != null) { - await showDialog( - context: context, - useRootNavigator: false, - builder: (c) => SendFileDialog( - files: [shareFile], - room: room, - outerContext: context, - ), - ); - Matrix.of(context).shareContent = null; - } else { - final consent = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).forward, - message: L10n.of(context).forwardMessageTo( - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - ), - okLabel: L10n.of(context).forward, - cancelLabel: L10n.of(context).cancel, - ); - if (consent == OkCancelResult.cancel) { - Matrix.of(context).shareContent = null; - return; - } - if (consent == OkCancelResult.ok) { - room.sendEvent(shareContent); - Matrix.of(context).shareContent = null; - } - } - } context.go('/rooms/${room.id}'); } @@ -274,16 +185,18 @@ class ChatListController extends State case ActiveFilter.groups: return (room) => !room.isSpace && - !room.isDirectChat // #Pangea + !room.isDirectChat + // #Pangea && !room.isAnalyticsRoom; - // Pangea#; + // Pangea# case ActiveFilter.unread: return (room) => - room.isUnreadOrInvited // #Pangea + room.isUnreadOrInvited + // #Pangea && !room.isAnalyticsRoom; - // Pangea#; + // Pangea# case ActiveFilter.spaces: return (room) => room.isSpace; } @@ -312,23 +225,19 @@ class ChatListController extends State context: context, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - prefixText: 'https://', - hintText: Matrix.of(context).client.homeserver?.host, - initialText: searchServer, - keyboardType: TextInputType.url, - autocorrect: false, - validator: (server) => server?.contains('.') == true - ? null - : L10n.of(context).invalidServerName, - ), - ], + prefixText: 'https://', + hintText: Matrix.of(context).client.homeserver?.host, + initialText: searchServer, + keyboardType: TextInputType.url, + autocorrect: false, + validator: (server) => server.contains('.') == true + ? null + : L10n.of(context).invalidServerName, ); if (newServer == null) return; - Matrix.of(context).store.setString(_serverStoreNamespace, newServer.single); + Matrix.of(context).store.setString(_serverStoreNamespace, newServer); setState(() { - searchServer = newServer.single; + searchServer = newServer; }); _coolDown?.cancel(); _coolDown = Timer(const Duration(milliseconds: 500), _search); @@ -463,53 +372,30 @@ class ChatListController extends State String? get activeChat => widget.activeChat; - SelectMode get selectMode => Matrix.of(context).shareContent != null - ? SelectMode.share - : SelectMode.normal; - void _processIncomingSharedMedia(List files) { if (files.isEmpty) return; - if (files.length > 1) { - Logs().w( - 'Received ${files.length} incoming shared media but app can only handle the first one', - ); - } - - // We only handle the first file currently - final sharedMedia = files.first; - - // Handle URIs and Texts, which are also passed in path - if (sharedMedia.type case SharedMediaType.text || SharedMediaType.url) { - return _processIncomingSharedText(sharedMedia.path); - } - - final file = XFile( - sharedMedia.path.replaceFirst('file://', ''), - mimeType: sharedMedia.mimeType, + showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: files.map( + (file) { + if ({ + SharedMediaType.text, + SharedMediaType.url, + }.contains(file.type)) { + return TextShareItem(file.path); + } + return FileShareItem( + XFile( + file.path.replaceFirst('file://', ''), + mimeType: file.mimeType, + ), + ); + }, + ).toList(), + ), ); - - Matrix.of(context).shareContent = { - 'msgtype': 'chat.fluffy.shared_file', - 'file': file, - if (sharedMedia.message != null) 'body': sharedMedia.message, - }; - context.go('/rooms'); - } - - void _processIncomingSharedText(String? text) { - if (text == null) return; - if (text.toLowerCase().startsWith(AppConfig.deepLinkPrefix) || - text.toLowerCase().startsWith(AppConfig.inviteLinkPrefix) || - (text.toLowerCase().startsWith(AppConfig.schemePrefix) && - !RegExp(r'\s').hasMatch(text))) { - return _processIncomingUris(text); - } - Matrix.of(context).shareContent = { - 'msgtype': 'm.text', - 'body': text, - }; - context.go('/rooms'); } void _processIncomingUris(String? text) async { @@ -573,8 +459,9 @@ class ChatListController extends State Matrix.of(context).store.getString(_serverStoreNamespace); Matrix.of(context).backgroundPush?.setupPush(); UpdateNotifier.showUpdateSnackBar(context); - + // #Pangea AppVersionUtil.showAppVersionDialog(context); + // Pangea# } // Workaround for system UI overlay style not applied on app start @@ -699,10 +586,6 @@ class ChatListController extends State BuildContext posContext, [ Room? space, ]) async { - if (room.membership == Membership.invite) { - return onChatTap(room); - } - final overlay = Overlay.of(posContext).context.findRenderObject() as RenderBox; @@ -781,114 +664,146 @@ class ChatListController extends State ], ), ), - PopupMenuItem( - value: ChatContextAction.mute, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - // #Pangea - // room.pushRuleState == PushRuleState.notify - // ? Icons.notifications_off_outlined - // : Icons.notifications_off, - room.pushRuleState == PushRuleState.notify - ? Icons.notifications_on_outlined - : Icons.notifications_off_outlined, - // Pangea# - ), - const SizedBox(width: 12), - Text( - // #Pangea - // room.pushRuleState == PushRuleState.notify - // ? L10n.of(context).muteChat - // : L10n.of(context).unmuteChat, - room.pushRuleState == PushRuleState.notify - ? L10n.of(context).notificationsOn - : L10n.of(context).notificationsOff, - // Pangea# - ), - ], - ), - ), - PopupMenuItem( - value: ChatContextAction.markUnread, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - room.markedUnread - ? Icons.mark_as_unread - : Icons.mark_as_unread_outlined, - ), - const SizedBox(width: 12), - Text( - room.markedUnread - ? L10n.of(context).markAsRead - : L10n.of(context).markAsUnread, - ), - ], - ), - ), - PopupMenuItem( - value: ChatContextAction.favorite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined), - const SizedBox(width: 12), - Text( - room.isFavourite - ? L10n.of(context).unpin - : L10n.of(context).pin, - ), - ], - ), - ), - if (spacesWithPowerLevels.isNotEmpty - // #Pangea - && - !room.isSpace - // Pangea# - ) + if (room.membership == Membership.join) ...[ PopupMenuItem( - value: ChatContextAction.addToSpace, + value: ChatContextAction.mute, child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.group_work_outlined), + Icon( + // #Pangea + // room.pushRuleState == PushRuleState.notify + // ? Icons.notifications_off_outlined + // : Icons.notifications_off, + room.pushRuleState == PushRuleState.notify + ? Icons.notifications_on_outlined + : Icons.notifications_off_outlined, + // Pangea# + ), const SizedBox(width: 12), - Text(L10n.of(context).addToSpace), + Text( + // #Pangea + // room.pushRuleState == PushRuleState.notify + // ? L10n.of(context).muteChat + // : L10n.of(context).unmuteChat, + room.pushRuleState == PushRuleState.notify + ? L10n.of(context).notificationsOn + : L10n.of(context).notificationsOff, + // Pangea# + ), ], ), ), - // #Pangea - // if the room has a parent for which the user has a high enough power level - // to set parent's space child events, show option to remove the room from the space - if (room.spaceParents.isNotEmpty && - room.pangeaSpaceParents.any( - (r) => r.canChangeStateEvent(EventTypes.SpaceChild), - ) && - activeSpaceId != null) PopupMenuItem( - value: ChatContextAction.removeFromSpace, + value: ChatContextAction.markUnread, child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.delete_sweep_outlined), + Icon( + room.markedUnread + ? Icons.mark_as_unread + : Icons.mark_as_unread_outlined, + ), const SizedBox(width: 12), - Text(L10n.of(context).removeFromSpace), + Text( + room.markedUnread + ? L10n.of(context).markAsRead + : L10n.of(context).markAsUnread, + ), + ], + ), + ), + PopupMenuItem( + value: ChatContextAction.favorite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, + ), + const SizedBox(width: 12), + Text( + room.isFavourite + ? L10n.of(context).unpin + : L10n.of(context).pin, + ), + ], + ), + ), + if (spacesWithPowerLevels.isNotEmpty + // #Pangea + && + !room.isSpace + // Pangea# + ) + PopupMenuItem( + value: ChatContextAction.addToSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.group_work_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).addToSpace), + ], + ), + ), + // #Pangea + // if the room has a parent for which the user has a high enough power level + // to set parent's space child events, show option to remove the room from the space + if (room.spaceParents.isNotEmpty && + room.pangeaSpaceParents.any( + (r) => r.canChangeStateEvent(EventTypes.SpaceChild), + ) && + activeSpaceId != null) + PopupMenuItem( + value: ChatContextAction.removeFromSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.delete_sweep_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).removeFromSpace), + ], + ), + ), + // Pangea# + ], + if (room.membership == Membership.invite) + PopupMenuItem( + value: ChatContextAction.block, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.block_outlined, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 12), + Text( + L10n.of(context).block, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), ], ), ), - // Pangea# PopupMenuItem( value: ChatContextAction.leave, child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.delete_outlined), + Icon( + Icons.delete_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), const SizedBox(width: 12), - Text(L10n.of(context).leave), + Text( + room.membership == Membership.invite + ? L10n.of(context).delete + : L10n.of(context).leave, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), ], ), ), @@ -932,15 +847,15 @@ class ChatListController extends State useRootNavigator: false, context: context, title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).leave, - cancelLabel: L10n.of(context).no, // #Pangea // message: L10n.of(context).archiveRoomDescription, message: room.isSpace ? L10n.of(context).leaveSpaceDescription : L10n.of(context).archiveRoomDescription, // Pangea# - isDestructiveAction: true, + okLabel: L10n.of(context).leave, + cancelLabel: L10n.of(context).cancel, + isDestructive: true, ); if (confirmed == OkCancelResult.cancel) return; if (!mounted) return; @@ -955,13 +870,13 @@ class ChatListController extends State return; case ChatContextAction.addToSpace: - final space = await showConfirmationDialog( + final space = await showModalActionPopup( context: context, title: L10n.of(context).space, actions: spacesWithPowerLevels .map( - (space) => AlertDialogAction( - key: space, + (space) => AdaptiveModalAction( + value: space, label: space .getLocalizedDisplayname(MatrixLocals(L10n.of(context))), ), @@ -1009,6 +924,10 @@ class ChatListController extends State ); return; // Pangea# + case ChatContextAction.block: + final userId = + room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId; + context.go('/rooms/settings/security/ignorelist', extra: userId); } } @@ -1050,15 +969,11 @@ class ChatListController extends State message: L10n.of(context).leaveEmptyToClearStatus, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - hintText: L10n.of(context).statusExampleMessage, - maxLines: 6, - minLines: 1, - maxLength: 255, - initialText: currentPresence.statusMsg, - ), - ], + hintText: L10n.of(context).statusExampleMessage, + maxLines: 6, + minLines: 1, + maxLength: 255, + initialText: currentPresence.statusMsg, ); if (input == null) return; if (!mounted) return; @@ -1067,7 +982,7 @@ class ChatListController extends State future: () => client.setPresence( client.userID!, PresenceType.online, - statusMsg: input.single, + statusMsg: input, ), ); } @@ -1118,7 +1033,9 @@ class ChatListController extends State // controller = ScaffoldMessenger.of(context).showSnackBar( // SnackBar( // duration: const Duration(seconds: 15), + // showCloseIcon: true, // backgroundColor: theme.colorScheme.errorContainer, + // closeIconColor: theme.colorScheme.onErrorContainer, // content: Text( // L10n.of(context).oneOfYourDevicesIsNotVerified, // style: TextStyle( @@ -1149,12 +1066,6 @@ class ChatListController extends State } // Pangea# - void cancelAction() { - if (selectMode == SelectMode.share) { - setState(() => Matrix.of(context).shareContent = null); - } - } - void setActiveFilter(ActiveFilter filter) { setState(() { activeFilter = filter; @@ -1190,17 +1101,18 @@ class ChatListController extends State final client = Matrix.of(context) .widget .clients[Matrix.of(context).getClientIndexByMatrixId(userId!)]; - final action = await showConfirmationDialog( + final action = await showModalActionPopup( context: context, title: L10n.of(context).editBundlesForAccount, + cancelLabel: L10n.of(context).cancel, actions: [ - AlertDialogAction( - key: EditBundleAction.addToBundle, + AdaptiveModalAction( + value: EditBundleAction.addToBundle, label: L10n.of(context).addToBundle, ), if (activeBundle != client.userID) - AlertDialogAction( - key: EditBundleAction.removeFromBundle, + AdaptiveModalAction( + value: EditBundleAction.removeFromBundle, label: L10n.of(context).removeFromBundle, ), ], @@ -1211,12 +1123,12 @@ class ChatListController extends State final bundle = await showTextInputDialog( context: context, title: l10n.bundleName, - textFields: [DialogTextField(hintText: l10n.bundleName)], + hintText: l10n.bundleName, ); - if (bundle == null || bundle.isEmpty || bundle.single.isEmpty) return; + if (bundle == null || bundle.isEmpty || bundle.isEmpty) return; await showFutureLoadingDialog( context: context, - future: () => client.setAccountBundle(bundle.single), + future: () => client.setAccountBundle(bundle), ); break; case EditBundleAction.removeFromBundle: @@ -1282,6 +1194,7 @@ enum ChatContextAction { mute, leave, addToSpace, + block, // #Pangea removeFromSpace, // Pangea# diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index b54c02ca2..55a996625 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -18,7 +18,6 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import '../../config/themes.dart'; -import '../../widgets/connection_status_header.dart'; import '../../widgets/matrix.dart'; class ChatListViewBody extends StatelessWidget { @@ -152,7 +151,6 @@ class ChatListViewBody extends StatelessWidget { // ), // ), // Pangea# - const ConnectionStatusHeader(), AnimatedContainer( height: controller.isTorBrowser ? 64 : 0, duration: FluffyThemes.animationDuration, diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index d201fce88..354fe1bb6 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; -import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; +import 'package:fluffychat/utils/sync_status_localization.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { final ChatListController controller; @@ -20,44 +22,24 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - - final selectMode = controller.selectMode; + final client = Matrix.of(context).client; return SliverAppBar( floating: true, - // #Pangea - // toolbarHeight: 72, - toolbarHeight: controller.isSearchMode ? 72 : 175, - // Pangea# - pinned: - FluffyThemes.isColumnMode(context) || selectMode != SelectMode.normal, - scrolledUnderElevation: selectMode == SelectMode.normal ? 0 : null, - // #Pangea - // backgroundColor: - // selectMode == SelectMode.normal ? Colors.transparent : null, - // Pangea# + toolbarHeight: 72, + pinned: FluffyThemes.isColumnMode(context), + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, automaticallyImplyLeading: false, - // #Pangea - // leading: selectMode == SelectMode.normal - // ? null - // : IconButton( - // tooltip: L10n.of(context).cancel, - // icon: const Icon(Icons.close_outlined), - // onPressed: controller.cancelAction, - // color: theme.colorScheme.primary, - // ), - // Pangea# - title: - // #Pangea - // selectMode == SelectMode.share - // ? Text( - // L10n.of(context).share, - // key: const ValueKey(SelectMode.share), - // ) - Column( - children: [ - // Pangea# - TextField( + title: StreamBuilder( + stream: client.onSyncStatus.stream, + builder: (context, snapshot) { + final status = client.onSyncStatus.value ?? + const SyncStatusUpdate(SyncStatus.waitingForResponse); + final hide = client.onSync.value != null && + status.status != SyncStatus.error && + client.prevBatch != null; + return TextField( controller: controller.searchController, focusNode: controller.searchFocusNode, textInputAction: TextInputAction.search, @@ -73,25 +55,36 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { borderRadius: BorderRadius.circular(99), ), contentPadding: EdgeInsets.zero, - hintText: L10n.of(context).searchChatsRooms, + hintText: hide + ? L10n.of(context).searchChatsRooms + : status.calcLocalizedString(context), hintStyle: TextStyle( - color: theme.colorScheme.onPrimaryContainer, + color: status.error != null + ? theme.colorScheme.error + : theme.colorScheme.onPrimaryContainer, fontWeight: FontWeight.normal, ), - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: controller.isSearchMode - ? IconButton( - tooltip: L10n.of(context).cancel, - icon: const Icon(Icons.close_outlined), - onPressed: controller.cancelSearch, - color: theme.colorScheme.onPrimaryContainer, - ) - : IconButton( - onPressed: controller.startSearch, - icon: Icon( - Icons.search_outlined, - color: theme.colorScheme.onPrimaryContainer, - ), + prefixIcon: hide + ? controller.isSearchMode + ? IconButton( + tooltip: L10n.of(context).cancel, + icon: const Icon(Icons.close_outlined), + onPressed: controller.cancelSearch, + color: theme.colorScheme.onPrimaryContainer, + ) + : IconButton( + onPressed: controller.startSearch, + icon: Icon( + Icons.search_outlined, + color: theme.colorScheme.onPrimaryContainer, + ), + ) + : Icon( + status.icon, + color: status.error != null + ? theme.colorScheme.error + : theme.colorScheme.onPrimaryContainer, + size: 18, ), suffixIcon: controller.isSearchMode && globalSearch ? controller.isSearching @@ -107,59 +100,33 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget { ), ), ) - // #Pangea - : const SizedBox( - width: 0, - child: ClientChooserButton(), + : TextButton.icon( + onPressed: controller.setServer, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(99), + ), + textStyle: const TextStyle(fontSize: 12), + ), + icon: const Icon(Icons.edit_outlined, size: 16), + label: Text( + controller.searchServer ?? + Matrix.of(context).client.homeserver!.host, + maxLines: 2, + ), ) - // : TextButton.icon( - // onPressed: controller.setServer, - // style: TextButton.styleFrom( - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(99), - // ), - // textStyle: const TextStyle(fontSize: 12), - // ), - // icon: const Icon(Icons.edit_outlined, size: 16), - // label: Text( - // controller.searchServer ?? - // Matrix.of(context).client.homeserver!.host, - // maxLines: 2, - // ), - // ) - // #Pangea : const SizedBox( width: 0, child: ClientChooserButton( // #Pangea - // controller + // controller, // Pangea# ), ), ), - ), - // #Pangea - if (!controller.isSearchMode) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: LearningProgressIndicators(), - ), - // Pangea# - ], + ); + }, ), - // #Pangea - // actions: selectMode == SelectMode.share - // ? [ - // Padding( - // padding: const EdgeInsets.symmetric( - // horizontal: 16.0, - // vertical: 8.0, - // ), - // child: ClientChooserButton(controller), - // ), - // ] - // : null, - // Pangea# ); } diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 4b676a13c..0519258e6 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -9,6 +8,7 @@ import 'package:fluffychat/pangea/chat_list/utils/get_chat_list_item_subtitle.da import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/room_status_extension.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/hover_builder.dart'; import '../../config/themes.dart'; @@ -58,7 +58,7 @@ class ChatListItem extends StatelessWidget { ? L10n.of(context).leaveSpaceDescription : L10n.of(context).archiveRoomDescription, // Pangea# - isDestructiveAction: true, + isDestructive: true, ); if (confirmed != OkCancelResult.ok) return false; final leaveResult = await showFutureLoadingDialog( @@ -339,7 +339,7 @@ class ChatListItem extends StatelessWidget { // Pangea# : FutureBuilder( key: ValueKey( - '${lastEvent?.eventId}_${lastEvent?.type}', + '${lastEvent?.eventId}_${lastEvent?.type}_${lastEvent?.redacted}', ), future: needLastEventSender ? lastEvent.calcLocalizedBody( diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 9eeba9a32..599d936f5 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -22,133 +22,119 @@ class ChatListView extends StatelessWidget { @override Widget build(BuildContext context) { final client = Matrix.of(context).client; - return StreamBuilder( - stream: Matrix.of(context).onShareContentChanged.stream, - builder: (_, __) { - final selectMode = controller.selectMode; - return PopScope( - canPop: controller.selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeSpaceId == null, - onPopInvokedWithResult: (pop, _) { - if (pop) return; - if (controller.activeSpaceId != null) { - controller.clearActiveSpace(); - return; - } - final selMode = controller.selectMode; - if (controller.isSearchMode) { - controller.cancelSearch(); - return; - } - if (selMode != SelectMode.normal) { - controller.cancelAction(); - return; - } - }, - child: Row( - children: [ - if (FluffyThemes.isColumnMode(context) && - controller.widget.displayNavigationRail) ...[ - StreamBuilder( - key: ValueKey( - client.userID.toString(), - ), - stream: client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, _) { - final allSpaces = Matrix.of(context) - .client - .rooms - .where((room) => room.isSpace); - final rootSpaces = allSpaces - .where( - (space) => !allSpaces.any( - (parentSpace) => parentSpace.spaceChildren - .any((child) => child.roomId == space.id), - ), - ) - .toList(); - - return SizedBox( - width: FluffyThemes.navRailWidth, - child: ListView.builder( - scrollDirection: Axis.vertical, - itemCount: rootSpaces.length + 2, - itemBuilder: (context, i) { - if (i == 0) { - return NaviRailItem( - isSelected: controller.activeSpaceId == null, - onTap: controller.clearActiveSpace, - icon: const Icon(Icons.forum_outlined), - selectedIcon: const Icon(Icons.forum), - toolTip: L10n.of(context).chats, - unreadBadgeFilter: (room) => true, - ); - } - i--; - if (i == rootSpaces.length) { - return NaviRailItem( - isSelected: false, - onTap: () => context.go('/rooms/newspace'), - icon: const Icon(Icons.add), - toolTip: L10n.of(context).createNewSpace, - ); - } - final space = rootSpaces[i]; - final displayname = - rootSpaces[i].getLocalizedDisplayname( - MatrixLocals(L10n.of(context)), - ); - final spaceChildrenIds = - space.spaceChildren.map((c) => c.roomId).toSet(); - return NaviRailItem( - toolTip: displayname, - isSelected: controller.activeSpaceId == space.id, - onTap: () => - controller.setActiveSpace(rootSpaces[i].id), - unreadBadgeFilter: (room) => - spaceChildrenIds.contains(room.id), - icon: Avatar( - mxContent: rootSpaces[i].avatar, - name: displayname, - // #Pangea - presenceUserId: space.directChatMatrixID, - // Pangea# - size: 32, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - ), - ); - }, + return PopScope( + canPop: !controller.isSearchMode && controller.activeSpaceId == null, + onPopInvokedWithResult: (pop, _) { + if (pop) return; + if (controller.activeSpaceId != null) { + controller.clearActiveSpace(); + return; + } + if (controller.isSearchMode) { + controller.cancelSearch(); + return; + } + }, + child: Row( + children: [ + if (FluffyThemes.isColumnMode(context) && + controller.widget.displayNavigationRail) ...[ + StreamBuilder( + key: ValueKey( + client.userID.toString(), + ), + stream: client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) { + final allSpaces = Matrix.of(context) + .client + .rooms + .where((room) => room.isSpace); + final rootSpaces = allSpaces + .where( + (space) => !allSpaces.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), ), - ); - }, - ), - Container( - color: Theme.of(context).dividerColor, - width: 1, - ), - ], - Expanded( - child: GestureDetector( - onTap: FocusManager.instance.primaryFocus?.unfocus, - excludeFromSemantics: true, - behavior: HitTestBehavior.translucent, - child: Scaffold( - // #Pangea - // body: ChatListViewBody(controller), - body: ChatListViewBodyWrapper(controller: controller), - // Pangea# - floatingActionButton: selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeSpaceId == null + ) + .toList(); + + return SizedBox( + width: FluffyThemes.navRailWidth, + child: ListView.builder( + scrollDirection: Axis.vertical, + itemCount: rootSpaces.length + 2, + itemBuilder: (context, i) { + if (i == 0) { + return NaviRailItem( + isSelected: controller.activeSpaceId == null, + onTap: controller.clearActiveSpace, + icon: const Icon(Icons.forum_outlined), + selectedIcon: const Icon(Icons.forum), + toolTip: L10n.of(context).chats, + unreadBadgeFilter: (room) => true, + ); + } + i--; + if (i == rootSpaces.length) { + return NaviRailItem( + isSelected: false, + onTap: () => context.go('/rooms/newspace'), + icon: const Icon(Icons.add), + toolTip: L10n.of(context).createNewSpace, + ); + } + final space = rootSpaces[i]; + final displayname = rootSpaces[i].getLocalizedDisplayname( + MatrixLocals(L10n.of(context)), + ); + final spaceChildrenIds = + space.spaceChildren.map((c) => c.roomId).toSet(); + return NaviRailItem( + toolTip: displayname, + isSelected: controller.activeSpaceId == space.id, + onTap: () => + controller.setActiveSpace(rootSpaces[i].id), + unreadBadgeFilter: (room) => + spaceChildrenIds.contains(room.id), + icon: Avatar( + mxContent: rootSpaces[i].avatar, + name: displayname, + // #Pangea + presenceUserId: space.directChatMatrixID, + // Pangea# + size: 32, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + ), + ); + }, + ), + ); + }, + ), + Container( + color: Theme.of(context).dividerColor, + width: 1, + ), + ], + Expanded( + child: GestureDetector( + onTap: FocusManager.instance.primaryFocus?.unfocus, + excludeFromSemantics: true, + behavior: HitTestBehavior.translucent, + child: Scaffold( + // #Pangea + // body: ChatListViewBody(controller), + body: ChatListViewBodyWrapper(controller: controller), + // Pangea# + floatingActionButton: + !controller.isSearchMode && controller.activeSpaceId == null ? FloatingActionButton.extended( // #Pangea - // onPressed: () => - // context.go('/rooms/newprivatechat'), + // onPressed: () => context.go('/rooms/newprivatechat'), onPressed: () => context.go('/rooms/newgroup'), // Pangea# icon: const Icon(Icons.add_outlined), @@ -158,13 +144,11 @@ class ChatListView extends StatelessWidget { ), ) : const SizedBox.shrink(), - ), - ), ), - ], + ), ), - ); - }, + ], + ), ); } } diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 3723890e7..8deaa4c65 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -8,6 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart'; import 'package:fluffychat/pangea/spaces/utils/space_code.dart'; import 'package:fluffychat/pangea/user/utils/logout.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pages/chat_list/nav_rail_item.dart b/lib/pages/chat_list/nav_rail_item.dart deleted file mode 100644 index 6264b0b18..000000000 --- a/lib/pages/chat_list/nav_rail_item.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import '../../config/themes.dart'; - -class NaviRailItem extends StatefulWidget { - final String toolTip; - final bool isSelected; - final void Function() onTap; - final Widget icon; - final Widget? selectedIcon; - - const NaviRailItem({ - required this.toolTip, - required this.isSelected, - required this.onTap, - required this.icon, - this.selectedIcon, - super.key, - }); - - @override - State createState() => _NaviRailItemState(); -} - -class _NaviRailItemState extends State { - bool _hovered = false; - - void _onHover(bool hover) { - if (hover == _hovered) return; - setState(() { - _hovered = hover; - }); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final borderRadius = BorderRadius.circular(AppConfig.borderRadius); - return SizedBox( - height: 64, - width: 64, - child: Stack( - children: [ - Positioned( - top: 16, - bottom: 16, - left: 0, - child: AnimatedContainer( - width: widget.isSelected ? 4 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: const BorderRadius.only( - topRight: Radius.circular(90), - bottomRight: Radius.circular(90), - ), - ), - ), - ), - Center( - child: AnimatedScale( - scale: _hovered ? 1.2 : 1.0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: Material( - borderRadius: borderRadius, - color: widget.isSelected - ? theme.colorScheme.primaryContainer - : theme.colorScheme.surface, - child: Tooltip( - message: widget.toolTip, - child: InkWell( - borderRadius: borderRadius, - onTap: widget.onTap, - onHover: _onHover, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 8.0, - ), - child: widget.isSelected - ? widget.selectedIcon ?? widget.icon - : widget.icon, - ), - ), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 29c337f31..68eacc2de 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; @@ -19,6 +18,7 @@ import 'package:fluffychat/pangea/spaces/widgets/add_room_dialog.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -305,6 +305,7 @@ class _SpaceViewState extends State { // message: L10n.of(context).archiveRoomDescription, message: L10n.of(context).leaveSpaceDescription, // Pangea# + isDestructive: true, ); if (!mounted) return; if (confirmed != OkCancelResult.ok) return; @@ -324,23 +325,17 @@ class _SpaceViewState extends State { void _addChatOrSubspace() async { // #Pangea - // final roomType = await showConfirmationDialog( + // final roomType = await showModalActionPopup( // context: context, // title: L10n.of(context).addChatOrSubSpace, // actions: [ - // AlertDialogAction( - // key: AddRoomType.subspace, - // // #Pangea - // // label: L10n.of(context).createNewSpace, - // label: L10n.of(context).newSpace, - // // Pangea# + // AdaptiveModalAction( + // value: AddRoomType.subspace, + // label: L10n.of(context).createNewSpace, // ), - // AlertDialogAction( - // key: AddRoomType.chat, - // // #Pangea - // // label: L10n.of(context).createGroup, - // label: L10n.of(context).newChat, - // // Pangea# + // AdaptiveModalAction( + // value: AddRoomType.chat, + // label: L10n.of(context).createGroup, // ), // ], // ); @@ -361,28 +356,18 @@ class _SpaceViewState extends State { // title: roomType == AddRoomType.subspace // ? L10n.of(context).createNewSpace // : L10n.of(context).createGroup, - // textFields: [ - // DialogTextField( - // hintText: roomType == AddRoomType.subspace - // ? L10n.of(context).spaceName - // : L10n.of(context).groupName, - // minLines: 1, - // maxLines: 1, - // maxLength: 64, - // validator: (text) { - // if (text == null || text.isEmpty) { - // return L10n.of(context).pleaseChoose; - // } - // return null; - // }, - // ), - // DialogTextField( - // hintText: L10n.of(context).chatDescription, - // minLines: 4, - // maxLines: 8, - // maxLength: 255, - // ), - // ], + // hintText: roomType == AddRoomType.subspace + // ? L10n.of(context).spaceName + // : L10n.of(context).groupName, + // minLines: 1, + // maxLines: 1, + // maxLength: 64, + // validator: (text) { + // if (text.isEmpty) { + // return L10n.of(context).pleaseChoose; + // } + // return null; + // }, // okLabel: L10n.of(context).create, // cancelLabel: L10n.of(context).cancel, // ); @@ -399,8 +384,7 @@ class _SpaceViewState extends State { // #Pangea // if (roomType == AddRoomType.subspace) { // roomId = await client.createSpace( - // name: names.first, - // topic: names.last.isEmpty ? null : names.last, + // name: names, // visibility: activeSpace.joinRules == JoinRules.public // ? sdk.Visibility.public // : sdk.Visibility.private, @@ -409,21 +393,13 @@ class _SpaceViewState extends State { // Pangea# roomId = await client.createGroupChat( // #Pangea - // groupName: names.first, + // groupName: names, // preset: activeSpace.joinRules == JoinRules.public // ? CreateRoomPreset.publicChat // : CreateRoomPreset.privateChat, // visibility: activeSpace.joinRules == JoinRules.public // ? sdk.Visibility.public // : sdk.Visibility.private, - // initialState: names.length > 1 && names.last.isNotEmpty - // ? [ - // StateEvent( - // type: EventTypes.RoomTopic, - // content: {'topic': names.last}, - // ), - // ] - // : null, groupName: response.roomName, preset: response.joinRules == sdk.JoinRules.public ? CreateRoomPreset.publicChat @@ -440,6 +416,7 @@ class _SpaceViewState extends State { enableEncryption: false, // Pangea# ); + // } await activeSpace.setSpaceChild(roomId); }, ); diff --git a/lib/pages/device_settings/device_settings.dart b/lib/pages/device_settings/device_settings.dart index e74cfacfd..566583f5e 100644 --- a/lib/pages/device_settings/device_settings.dart +++ b/lib/pages/device_settings/device_settings.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/encryption/utils/key_verification.dart'; @@ -8,6 +7,8 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/device_settings/device_settings_view.dart'; import 'package:fluffychat/pages/key_verification/key_verification_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; @@ -54,9 +55,10 @@ class DevicesSettingsController extends State { if (await showOkCancelAlertDialog( context: context, title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).yes, + okLabel: L10n.of(context).remove, cancelLabel: L10n.of(context).cancel, message: L10n.of(context).removeDevicesDescription, + isDestructive: true, ) == OkCancelResult.cancel) { return; @@ -86,18 +88,14 @@ class DevicesSettingsController extends State { title: L10n.of(context).changeDeviceName, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - hintText: device.displayName, - ), - ], + hintText: device.displayName, ); if (displayName == null) return; final success = await showFutureLoadingDialog( context: context, future: () => Matrix.of(context) .client - .updateDevice(device.deviceId, displayName: displayName.single), + .updateDevice(device.deviceId, displayName: displayName), ); if (success.error == null) { reload(); @@ -111,7 +109,6 @@ class DevicesSettingsController extends State { message: L10n.of(context).verifyOtherDeviceDescription, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - fullyCapitalizedForMaterial: false, ); if (consent != OkCancelResult.ok) return; final req = await Matrix.of(context) diff --git a/lib/pages/device_settings/device_settings_view.dart b/lib/pages/device_settings/device_settings_view.dart index 01880512c..57fb87cad 100644 --- a/lib/pages/device_settings/device_settings_view.dart +++ b/lib/pages/device_settings/device_settings_view.dart @@ -99,6 +99,7 @@ class DevicesSettingsView extends StatelessWidget { L10n.of(context).removeAllOtherDevices, ), style: TextButton.styleFrom( + iconColor: theme.colorScheme.onErrorContainer, foregroundColor: theme.colorScheme.onErrorContainer, backgroundColor: diff --git a/lib/pages/device_settings/user_device_list_item.dart b/lib/pages/device_settings/user_device_list_item.dart index 96be6c787..5c9e80329 100644 --- a/lib/pages/device_settings/user_device_list_item.dart +++ b/lib/pages/device_settings/user_device_list_item.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import '../../utils/date_time_extension.dart'; import '../../utils/matrix_sdk_extensions/device_extension.dart'; import '../../widgets/matrix.dart'; @@ -49,37 +49,43 @@ class UserDeviceListItem extends StatelessWidget { clipBehavior: Clip.hardEdge, child: ListTile( onTap: () async { - final action = await showModalActionSheet( + final action = await showModalActionPopup( context: context, title: '${userDevice.displayName} (${userDevice.deviceId})', + cancelLabel: L10n.of(context).cancel, actions: [ - SheetAction( - key: UserDeviceListItemAction.rename, + AdaptiveModalAction( + value: UserDeviceListItemAction.rename, + icon: const Icon(Icons.edit_outlined), label: L10n.of(context).changeDeviceName, ), if (!isOwnDevice && keys != null) ...{ - SheetAction( - key: UserDeviceListItemAction.verify, + AdaptiveModalAction( + value: UserDeviceListItemAction.verify, + icon: const Icon(Icons.verified_outlined), label: L10n.of(context).verifyStart, ), if (!keys.blocked) - SheetAction( - key: UserDeviceListItemAction.block, + AdaptiveModalAction( + value: UserDeviceListItemAction.block, + icon: const Icon(Icons.block_outlined), label: L10n.of(context).blockDevice, - isDestructiveAction: true, + isDestructive: true, ), if (keys.blocked) - SheetAction( - key: UserDeviceListItemAction.unblock, + AdaptiveModalAction( + value: UserDeviceListItemAction.unblock, + icon: const Icon(Icons.block), label: L10n.of(context).unblockDevice, - isDestructiveAction: true, + isDestructive: true, ), }, if (!isOwnDevice) - SheetAction( - key: UserDeviceListItemAction.remove, + AdaptiveModalAction( + value: UserDeviceListItemAction.remove, + icon: const Icon(Icons.delete_outlined), label: L10n.of(context).delete, - isDestructiveAction: true, + isDestructive: true, ), ], ); diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index a50083f7a..d8c584a98 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; @@ -17,6 +16,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; @@ -33,7 +33,6 @@ class HomeserverPicker extends StatefulWidget { class HomeserverPickerController extends State { bool isLoading = false; - bool isLoggingIn = false; final TextEditingController homeserverController = TextEditingController( text: AppConfig.defaultHomeserver, @@ -61,58 +60,29 @@ class HomeserverPickerController extends State { isTorBrowser = isTor; } - String? _lastCheckedUrl; - - Timer? _checkHomeserverCooldown; - - tryCheckHomeserverActionWithCooldown([_]) { - _checkHomeserverCooldown?.cancel(); - _checkHomeserverCooldown = Timer( - const Duration(milliseconds: 500), - checkHomeserverAction, - ); - } - - void tryCheckHomeserverActionWithoutCooldown([_]) { - _checkHomeserverCooldown?.cancel(); - _lastCheckedUrl = null; - checkHomeserverAction(); - } - - void onSubmitted([_]) { - if (isLoading || _checkHomeserverCooldown?.isActive == true) { - return tryCheckHomeserverActionWithoutCooldown(); - } - if (supportsSso) return ssoLoginAction(); - if (supportsPasswordLogin) return login(); - return tryCheckHomeserverActionWithoutCooldown(); - } - /// Starts an analysis of the given homeserver. It uses the current domain and /// makes sure that it is prefixed with https. Then it searches for the /// well-known information and forwards to the login page depending on the /// login type. - Future checkHomeserverAction([_]) async { + Future checkHomeserverAction({bool legacyPasswordLogin = false}) async { final homeserverInput = homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); - if (homeserverInput.isEmpty || !homeserverInput.contains('.')) { + if (homeserverInput.isEmpty) { setState(() { error = loginFlows = null; isLoading = false; Matrix.of(context).getLoginClient().homeserver = null; - _lastCheckedUrl = null; }); return; } - if (_lastCheckedUrl == homeserverInput) return; - - _lastCheckedUrl = homeserverInput; setState(() { error = loginFlows = null; isLoading = true; }); + final l10n = L10n.of(context); + try { var homeserver = Uri.parse(homeserverInput); if (homeserver.scheme.isEmpty) { @@ -121,6 +91,21 @@ class HomeserverPickerController extends State { final client = Matrix.of(context).getLoginClient(); final (_, _, loginFlows) = await client.checkHomeserver(homeserver); this.loginFlows = loginFlows; + if (supportsSso && !legacyPasswordLogin) { + if (!PlatformInfos.isMobile) { + final consent = await showOkCancelAlertDialog( + context: context, + title: l10n.appWantsToUseForLogin(homeserverInput), + message: l10n.appWantsToUseForLoginDescription, + okLabel: l10n.continueText, + ); + if (consent != OkCancelResult.ok) return; + } + return ssoLoginAction(); + } + context.push( + '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', + ); } catch (e) { setState( () => error = (e).toLocalizedString( @@ -176,7 +161,7 @@ class HomeserverPickerController extends State { setState(() { error = null; - isLoading = isLoggingIn = true; + isLoading = true; }); try { await Matrix.of(context).getLoginClient().login( @@ -191,27 +176,16 @@ class HomeserverPickerController extends State { } finally { if (mounted) { setState(() { - isLoading = isLoggingIn = false; + isLoading = false; }); } } } - void login() async { - if (!supportsPasswordLogin) { - homeserverController.text = AppConfig.defaultHomeserver; - await checkHomeserverAction(); - } - context.push( - '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', - ); - } - @override void initState() { _checkTorBrowser(); super.initState(); - WidgetsBinding.instance.addPostFrameCallback(checkHomeserverAction); } @override @@ -223,7 +197,7 @@ class HomeserverPickerController extends State { if (file == null) return; setState(() { error = null; - isLoading = isLoggingIn = true; + isLoading = true; }); try { final client = Matrix.of(context).getLoginClient(); @@ -236,7 +210,7 @@ class HomeserverPickerController extends State { } finally { if (mounted) { setState(() { - isLoading = isLoggingIn = false; + isLoading = false; }); } } @@ -245,7 +219,7 @@ class HomeserverPickerController extends State { void onMoreAction(MoreLoginActions action) { switch (action) { case MoreLoginActions.passwordLogin: - login(); + checkHomeserverAction(legacyPasswordLogin: true); case MoreLoginActions.privacy: launchUrlString(AppConfig.privacyUrl); case MoreLoginActions.about: diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index ff3045c27..8ae67077c 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; @@ -121,7 +121,7 @@ class HomeserverPickerView extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: SelectableLinkify( - text: L10n.of(context).welcomeText, + text: L10n.of(context).appIntroduction, style: TextStyle( color: theme.colorScheme.onSecondaryContainer, fontWeight: FontWeight.w500, @@ -142,30 +142,13 @@ class HomeserverPickerView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( - onChanged: - controller.tryCheckHomeserverActionWithCooldown, - onSubmitted: controller.onSubmitted, - onTap: - controller.tryCheckHomeserverActionWithCooldown, + onSubmitted: (_) => + controller.checkHomeserverAction(), controller: controller.homeserverController, autocorrect: false, keyboardType: TextInputType.url, decoration: InputDecoration( - prefixIcon: controller.isLoading - ? Container( - width: 16, - height: 16, - alignment: Alignment.center, - child: const SizedBox( - width: 16, - height: 16, - child: - CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ), - ) - : const Icon(Icons.search_outlined), + prefixIcon: const Icon(Icons.search_outlined), filled: false, border: OutlineInputBorder( borderRadius: BorderRadius.circular( @@ -219,25 +202,21 @@ class HomeserverPickerView extends StatelessWidget { backgroundColor: theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, ), - onPressed: - controller.isLoggingIn || controller.isLoading - ? null - : controller.supportsSso - ? controller.ssoLoginAction - : controller.supportsPasswordLogin - ? controller.login - : null, - child: Text(L10n.of(context).continueText), + onPressed: controller.isLoading + ? null + : controller.checkHomeserverAction, + child: controller.isLoading + ? const LinearProgressIndicator() + : Text(L10n.of(context).continueText), ), TextButton( style: TextButton.styleFrom( foregroundColor: theme.colorScheme.secondary, textStyle: theme.textTheme.labelMedium, ), - onPressed: - controller.isLoggingIn || controller.isLoading - ? null - : controller.restoreBackup, + onPressed: controller.isLoading + ? null + : controller.restoreBackup, child: Text(L10n.of(context).hydrate), ), ], diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 9e8cfe82c..2b2b315e0 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -1,36 +1,89 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer_view.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/utils/show_scaffold_dialog.dart'; +import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../utils/matrix_sdk_extensions/event_extension.dart'; class ImageViewer extends StatefulWidget { final Event event; + final Timeline? timeline; final BuildContext outerContext; - const ImageViewer(this.event, {required this.outerContext, super.key}); + const ImageViewer( + this.event, { + required this.outerContext, + this.timeline, + super.key, + }); @override ImageViewerController createState() => ImageViewerController(); } class ImageViewerController extends State { - /// Forward this image to another room. - void forwardAction() { - Matrix.of(widget.outerContext).shareContent = widget.event.content; - Navigator.of(context).pop(); - widget.outerContext.go('/rooms'); + @override + void initState() { + super.initState(); + allEvents = widget.timeline?.events + .where((event) => event.messageType == MessageTypes.Image) + .toList() + .reversed + .toList() ?? + [widget.event]; + var index = + allEvents.indexWhere((event) => event.eventId == widget.event.eventId); + if (index < 0) index = 0; + pageController = PageController(initialPage: index); } - /// Save this file with a system call. - void saveFileAction(BuildContext context) => widget.event.saveFile(context); + late final PageController pageController; + + late final List allEvents; + + void prevImage() async { + await pageController.previousPage( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + if (!mounted) return; + setState(() {}); + } + + void nextImage() async { + await pageController.nextPage( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + ); + if (!mounted) return; + setState(() {}); + } + + int get _index => pageController.page?.toInt() ?? 0; + + Event get currentEvent => allEvents[_index]; + + bool get canGoNext => _index < allEvents.length - 1; + + bool get canGoBack => _index > 0; + + /// Forward this image to another room. + void forwardAction() => showScaffoldDialog( + context: context, + builder: (context) => ShareScaffoldDialog( + items: [ContentShareItem(currentEvent.content)], + ), + ); /// Save this file with a system call. - void shareFileAction(BuildContext context) => widget.event.shareFile(context); + void saveFileAction(BuildContext context) => currentEvent.saveFile(context); + + /// Save this file with a system call. + void shareFileAction(BuildContext context) => currentEvent.shareFile(context); static const maxScaleFactor = 1.5; diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index b727cc92b..c6a0b8835 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'image_viewer.dart'; @@ -13,73 +14,113 @@ class ImageViewerView extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black.withAlpha(128), - extendBodyBehindAppBar: true, - appBar: AppBar( - elevation: 0, - leading: IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), - ), - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - color: Colors.white, - tooltip: L10n.of(context).close, - ), - backgroundColor: Colors.transparent, - actions: [ - IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), - ), - icon: const Icon(Icons.reply_outlined), - onPressed: controller.forwardAction, + final iconButtonStyle = IconButton.styleFrom( + backgroundColor: Colors.black.withAlpha(200), + foregroundColor: Colors.white, + ); + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Scaffold( + backgroundColor: Colors.black.withAlpha(128), + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + leading: IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, color: Colors.white, - tooltip: L10n.of(context).share, + tooltip: L10n.of(context).close, ), - const SizedBox(width: 8), - IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), + backgroundColor: Colors.transparent, + actions: [ + IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.reply_outlined), + onPressed: controller.forwardAction, + color: Colors.white, + tooltip: L10n.of(context).share, ), - icon: const Icon(Icons.download_outlined), - onPressed: () => controller.saveFileAction(context), - color: Colors.white, - tooltip: L10n.of(context).downloadFile, - ), - const SizedBox(width: 8), - if (PlatformInfos.isMobile) - // Use builder context to correctly position the share dialog on iPad - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Builder( - builder: (context) => IconButton( - style: IconButton.styleFrom( - backgroundColor: Colors.black.withAlpha(128), + const SizedBox(width: 8), + IconButton( + style: iconButtonStyle, + icon: const Icon(Icons.download_outlined), + onPressed: () => controller.saveFileAction(context), + color: Colors.white, + tooltip: L10n.of(context).downloadFile, + ), + const SizedBox(width: 8), + if (PlatformInfos.isMobile) + // Use builder context to correctly position the share dialog on iPad + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Builder( + builder: (context) => IconButton( + style: iconButtonStyle, + onPressed: () => controller.shareFileAction(context), + tooltip: L10n.of(context).share, + color: Colors.white, + icon: Icon(Icons.adaptive.share_outlined), ), - onPressed: () => controller.shareFileAction(context), - tooltip: L10n.of(context).share, - color: Colors.white, - icon: Icon(Icons.adaptive.share_outlined), ), ), - ), - ], - ), - body: InteractiveViewer( - minScale: 1.0, - maxScale: 10.0, - onInteractionEnd: controller.onInteractionEnds, - child: Center( - child: Hero( - tag: controller.widget.event.eventId, - child: MxcImage( - event: controller.widget.event, - fit: BoxFit.contain, - isThumbnail: false, - animated: true, - ), + ], + ), + body: HoverBuilder( + builder: (context, hovered) => Stack( + children: [ + PageView.builder( + controller: controller.pageController, + itemCount: controller.allEvents.length, + itemBuilder: (context, i) => InteractiveViewer( + minScale: 1.0, + maxScale: 10.0, + onInteractionEnd: controller.onInteractionEnds, + child: Center( + child: Hero( + tag: controller.allEvents[i].eventId, + child: GestureDetector( + // Ignore taps to not go back here: + onTap: () {}, + child: MxcImage( + key: ValueKey(controller.allEvents[i].eventId), + event: controller.allEvents[i], + fit: BoxFit.contain, + isThumbnail: false, + animated: true, + ), + ), + ), + ), + ), + ), + if (hovered && controller.canGoBack) + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: IconButton( + style: iconButtonStyle, + tooltip: L10n.of(context).previous, + icon: const Icon(Icons.chevron_left_outlined), + onPressed: controller.prevImage, + ), + ), + ), + if (hovered && controller.canGoNext) + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: IconButton( + style: iconButtonStyle, + tooltip: L10n.of(context).next, + icon: const Icon(Icons.chevron_right_outlined), + onPressed: controller.nextImage, + ), + ), + ), + ], ), ), ), diff --git a/lib/pages/key_verification/key_verification_dialog.dart b/lib/pages/key_verification/key_verification_dialog.dart index 8d195149b..3dfa75be6 100644 --- a/lib/pages/key_verification/key_verification_dialog.dart +++ b/lib/pages/key_verification/key_verification_dialog.dart @@ -4,12 +4,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -88,7 +88,7 @@ class KeyVerificationPageState extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context).incorrectPassphraseOrKey, + title: L10n.of(context).incorrectPassphraseOrKey, ); } } diff --git a/lib/pages/login/login.dart b/lib/pages/login/login.dart index 5d5487134..a81916f8e 100644 --- a/lib/pages/login/login.dart +++ b/lib/pages/login/login.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -12,6 +11,8 @@ import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/login/pages/pangea_login_view.dart'; import 'package:fluffychat/pangea/login/widgets/p_sso_button.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/platform_infos.dart'; @@ -255,7 +256,7 @@ class LoginController extends State { final dialogResult = await showOkCancelAlertDialog( context: context, useRootNavigator: false, - message: L10n.of(context).noMatrixServer(newDomain, oldHomeserver!), + title: L10n.of(context).noMatrixServer(newDomain, oldHomeserver!), okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, ); @@ -289,15 +290,10 @@ class LoginController extends State { message: L10n.of(context).enterAnEmailAddress, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - fullyCapitalizedForMaterial: false, - textFields: [ - DialogTextField( - initialText: - usernameController.text.isEmail ? usernameController.text : '', - hintText: L10n.of(context).enterAnEmailAddress, - keyboardType: TextInputType.emailAddress, - ), - ], + initialText: + usernameController.text.isEmail ? usernameController.text : '', + hintText: L10n.of(context).enterAnEmailAddress, + keyboardType: TextInputType.emailAddress, ); if (input == null) return; final clientSecret = DateTime.now().millisecondsSinceEpoch.toString(); @@ -306,7 +302,7 @@ class LoginController extends State { future: () => Matrix.of(context).getLoginClient().requestTokenToResetPasswordEmail( clientSecret, - input.single, + input, sendAttempt++, ), ); @@ -318,15 +314,10 @@ class LoginController extends State { message: L10n.of(context).chooseAStrongPassword, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - fullyCapitalizedForMaterial: false, - textFields: [ - const DialogTextField( - hintText: '******', - obscureText: true, - minLines: 1, - maxLines: 1, - ), - ], + hintText: '******', + obscureText: true, + minLines: 1, + maxLines: 1, ); if (password == null) return; final ok = await showOkAlertDialog( @@ -335,11 +326,10 @@ class LoginController extends State { title: L10n.of(context).weSentYouAnEmail, message: L10n.of(context).pleaseClickOnLink, okLabel: L10n.of(context).iHaveClickedOnLink, - fullyCapitalizedForMaterial: false, ); if (ok != OkCancelResult.ok) return; final data = { - 'new_password': password.single, + 'new_password': password, 'logout_devices': false, "auth": AuthenticationThreePidCreds( type: AuthenticationTypes.emailIdentity, @@ -361,8 +351,8 @@ class LoginController extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)), ); - usernameController.text = input.single; - passwordController.text = password.single; + usernameController.text = input; + passwordController.text = password; login(); } } diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart index d0b2fd4a6..35ab68df0 100644 --- a/lib/pages/new_group/new_group.dart +++ b/lib/pages/new_group/new_group.dart @@ -35,7 +35,7 @@ class NewGroupController extends State { bool requiredCodeToJoin = false; // bool publicGroup = false; // Pangea# - bool groupCanBeFound = true; + bool groupCanBeFound = false; Uint8List? avatar; @@ -54,7 +54,8 @@ class NewGroupController extends State { setState(() => _createGroupType = b.single); // #Pangea - // void setPublicGroup(bool b) => setState(() => publicGroup = b); + // void setPublicGroup(bool b) => + // setState(() => publicGroup = groupCanBeFound = b); void setRequireCode(bool b) => setState(() => requiredCodeToJoin = b); // Pangea# diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index 801a72de1..6dd01a13f 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import '../../widgets/qr_code_viewer.dart'; class NewPrivateChatView extends StatelessWidget { final NewPrivateChatController controller; @@ -25,6 +26,7 @@ class NewPrivateChatView extends StatelessWidget { final theme = Theme.of(context); final searchResponse = controller.searchResponse; + final userId = Matrix.of(context).client.userID!; return Scaffold( appBar: AppBar( scrolledUnderElevation: 0, @@ -142,10 +144,7 @@ class NewPrivateChatView extends StatelessWidget { foregroundColor: theme.colorScheme.onTertiaryContainer, child: const Icon(Icons.group_add_outlined), ), - // #Pangea - // title: Text(L10n.of(context).createGroup), - title: Text(L10n.of(context).createChat), - // Pangea# + title: Text(L10n.of(context).createGroup), onTap: () => context.go('/rooms/newgroup'), ), if (PlatformInfos.isMobile) @@ -160,26 +159,35 @@ class NewPrivateChatView extends StatelessWidget { ), Center( child: Padding( - padding: const EdgeInsets.all(64.0), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: Material( - borderRadius: BorderRadius.circular(12), - elevation: 10, - color: Colors.white, - shadowColor: theme.appBarTheme.shadowColor, - clipBehavior: Clip.hardEdge, + padding: const EdgeInsets.symmetric( + horizontal: 64.0, + vertical: 24.0, + ), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + color: theme.colorScheme.primaryContainer, + clipBehavior: Clip.hardEdge, + child: InkWell( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + onTap: () => showQrCodeViewer( + context, + userId, + ), child: Padding( - padding: const EdgeInsets.all(8), - child: PrettyQrView.data( - data: - 'https://matrix.to/#/${Matrix.of(context).client.userID}', - decoration: PrettyQrDecoration( - shape: PrettyQrSmoothSymbol( - roundFactor: 1, - color: theme.brightness == Brightness.light - ? theme.colorScheme.primary - : theme.colorScheme.onPrimary, + padding: const EdgeInsets.all(32.0), + child: ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 256), + child: PrettyQrView.data( + data: 'https://matrix.to/#/$userId', + decoration: PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + roundFactor: 1, + color: + theme.colorScheme.onPrimaryContainer, + ), ), ), ), diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 2e308549d..393bbbb85 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:image_picker/image_picker.dart'; @@ -9,6 +8,8 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/user/utils/logout.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_view.dart'; @@ -37,25 +38,14 @@ class SettingsController extends State { title: L10n.of(context).editDisplayname, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - // #Pangea - maxLength: 32, - // Pangea# - initialText: profile?.displayName ?? - Matrix.of(context).client.userID!.localpart, - ), - ], - // #Pangea - autoSubmit: true, - // Pangea# + initialText: + profile?.displayName ?? Matrix.of(context).client.userID!.localpart, ); if (input == null) return; final matrix = Matrix.of(context); final success = await showFutureLoadingDialog( context: context, - future: () => - matrix.client.setDisplayName(matrix.client.userID!, input.single), + future: () => matrix.client.setDisplayName(matrix.client.userID!, input), ); if (success.error == null) { updateProfile(); @@ -71,7 +61,7 @@ class SettingsController extends State { // context: context, // title: L10n.of(context).areYouSureYouWantToLogout, // message: L10n.of(context).noBackupWarning, - // isDestructiveAction: noBackup, + // isDestructive: noBackup, // okLabel: L10n.of(context).logout, // cancelLabel: L10n.of(context).cancel, // ) == @@ -90,30 +80,31 @@ class SettingsController extends State { final profile = await profileFuture; final actions = [ if (PlatformInfos.isMobile) - SheetAction( - key: AvatarAction.camera, + AdaptiveModalAction( + value: AvatarAction.camera, label: L10n.of(context).openCamera, isDefaultAction: true, - icon: Icons.camera_alt_outlined, + icon: const Icon(Icons.camera_alt_outlined), ), - SheetAction( - key: AvatarAction.file, + AdaptiveModalAction( + value: AvatarAction.file, label: L10n.of(context).openGallery, - icon: Icons.photo_outlined, + icon: const Icon(Icons.photo_outlined), ), if (profile?.avatarUrl != null) - SheetAction( - key: AvatarAction.remove, + AdaptiveModalAction( + value: AvatarAction.remove, label: L10n.of(context).removeYourAvatar, - isDestructiveAction: true, - icon: Icons.delete_outlined, + isDestructive: true, + icon: const Icon(Icons.delete_outlined), ), ]; final action = actions.length == 1 - ? actions.single.key - : await showModalActionSheet( + ? actions.single.value + : await showModalActionPopup( context: context, title: L10n.of(context).changeYourAvatar, + cancelLabel: L10n.of(context).cancel, actions: actions, ); if (action == null) return; diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 5079a3043..9b4478520 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -96,6 +96,7 @@ class SettingsView extends StatelessWidget { ), style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, + iconColor: theme.colorScheme.onSurface, ), label: Text( displayname, @@ -115,6 +116,7 @@ class SettingsView extends StatelessWidget { ), style: TextButton.styleFrom( foregroundColor: theme.colorScheme.secondary, + iconColor: theme.colorScheme.secondary, ), label: Text( mxid, diff --git a/lib/pages/settings_3pid/settings_3pid.dart b/lib/pages/settings_3pid/settings_3pid.dart index d73814608..73ec22744 100644 --- a/lib/pages/settings_3pid/settings_3pid.dart +++ b/lib/pages/settings_3pid/settings_3pid.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'settings_3pid_view.dart'; @@ -25,12 +26,8 @@ class Settings3PidController extends State { title: L10n.of(context).enterAnEmailAddress, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - hintText: L10n.of(context).enterAnEmailAddress, - keyboardType: TextInputType.emailAddress, - ), - ], + hintText: L10n.of(context).enterAnEmailAddress, + keyboardType: TextInputType.emailAddress, ); if (input == null) return; final clientSecret = DateTime.now().millisecondsSinceEpoch.toString(); @@ -38,7 +35,7 @@ class Settings3PidController extends State { context: context, future: () => Matrix.of(context).client.requestTokenToRegisterEmail( clientSecret, - input.single, + input, Settings3Pid.sendAttempt++, ), ); diff --git a/lib/pages/settings_emotes/import_archive_dialog.dart b/lib/pages/settings_emotes/import_archive_dialog.dart index 02cafad78..b2b5885f2 100644 --- a/lib/pages/settings_emotes/import_archive_dialog.dart +++ b/lib/pages/settings_emotes/import_archive_dialog.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:archive/archive.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -11,6 +10,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart'; import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ImportEmoteArchiveDialog extends StatefulWidget { diff --git a/lib/pages/settings_emotes/settings_emotes.dart b/lib/pages/settings_emotes/settings_emotes.dart index 259409030..1457d62a8 100644 --- a/lib/pages/settings_emotes/settings_emotes.dart +++ b/lib/pages/settings_emotes/settings_emotes.dart @@ -1,7 +1,9 @@ +import 'package:archive/archive.dart' + if (dart.library.io) 'package:archive/archive_io.dart'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; @@ -11,14 +13,12 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/file_selector.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'import_archive_dialog.dart'; import 'settings_emotes_view.dart'; -import 'package:archive/archive.dart' - if (dart.library.io) 'package:archive/archive_io.dart'; - class EmotesSettings extends StatefulWidget { const EmotesSettings({super.key}); @@ -137,7 +137,7 @@ class EmotesSettingsController extends State { showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context).emoteExists, + title: L10n.of(context).emoteExists, okLabel: L10n.of(context).ok, ); return; @@ -147,7 +147,7 @@ class EmotesSettingsController extends State { showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context).emoteInvalid, + title: L10n.of(context).emoteInvalid, okLabel: L10n.of(context).ok, ); return; @@ -183,7 +183,7 @@ class EmotesSettingsController extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context).emoteWarnNeedToPick, + title: L10n.of(context).emoteWarnNeedToPick, okLabel: L10n.of(context).ok, ); return; @@ -193,7 +193,7 @@ class EmotesSettingsController extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context).emoteExists, + title: L10n.of(context).emoteExists, okLabel: L10n.of(context).ok, ); return; @@ -202,7 +202,7 @@ class EmotesSettingsController extends State { await showOkAlertDialog( useRootNavigator: false, context: context, - message: L10n.of(context).emoteInvalid, + title: L10n.of(context).emoteInvalid, okLabel: L10n.of(context).ok, ); return; diff --git a/lib/pages/settings_homeserver/settings_homeserver_view.dart b/lib/pages/settings_homeserver/settings_homeserver_view.dart index 792d6093e..59ee61c96 100644 --- a/lib/pages/settings_homeserver/settings_homeserver_view.dart +++ b/lib/pages/settings_homeserver/settings_homeserver_view.dart @@ -88,7 +88,7 @@ class SettingsHomeserverView extends StatelessWidget { if (supportPage != null) ListTile( title: Text(L10n.of(context).supportPage), - subtitle: Text(supportPage), + subtitle: Text(supportPage.toString()), ), if (contacts != null) ...contacts.map( diff --git a/lib/pages/settings_notifications/settings_notifications.dart b/lib/pages/settings_notifications/settings_notifications.dart index 25d3e3038..772cdef5a 100644 --- a/lib/pages/settings_notifications/settings_notifications.dart +++ b/lib/pages/settings_notifications/settings_notifications.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../widgets/matrix.dart'; import 'settings_notifications_view.dart'; @@ -138,15 +138,16 @@ class SettingsNotificationsController extends State { } void onPusherTap(Pusher pusher) async { - final delete = await showModalActionSheet( + final delete = await showModalActionPopup( context: context, title: pusher.deviceDisplayName, message: '${pusher.appDisplayName} (${pusher.appId})', + cancelLabel: L10n.of(context).cancel, actions: [ - SheetAction( + AdaptiveModalAction( label: L10n.of(context).delete, - isDestructiveAction: true, - key: true, + isDestructive: true, + value: true, ), ], ); diff --git a/lib/pages/settings_security/settings_security.dart b/lib/pages/settings_security/settings_security.dart index 1eb94b13a..b60238f38 100644 --- a/lib/pages/settings_security/settings_security.dart +++ b/lib/pages/settings_security/settings_security.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/app_lock.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -29,25 +30,20 @@ class SettingsSecurityController extends State { title: L10n.of(context).pleaseChooseAPasscode, message: L10n.of(context).pleaseEnter4Digits, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - validator: (text) { - if (text!.isEmpty || - (text.length == 4 && int.tryParse(text)! >= 0)) { - return null; - } - return L10n.of(context).pleaseEnter4Digits; - }, - keyboardType: TextInputType.number, - obscureText: true, - maxLines: 1, - minLines: 1, - maxLength: 4, - ), - ], + validator: (text) { + if (text.isEmpty || (text.length == 4 && int.tryParse(text)! >= 0)) { + return null; + } + return L10n.of(context).pleaseEnter4Digits; + }, + keyboardType: TextInputType.number, + obscureText: true, + maxLines: 1, + minLines: 1, + maxLength: 4, ); if (newLock != null) { - await AppLock.of(context).changePincode(newLock.single); + await AppLock.of(context).changePincode(newLock); } } @@ -82,7 +78,7 @@ class SettingsSecurityController extends State { message: L10n.of(context).deactivateAccountWarning, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - isDestructiveAction: true, + isDestructive: true, ) == OkCancelResult.cancel) { return; @@ -92,18 +88,14 @@ class SettingsSecurityController extends State { useRootNavigator: false, context: context, title: L10n.of(context).confirmMatrixId, - textFields: [ - DialogTextField( - validator: (text) => text == supposedMxid - ? null - : L10n.of(context).supposedMxid(supposedMxid), - ), - ], - isDestructiveAction: true, + validator: (text) => text == supposedMxid + ? null + : L10n.of(context).supposedMxid(supposedMxid), + isDestructive: true, okLabel: L10n.of(context).delete, cancelLabel: L10n.of(context).cancel, ); - if (mxids == null || mxids.length != 1 || mxids.single != supposedMxid) { + if (mxids == null || mxids.length != 1 || mxids != supposedMxid) { return; } final input = await showTextInputDialog( @@ -112,22 +104,18 @@ class SettingsSecurityController extends State { title: L10n.of(context).pleaseEnterYourPassword, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - isDestructiveAction: true, - textFields: [ - const DialogTextField( - obscureText: true, - hintText: '******', - minLines: 1, - maxLines: 1, - ), - ], + isDestructive: true, + obscureText: true, + hintText: '******', + minLines: 1, + maxLines: 1, ); if (input == null) return; await showFutureLoadingDialog( context: context, future: () => Matrix.of(context).client.deactivateAccount( auth: AuthenticationPassword( - password: input.single, + password: input, identifier: AuthenticationUserIdentifier( user: Matrix.of(context).client.userID!, ), diff --git a/lib/pages/settings_style/settings_style_view.dart b/lib/pages/settings_style/settings_style_view.dart index eea2bc660..d93e492fc 100644 --- a/lib/pages/settings_style/settings_style_view.dart +++ b/lib/pages/settings_style/settings_style_view.dart @@ -214,7 +214,9 @@ class SettingsStyleView extends StatelessWidget { ), child: DecoratedBox( decoration: BoxDecoration( - color: theme.colorScheme.primary, + color: theme.brightness == Brightness.light + ? theme.colorScheme.primary + : theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular( AppConfig.borderRadius, ), @@ -227,7 +229,11 @@ class SettingsStyleView extends StatelessWidget { child: Text( 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor', style: TextStyle( - color: theme.colorScheme.onPrimary, + color: + theme.brightness == Brightness.light + ? theme.colorScheme.onPrimary + : theme.colorScheme + .onPrimaryContainer, fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor, ), diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index d0d7b6012..fa0481c0d 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -1,19 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.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/permission_slider_dialog.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet_view.dart'; enum UserBottomSheetAction { - report, + // #Pangea + // report, + // Pangea# mention, ban, kick, @@ -94,55 +96,56 @@ class UserBottomSheetController extends State { if (userId == null) throw ('user or profile must not be null!'); switch (action) { - case UserBottomSheetAction.report: - if (user == null) throw ('User must not be null for this action!'); + // #Pangea + // case UserBottomSheetAction.report: + // if (user == null) throw ('User must not be null for this action!'); - final score = await showConfirmationDialog( - context: context, - title: L10n.of(context).reportUser, - message: L10n.of(context).howOffensiveIsThisContent, - cancelLabel: L10n.of(context).cancel, - okLabel: L10n.of(context).ok, - actions: [ - AlertDialogAction( - key: -100, - label: L10n.of(context).extremeOffensive, - ), - AlertDialogAction( - key: -50, - label: L10n.of(context).offensive, - ), - AlertDialogAction( - key: 0, - label: L10n.of(context).inoffensive, - ), - ], - ); - if (score == null) return; - final reason = await showTextInputDialog( - useRootNavigator: false, - context: context, - title: L10n.of(context).whyDoYouWantToReportThis, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - textFields: [DialogTextField(hintText: L10n.of(context).reason)], - ); - if (reason == null || reason.single.isEmpty) return; + // final score = await showModalActionPopup( + // context: context, + // title: L10n.of(context).reportUser, + // message: L10n.of(context).howOffensiveIsThisContent, + // cancelLabel: L10n.of(context).cancel, + // actions: [ + // AdaptiveModalAction( + // value: -100, + // label: L10n.of(context).extremeOffensive, + // ), + // AdaptiveModalAction( + // value: -50, + // label: L10n.of(context).offensive, + // ), + // AdaptiveModalAction( + // value: 0, + // label: L10n.of(context).inoffensive, + // ), + // ], + // ); + // if (score == null) return; + // final reason = await showTextInputDialog( + // useRootNavigator: false, + // context: context, + // title: L10n.of(context).whyDoYouWantToReportThis, + // okLabel: L10n.of(context).ok, + // cancelLabel: L10n.of(context).cancel, + // hintText: L10n.of(context).reason, + // ); + // if (reason == null || reason.isEmpty) return; - final result = await showFutureLoadingDialog( - context: context, - future: () => Matrix.of(widget.outerContext).client.reportContent( - user.room.id, - user.id, - reason: reason.single, - score: score, - ), - ); - if (result.error != null) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).contentHasBeenReported)), - ); - break; + // final result = await showFutureLoadingDialog( + // context: context, + // future: () => Matrix.of(widget.outerContext).client.reportEvent( + // user.room.id, + // user.id, + // reason: reason, + // score: score, + // ), + // ); + // if (result.error != null) return; + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text(L10n.of(context).contentHasBeenReported)), + // ); + // break; + // Pangea# case UserBottomSheetAction.mention: if (user == null) throw ('User must not be null for this action!'); Navigator.of(context).pop(); @@ -237,50 +240,10 @@ class UserBottomSheetController extends State { } } - bool isSending = false; - Object? sendError; final TextEditingController sendController = TextEditingController(); - void sendAction([_]) async { - final userId = widget.user?.id ?? widget.profile?.userId; - final client = Matrix.of(widget.outerContext).client; - if (userId == null) throw ('user or profile must not be null!'); - - final input = sendController.text.trim(); - if (input.isEmpty) return; - - setState(() { - isSending = true; - sendError = null; - }); - try { - final roomId = await client.startDirectChat( - userId, - // #Pangea - enableEncryption: false, - // Pangea# - ); - if (!mounted) return; - final room = client.getRoomById(roomId); - if (room == null) { - throw ('DM Room found or created but room not found in client'); - } - await room.sendTextEvent(input); - setState(() { - isSending = false; - sendController.clear(); - }); - } catch (e, s) { - Logs().d('Unable to send message', e, s); - setState(() { - isSending = false; - sendError = e; - }); - } - } - void knockAccept() async { final user = widget.user!; final result = await showFutureLoadingDialog( diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 68a840571..0254c6b14 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -8,10 +8,10 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/presence_builder.dart'; +import 'package:fluffychat/widgets/qr_code_viewer.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet.dart'; @@ -32,370 +32,350 @@ class UserBottomSheetView extends StatelessWidget { final client = Matrix.of(controller.widget.outerContext).client; final profileSearchError = controller.widget.profileSearchError; final dmRoomId = client.getDirectChatFromUserId(userId); - return SafeArea( - child: Scaffold( - appBar: AppBar( - leading: CloseButton( + return Scaffold( + appBar: AppBar( + leading: Center( + child: CloseButton( onPressed: Navigator.of(context, rootNavigator: false).pop, ), - centerTitle: false, - title: Text(displayname), - actions: dmRoomId == null - ? null - : [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: FloatingActionButton.small( - elevation: 0, - onPressed: () => controller - .participantAction(UserBottomSheetAction.message), - child: const Icon(Icons.chat_outlined), - ), - ), - ], ), - body: StreamBuilder( - stream: user?.room.client.onSync.stream.where( - (syncUpdate) => - syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any( - (state) => state.type == EventTypes.RoomPowerLevels, - ) ?? - false, + centerTitle: false, + title: Text(displayname), + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + onPressed: () => showQrCodeViewer(context, userId), + icon: const Icon(Icons.qr_code_outlined), + ), ), - builder: (context, snapshot) { - final theme = Theme.of(context); - return ListView( - children: [ - if (user?.membership == Membership.knock) - Padding( - padding: const EdgeInsets.all(12.0), - child: Material( - color: theme.colorScheme.surfaceContainerHigh, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - child: ListTile( - minVerticalPadding: 16, - title: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Text( - L10n.of(context) - .userWouldLikeToChangeTheChat(displayname), - ), - ), - subtitle: Row( - children: [ - TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: theme.colorScheme.surface, - foregroundColor: theme.colorScheme.primary, - ), - onPressed: controller.knockAccept, - icon: const Icon(Icons.check_outlined), - label: Text(L10n.of(context).accept), - ), - const SizedBox(width: 12), - TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: - theme.colorScheme.errorContainer, - foregroundColor: - theme.colorScheme.onErrorContainer, - ), - onPressed: controller.knockDecline, - icon: const Icon(Icons.cancel_outlined), - label: Text(L10n.of(context).decline), - ), - ], + ], + ), + body: StreamBuilder( + stream: user?.room.client.onSync.stream.where( + (syncUpdate) => + syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any( + (state) => state.type == EventTypes.RoomPowerLevels, + ) ?? + false, + ), + builder: (context, snapshot) { + final theme = Theme.of(context); + return ListView( + children: [ + if (user?.membership == Membership.knock) + Padding( + padding: const EdgeInsets.all(12.0), + child: Material( + color: theme.colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: ListTile( + minVerticalPadding: 16, + title: Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + L10n.of(context) + .userWouldLikeToChangeTheChat(displayname), ), ), - ), - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Avatar( - client: - Matrix.of(controller.widget.outerContext).client, - mxContent: avatarUrl, - name: displayname, - // #Pangea - presenceUserId: user?.id, - // Pangea# - size: Avatar.defaultSize * 2.5, - ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + subtitle: Row( children: [ TextButton.icon( - onPressed: () => FluffyShare.share( - userId, - context, - copyOnly: true, - ), - icon: const Icon( - Icons.copy_outlined, - size: 14, - ), style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.onSurface, - ), - label: Text( - userId, - maxLines: 1, - overflow: TextOverflow.ellipsis, + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.primary, + iconColor: theme.colorScheme.primary, ), + onPressed: controller.knockAccept, + icon: const Icon(Icons.check_outlined), + label: Text(L10n.of(context).accept), ), - PresenceBuilder( - userId: userId, - client: client, - builder: (context, presence) { - if (presence == null || - (presence.presence == PresenceType.offline && - presence.lastActiveTimestamp == null)) { - return const SizedBox.shrink(); - } - - final dotColor = presence.presence.isOnline - ? Colors.green - : presence.presence.isUnavailable - ? Colors.orange - : Colors.grey; - - final lastActiveTimestamp = - presence.lastActiveTimestamp; - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 16), - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: dotColor, - borderRadius: BorderRadius.circular(16), - ), - ), - const SizedBox(width: 12), - if (presence.currentlyActive == true) - Text( - L10n.of(context).currentlyActive, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, - ) - else if (lastActiveTimestamp != null) - Text( - L10n.of(context).lastActiveAgo( - lastActiveTimestamp - .localizedTimeShort(context), - ), - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, - ), - ], - ); - }, + const SizedBox(width: 12), + TextButton.icon( + style: TextButton.styleFrom( + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: + theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, + ), + onPressed: controller.knockDecline, + icon: const Icon(Icons.cancel_outlined), + label: Text(L10n.of(context).decline), ), ], ), ), - ], + ), ), - PresenceBuilder( - userId: userId, - client: client, - builder: (context, presence) { - final status = presence?.statusMsg; - if (status == null || status.isEmpty) { - return const SizedBox.shrink(); - } - return ListTile( - title: SelectableLinkify( - text: status, - style: const TextStyle(fontSize: 16), - options: const LinkifyOptions(humanize: false), - linkStyle: const TextStyle( - color: Colors.blueAccent, - decorationColor: Colors.blueAccent, - ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), - ); - }, - ), - if (userId != client.userID) + Row( + children: [ Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + padding: const EdgeInsets.all(16.0), + child: Avatar( + client: Matrix.of(controller.widget.outerContext).client, + mxContent: avatarUrl, + name: displayname, + size: Avatar.defaultSize * 2.5, ), - child: dmRoomId == null - ? ElevatedButton.icon( - onPressed: () => controller.participantAction( - UserBottomSheetAction.message, - ), - icon: const Icon(Icons.chat_outlined), - label: Text(L10n.of(context).startConversation), - ) - : TextField( - controller: controller.sendController, - readOnly: controller.isSending, - onSubmitted: controller.sendAction, - minLines: 1, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () => FluffyShare.share( + userId, + context, + copyOnly: true, + ), + icon: const Icon( + Icons.copy_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.onSurface, + iconColor: theme.colorScheme.onSurface, + ), + label: Text( + userId, maxLines: 1, - textInputAction: TextInputAction.send, - decoration: InputDecoration( - errorText: controller.sendError - ?.toLocalizedString(context), - hintText: L10n.of(context).sendMessages, - suffix: controller.isSending - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ) - : null, - suffixIcon: controller.isSending - ? null - : IconButton( - icon: const Icon(Icons.send_outlined), - onPressed: controller.sendAction, + overflow: TextOverflow.ellipsis, + ), + ), + PresenceBuilder( + userId: userId, + client: client, + builder: (context, presence) { + if (presence == null || + (presence.presence == PresenceType.offline && + presence.lastActiveTimestamp == null)) { + return const SizedBox.shrink(); + } + + final dotColor = presence.presence.isOnline + ? Colors.green + : presence.presence.isUnavailable + ? Colors.orange + : Colors.grey; + + final lastActiveTimestamp = + presence.lastActiveTimestamp; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 16), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: dotColor, + borderRadius: BorderRadius.circular(16), + ), + ), + const SizedBox(width: 12), + if (presence.currentlyActive == true) + Text( + L10n.of(context).currentlyActive, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ) + else if (lastActiveTimestamp != null) + Text( + L10n.of(context).lastActiveAgo( + lastActiveTimestamp + .localizedTimeShort(context), ), - ), - ), - ), - if (controller.widget.onMention != null) - ListTile( - leading: const Icon(Icons.alternate_email_outlined), - title: Text(L10n.of(context).mention), - onTap: () => controller - .participantAction(UserBottomSheetAction.mention), - ), - if (user != null) ...[ - Divider(color: theme.dividerColor), - ListTile( - title: Text( - '${L10n.of(context).userRole} (${user.powerLevel})', - ), - leading: const Icon(Icons.person_outlined), - trailing: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius / 2), - color: theme.colorScheme.onInverseSurface, - child: DropdownButton( - onChanged: user.canChangeUserPowerLevel || - // Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765 - (user.room.canChangePowerLevel && - user.id == user.room.client.userID) - ? controller.setPowerLevel - : null, - value: {0, 50, 100}.contains(user.powerLevel) - ? user.powerLevel - : null, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - borderRadius: - BorderRadius.circular(AppConfig.borderRadius / 2), - underline: const SizedBox.shrink(), - items: [ - DropdownMenuItem( - value: 0, - child: Text(L10n.of(context).user), - ), - DropdownMenuItem( - value: 50, - child: Text(L10n.of(context).moderator), - ), - DropdownMenuItem( - value: 100, - child: Text(L10n.of(context).admin), - ), - DropdownMenuItem( - value: null, - child: Text(L10n.of(context).custom), - ), - ], - ), + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ], + ); + }, + ), + ], ), ), ], + ), + PresenceBuilder( + userId: userId, + client: client, + builder: (context, presence) { + final status = presence?.statusMsg; + if (status == null || status.isEmpty) { + return const SizedBox.shrink(); + } + return ListTile( + title: SelectableLinkify( + text: status, + style: const TextStyle(fontSize: 16), + options: const LinkifyOptions(humanize: false), + linkStyle: const TextStyle( + color: Colors.blueAccent, + decorationColor: Colors.blueAccent, + ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), + ), + ); + }, + ), + if (userId != client.userID) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: ElevatedButton.icon( + onPressed: () => controller.participantAction( + UserBottomSheetAction.message, + ), + icon: const Icon(Icons.forum_outlined), + label: Text( + dmRoomId == null + ? L10n.of(context).startConversation + : L10n.of(context).sendAMessage, + ), + ), + ), + if (controller.widget.onMention != null) + ListTile( + leading: const Icon(Icons.alternate_email_outlined), + title: Text(L10n.of(context).mention), + onTap: () => controller + .participantAction(UserBottomSheetAction.mention), + ), + if (user != null) ...[ Divider(color: theme.dividerColor), - if (user != null && user.canKick) - ListTile( - textColor: theme.colorScheme.error, - iconColor: theme.colorScheme.error, - title: Text(L10n.of(context).kickFromChat), - leading: const Icon(Icons.exit_to_app_outlined), - onTap: () => controller - .participantAction(UserBottomSheetAction.kick), - ), - if (user != null && - user.canBan && - user.membership != Membership.ban) - ListTile( - textColor: theme.colorScheme.onErrorContainer, - iconColor: theme.colorScheme.onErrorContainer, - title: Text(L10n.of(context).banFromChat), - leading: const Icon(Icons.warning_sharp), - onTap: () => - controller.participantAction(UserBottomSheetAction.ban), - ) - else if (user != null && - user.canBan && - user.membership == Membership.ban) - ListTile( - title: Text(L10n.of(context).unbanFromChat), - leading: const Icon(Icons.warning_outlined), - onTap: () => controller - .participantAction(UserBottomSheetAction.unban), - ), - // #Pangea - // if (user != null && user.id != client.userID) - // ListTile( - // textColor: theme.colorScheme.onErrorContainer, - // iconColor: theme.colorScheme.onErrorContainer, - // title: Text(L10n.of(context).reportUser), - // leading: const Icon(Icons.gavel_outlined), - // onTap: () => controller - // .participantAction(UserBottomSheetAction.report), - // ), - // Pangea# - if (profileSearchError != null) - ListTile( - leading: const Icon( - Icons.warning_outlined, - color: Colors.orange, - ), - subtitle: Text( - L10n.of(context).profileNotFound, - style: const TextStyle(color: Colors.orange), + ListTile( + title: Text(L10n.of(context).userRole), + leading: const Icon(Icons.admin_panel_settings_outlined), + trailing: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius / 2), + color: theme.colorScheme.onInverseSurface, + child: DropdownButton( + onChanged: user.canChangeUserPowerLevel || + // Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765 + (user.room.canChangePowerLevel && + user.id == user.room.client.userID) + ? controller.setPowerLevel + : null, + value: {0, 50, 100}.contains(user.powerLevel) + ? user.powerLevel + : null, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius / 2), + underline: const SizedBox.shrink(), + items: [ + DropdownMenuItem( + value: 0, + child: Text( + L10n.of(context).userLevel( + user.powerLevel < 50 ? user.powerLevel : 0, + ), + ), + ), + DropdownMenuItem( + value: 50, + child: Text( + L10n.of(context).moderatorLevel( + user.powerLevel >= 50 && user.powerLevel < 100 + ? user.powerLevel + : 50, + ), + ), + ), + DropdownMenuItem( + value: 100, + child: Text( + L10n.of(context).adminLevel( + user.powerLevel >= 100 ? user.powerLevel : 100, + ), + ), + ), + DropdownMenuItem( + value: null, + child: Text(L10n.of(context).custom), + ), + ], ), ), - if (userId != client.userID && - !client.ignoredUsers.contains(userId) - // #Pangea - && - userId != BotName.byEnvironment - // Pangea# - ) - ListTile( - textColor: theme.colorScheme.onErrorContainer, - iconColor: theme.colorScheme.onErrorContainer, - leading: const Icon(Icons.block_outlined), - title: Text(L10n.of(context).block), - onTap: () => controller - .participantAction(UserBottomSheetAction.ignore), - ), + ), ], - ); - }, - ), + Divider(color: theme.dividerColor), + if (user != null && user.canKick) + ListTile( + textColor: theme.colorScheme.error, + iconColor: theme.colorScheme.error, + title: Text(L10n.of(context).kickFromChat), + leading: const Icon(Icons.exit_to_app_outlined), + onTap: () => + controller.participantAction(UserBottomSheetAction.kick), + ), + if (user != null && + user.canBan && + user.membership != Membership.ban) + ListTile( + textColor: theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, + title: Text(L10n.of(context).banFromChat), + leading: const Icon(Icons.warning_sharp), + onTap: () => + controller.participantAction(UserBottomSheetAction.ban), + ) + else if (user != null && + user.canBan && + user.membership == Membership.ban) + ListTile( + title: Text(L10n.of(context).unbanFromChat), + leading: const Icon(Icons.warning_outlined), + onTap: () => + controller.participantAction(UserBottomSheetAction.unban), + ), + // #Pangea + // if (user != null && user.id != client.userID) + // ListTile( + // textColor: theme.colorScheme.onErrorContainer, + // iconColor: theme.colorScheme.onErrorContainer, + // title: Text(L10n.of(context).reportUser), + // leading: const Icon(Icons.gavel_outlined), + // onTap: () => controller + // .participantAction(UserBottomSheetAction.report), + // ), + // Pangea# + if (profileSearchError != null) + ListTile( + leading: const Icon( + Icons.warning_outlined, + color: Colors.orange, + ), + subtitle: Text( + L10n.of(context).profileNotFound, + style: const TextStyle(color: Colors.orange), + ), + ), + if (userId != client.userID && + !client.ignoredUsers.contains(userId) + // #Pangea + && + userId != BotName.byEnvironment + // Pangea# + ) + ListTile( + textColor: theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, + leading: const Icon(Icons.block_outlined), + title: Text(L10n.of(context).block), + onTap: () => controller + .participantAction(UserBottomSheetAction.ignore), + ), + ], + ); + }, ), ); } diff --git a/lib/pangea/activity_planner/activity_plan_message.dart b/lib/pangea/activity_planner/activity_plan_message.dart index e376a326b..d9ada9d7c 100644 --- a/lib/pangea/activity_planner/activity_plan_message.dart +++ b/lib/pangea/activity_planner/activity_plan_message.dart @@ -119,6 +119,11 @@ class ActivityPlanMessage extends StatelessWidget { borderRadius: borderRadius, controller: controller, immersionMode: false, + timeline: timeline, + linkColor: theme.brightness == + Brightness.light + ? theme.colorScheme.primaryFixed + : theme.colorScheme.onTertiaryContainer, ), if (event.hasAggregatedEvents( timeline, diff --git a/lib/pangea/analytics_details_popup/analytics_details_popup.dart b/lib/pangea/analytics_details_popup/analytics_details_popup.dart index 136fc0f67..dc2b83550 100644 --- a/lib/pangea/analytics_details_popup/analytics_details_popup.dart +++ b/lib/pangea/analytics_details_popup/analytics_details_popup.dart @@ -10,16 +10,20 @@ import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dar import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; class AnalyticsPopupWrapper extends StatefulWidget { - AnalyticsPopupWrapper({super.key, this.constructZoom, required this.view}); + const AnalyticsPopupWrapper({ + super.key, + this.constructZoom, + required this.view, + }); - ConstructTypeEnum view; - ConstructIdentifier? constructZoom; + final ConstructTypeEnum view; + final ConstructIdentifier? constructZoom; @override - _AnalyticsPopupWrapperState createState() => _AnalyticsPopupWrapperState(); + AnalyticsPopupWrapperState createState() => AnalyticsPopupWrapperState(); } -class _AnalyticsPopupWrapperState extends State { +class AnalyticsPopupWrapperState extends State { ConstructIdentifier? localConstructZoom; ConstructTypeEnum localView = ConstructTypeEnum.vocab; diff --git a/lib/pangea/chat/widgets/chat_view_background.dart b/lib/pangea/chat/widgets/chat_view_background.dart index 49ca75807..1eadb40a8 100644 --- a/lib/pangea/chat/widgets/chat_view_background.dart +++ b/lib/pangea/chat/widgets/chat_view_background.dart @@ -7,10 +7,7 @@ import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; class ChatViewBackground extends StatefulWidget { final Choreographer choreographer; - const ChatViewBackground({ - super.key, - required this.choreographer, - }); + const ChatViewBackground(this.choreographer, {super.key}); @override ChatViewBackgroundState createState() => ChatViewBackgroundState(); diff --git a/lib/pangea/chat_list/utils/app_version_util.dart b/lib/pangea/chat_list/utils/app_version_util.dart index baff4f4d8..af736339c 100644 --- a/lib/pangea/chat_list/utils/app_version_util.dart +++ b/lib/pangea/chat_list/utils/app_version_util.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:http/http.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -18,6 +17,7 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class AppVersionUtil { @@ -130,7 +130,7 @@ class AppVersionUtil { return; } - final OkCancelResult dialogResponse = await _showDialog( + final OkCancelResult? dialogResponse = await _showDialog( context, mandatoryUpdate, currentVersion, @@ -151,7 +151,7 @@ class AppVersionUtil { } } - static Future _showDialog( + static Future _showDialog( BuildContext context, bool mandatoryUpdate, String currentVersion, @@ -174,16 +174,16 @@ class AppVersionUtil { context: context, title: title, message: message, - canPop: false, - barrierDismissible: false, + // canPop: false, + // barrierDismissible: false, okLabel: L10n.of(context).updateNow, ) : showOkCancelAlertDialog( context: context, title: title, message: message, - canPop: false, - barrierDismissible: false, + // canPop: false, + // barrierDismissible: false, okLabel: L10n.of(context).updateNow, cancelLabel: L10n.of(context).updateLater, ); diff --git a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart index 0d1c7b397..afdd2f222 100644 --- a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -10,6 +9,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/common/constants/local.key.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.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'; import '../../common/utils/error_handler.dart'; diff --git a/lib/pangea/chat_settings/pages/pangea_chat_details.dart b/lib/pangea/chat_settings/pages/pangea_chat_details.dart index 7f929d214..ab9cad82d 100644 --- a/lib/pangea/chat_settings/pages/pangea_chat_details.dart +++ b/lib/pangea/chat_settings/pages/pangea_chat_details.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:go_router/go_router.dart'; @@ -21,6 +20,8 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; @@ -37,20 +38,20 @@ class PangeaChatDetailsView extends StatelessWidget { Matrix.of(context).client.getRoomById(controller.roomId!); if (room == null) return; - final type = await showConfirmationDialog( + final type = await showModalActionPopup( context: context, title: L10n.of(context).downloadGroupText, actions: [ - AlertDialogAction( - key: DownloadType.csv, + AdaptiveModalAction( + value: DownloadType.csv, label: L10n.of(context).downloadCSVFile, ), - AlertDialogAction( - key: DownloadType.txt, + AdaptiveModalAction( + value: DownloadType.txt, label: L10n.of(context).downloadTxtFile, ), - AlertDialogAction( - key: DownloadType.xlsx, + AdaptiveModalAction( + value: DownloadType.xlsx, label: L10n.of(context).downloadXLSXFile, ), ], @@ -413,7 +414,7 @@ class PangeaChatDetailsView extends StatelessWidget { message: room.isSpace ? L10n.of(context).leaveSpaceDescription : L10n.of(context).archiveRoomDescription, - isDestructiveAction: true, + isDestructive: true, ); if (confirmed == OkCancelResult.cancel) return; final resp = await showFutureLoadingDialog( diff --git a/lib/pangea/chat_settings/widgets/room_capacity_button.dart b/lib/pangea/chat_settings/widgets/room_capacity_button.dart index dee32b912..9555b93cc 100644 --- a/lib/pangea/chat_settings/widgets/room_capacity_button.dart +++ b/lib/pangea/chat_settings/widgets/room_capacity_button.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; class RoomCapacityButton extends StatefulWidget { @@ -124,35 +124,28 @@ class RoomCapacityButtonState extends State { : L10n.of(context).chatCapacityExplanation, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - initialText: ((capacity != null) ? '$capacity' : ''), - keyboardType: TextInputType.number, - maxLength: 3, - validator: (value) { - if (value == null || - value.isEmpty || - int.tryParse(value) == null || - int.parse(value) < 0) { - return L10n.of(context).enterNumber; - } - if (nonAdmins != null && int.parse(value) < int.parse(nonAdmins!)) { - return spaceMode - ? L10n.of(context).spaceCapacitySetTooLow - : L10n.of(context).chatCapacitySetTooLow; - } - return null; - }, - ), - ], + initialText: ((capacity != null) ? '$capacity' : ''), + keyboardType: TextInputType.number, + maxLength: 3, + validator: (value) { + if (value.isEmpty || + int.tryParse(value) == null || + int.parse(value) < 0) { + return L10n.of(context).enterNumber; + } + if (nonAdmins != null && int.parse(value) < int.parse(nonAdmins!)) { + return spaceMode + ? L10n.of(context).spaceCapacitySetTooLow + : L10n.of(context).chatCapacitySetTooLow; + } + return null; + }, ); - if (input == null || - input.first == "" || - int.tryParse(input.first) == null) { + if (input == null || input.isEmpty || int.tryParse(input) == null) { return; } - final newCapacity = int.parse(input.first); + final newCapacity = int.parse(input); final success = await showFutureLoadingDialog( context: context, future: () => ((widget.room != null) diff --git a/lib/pangea/spaces/utils/space_code.dart b/lib/pangea/spaces/utils/space_code.dart index 8b22cfb1d..b8b56b0e8 100644 --- a/lib/pangea/spaces/utils/space_code.dart +++ b/lib/pangea/spaces/utils/space_code.dart @@ -2,11 +2,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; class SpaceCodeUtil { static const codeLength = 7; @@ -41,20 +41,18 @@ class SpaceCodeUtil { BuildContext context, PangeaController pangeaController, ) async { - final List? spaceCode = await showTextInputDialog( + final String? spaceCode = await showTextInputDialog( context: context, title: L10n.of(context).joinWithClassCode, okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField(hintText: L10n.of(context).joinWithClassCodeHint), - ], + hintText: L10n.of(context).joinWithClassCodeHint, autoSubmit: true, ); - if (spaceCode == null || spaceCode.single.isEmpty) return; + if (spaceCode == null || spaceCode.isEmpty) return; await pangeaController.classController.joinClasswithCode( context, - spaceCode.first, + spaceCode, ); } diff --git a/lib/pangea/subscription/pages/settings_subscription.dart b/lib/pangea/subscription/pages/settings_subscription.dart index c16732784..4d9153741 100644 --- a/lib/pangea/subscription/pages/settings_subscription.dart +++ b/lib/pangea/subscription/pages/settings_subscription.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -13,6 +12,7 @@ import 'package:fluffychat/pangea/subscription/controllers/subscription_controll import 'package:fluffychat/pangea/subscription/pages/settings_subscription_view.dart'; import 'package:fluffychat/pangea/subscription/utils/subscription_app_id.dart'; import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; class SubscriptionManagement extends StatefulWidget { diff --git a/lib/pangea/toolbar/enums/activity_type_enum.dart b/lib/pangea/toolbar/enums/activity_type_enum.dart index d6bbfc8e7..86336c639 100644 --- a/lib/pangea/toolbar/enums/activity_type_enum.dart +++ b/lib/pangea/toolbar/enums/activity_type_enum.dart @@ -12,7 +12,8 @@ enum ActivityTypeEnum { hiddenWordListening, lemmaId, emoji, - morphId + morphId, + // correctionPuzzle, } extension ActivityTypeExtension on ActivityTypeEnum { diff --git a/lib/pangea/toolbar/utils/update_version_dialog.dart b/lib/pangea/toolbar/utils/update_version_dialog.dart index 1c59f09e2..afd234cd2 100644 --- a/lib/pangea/toolbar/utils/update_version_dialog.dart +++ b/lib/pangea/toolbar/utils/update_version_dialog.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; Future showUpdateVersionDialog({ required Future Function() future, diff --git a/lib/pangea/toolbar/widgets/message_audio_card.dart b/lib/pangea/toolbar/widgets/message_audio_card.dart index c18e498e3..9b0ed2c1b 100644 --- a/lib/pangea/toolbar/widgets/message_audio_card.dart +++ b/lib/pangea/toolbar/widgets/message_audio_card.dart @@ -201,7 +201,18 @@ class MessageAudioCardState extends State { fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor, padding: 0, + isOverlay: true, + chatController: + widget.overlayController.widget.chatController, overlayController: widget.overlayController, + linkColor: widget.messageEvent.senderId == + widget.messageEvent.event.room.client.userID + ? Theme.of(context).brightness == Brightness.light + ? Theme.of(context).colorScheme.primaryFixed + : Theme.of(context) + .colorScheme + .onTertiaryContainer + : Theme.of(context).colorScheme.primary, ) : const CardErrorWidget( error: "Null audio file in message_audio_card", diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index 092658f7e..4fbd189cb 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -189,6 +189,12 @@ class OverlayMessage extends StatelessWidget { nextEvent: nextEvent, prevEvent: prevEvent, borderRadius: borderRadius, + timeline: timeline, + linkColor: ownMessage + ? theme.brightness == Brightness.light + ? theme.colorScheme.primaryFixed + : theme.colorScheme.onTertiaryContainer + : theme.colorScheme.primary, ), if (event.hasAggregatedEvents( timeline, diff --git a/lib/pangea/user/utils/logout.dart b/lib/pangea/user/utils/logout.dart index d1baca382..7d435a82d 100644 --- a/lib/pangea/user/utils/logout.dart +++ b/lib/pangea/user/utils/logout.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.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'; @@ -12,7 +12,6 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async { context: context, title: L10n.of(context).areYouSureYouWantToLogout, message: L10n.of(context).noBackupWarning, - isDestructiveAction: isDestructiveAction ?? false, okLabel: L10n.of(context).logout, cancelLabel: L10n.of(context).cancel, ) == diff --git a/lib/pangea/user/utils/password_forgotten.dart b/lib/pangea/user/utils/password_forgotten.dart index 618b890ed..30d3b8089 100644 --- a/lib/pangea/user/utils/password_forgotten.dart +++ b/lib/pangea/user/utils/password_forgotten.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/login/login.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../../widgets/matrix.dart'; @@ -99,7 +99,6 @@ extension PangeaPasswordForgotten on LoginController { title: L10n.of(context).weSentYouAnEmail, message: L10n.of(context).pleaseClickOnLink, okLabel: L10n.of(context).iHaveClickedOnLink, - fullyCapitalizedForMaterial: false, ); if (ok != OkCancelResult.ok) return; final data = { diff --git a/lib/utils/adaptive_bottom_sheet.dart b/lib/utils/adaptive_bottom_sheet.dart index d21ca6c44..f2addb838 100644 --- a/lib/utils/adaptive_bottom_sheet.dart +++ b/lib/utils/adaptive_bottom_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -8,25 +10,34 @@ Future showAdaptiveBottomSheet({ required Widget Function(BuildContext) builder, bool isDismissible = true, bool isScrollControlled = true, - double maxHeight = 512, bool useRootNavigator = true, -}) => - showModalBottomSheet( - context: context, - builder: builder, - // this sadly is ugly on desktops but otherwise breaks `.of(context)` calls - useRootNavigator: useRootNavigator, - isDismissible: isDismissible, - isScrollControlled: isScrollControlled, - constraints: BoxConstraints( - maxHeight: maxHeight, - maxWidth: FluffyThemes.columnWidth * 1.25, +}) { + final maxHeight = min(MediaQuery.of(context).size.height - 32, 600); + final dialogMode = FluffyThemes.isColumnMode(context); + return showModalBottomSheet( + context: context, + builder: (context) => Padding( + padding: dialogMode + ? const EdgeInsets.symmetric(vertical: 32.0) + : EdgeInsets.zero, + child: ClipRRect( + borderRadius: dialogMode + ? BorderRadius.circular(AppConfig.borderRadius) + : const BorderRadius.only( + topLeft: Radius.circular(AppConfig.borderRadius), + topRight: Radius.circular(AppConfig.borderRadius), + ), + child: builder(context), ), - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(AppConfig.borderRadius), - topRight: Radius.circular(AppConfig.borderRadius), - ), - ), - ); + ), + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + isScrollControlled: isScrollControlled, + constraints: BoxConstraints( + maxHeight: maxHeight + (dialogMode ? 64 : 0), + maxWidth: FluffyThemes.columnWidth * 1.25, + ), + backgroundColor: Colors.transparent, + clipBehavior: Clip.hardEdge, + ); +} diff --git a/lib/utils/custom_image_resizer.dart b/lib/utils/custom_image_resizer.dart index 48b803a90..f783d5cac 100644 --- a/lib/utils/custom_image_resizer.dart +++ b/lib/utils/custom_image_resizer.dart @@ -1,67 +1,103 @@ -import 'dart:typed_data'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; + import 'package:matrix/matrix.dart'; import 'package:native_imaging/native_imaging.dart' as native; +(int, int) _scaleToBox(int width, int height, {required int boxSize}) { + final fit = applyBoxFit( + BoxFit.scaleDown, + Size(width.toDouble(), height.toDouble()), + Size(boxSize.toDouble(), boxSize.toDouble()), + ).destination; + return (fit.width.round(), fit.height.round()); +} + Future customImageResizer( MatrixImageFileResizeArguments arguments, ) async { + if (kIsWeb) { + throw UnsupportedError( + 'customImageResizer only supports non-web platforms.', + ); + } + await native.init(); - late native.Image nativeImg; + + var imageBytes = arguments.bytes; + String? blurhash; + + var originalWidth = 0; + var originalHeight = 0; + var width = 0; + var height = 0; try { - nativeImg = await native.Image.loadEncoded(arguments.bytes); // load on web - } on UnsupportedError { - try { - // for the other platforms - final dartCodec = await instantiateImageCodec(arguments.bytes); - final dartFrame = await dartCodec.getNextFrame(); - final rgbaData = await dartFrame.image.toByteData(); - if (rgbaData == null) { - return null; - } - final rgba = Uint8List.view( - rgbaData.buffer, - rgbaData.offsetInBytes, - rgbaData.lengthInBytes, + // for the other platforms + final dartCodec = await instantiateImageCodec(arguments.bytes); + final frameCount = dartCodec.frameCount; + final dartFrame = await dartCodec.getNextFrame(); + final rgbaData = await dartFrame.image.toByteData(); + if (rgbaData == null) { + return null; + } + final rgba = Uint8List.view( + rgbaData.buffer, + rgbaData.offsetInBytes, + rgbaData.lengthInBytes, + ); + + width = originalWidth = dartFrame.image.width; + height = originalHeight = dartFrame.image.height; + + var nativeImg = native.Image.fromRGBA(width, height, rgba); + + dartFrame.image.dispose(); + dartCodec.dispose(); + + if (arguments.calcBlurhash) { + // scale down image for blurhashing to speed it up + final (blurW, blurH) = _scaleToBox(width, height, boxSize: 100); + final blurhashImg = nativeImg.resample( + blurW, blurH, + // nearest is unsupported... + native.Transform.bilinear, ); - final width = dartFrame.image.width; - final height = dartFrame.image.height; + blurhash = blurhashImg.toBlurhash(3, 3); - dartFrame.image.dispose(); - dartCodec.dispose(); - - nativeImg = native.Image.fromRGBA(width, height, rgba); - } catch (e, s) { - Logs().e("Could not generate preview", e, s); - rethrow; + blurhashImg.free(); } - } - final width = nativeImg.width; - final height = nativeImg.height; - - final max = arguments.maxDimension; - if (width > max || height > max) { - var w = max, h = max; - if (width > height) { - h = max * height ~/ width; + if (frameCount > 1) { + // Don't scale down animated images, since those would lose frames. + nativeImg.free(); } else { - w = max * width ~/ height; - } + final max = arguments.maxDimension; + if (width > max || height > max) { + (width, height) = _scaleToBox(width, height, boxSize: max); - final scaledImg = nativeImg.resample(w, h, native.Transform.lanczos); - nativeImg.free(); - nativeImg = scaledImg; + final scaledImg = + nativeImg.resample(width, height, native.Transform.lanczos); + nativeImg.free(); + nativeImg = scaledImg; + } + + imageBytes = await nativeImg.toJpeg(75); + nativeImg.free(); + } + } catch (e, s) { + Logs().e("Could not generate preview", e, s); } - final jpegBytes = await nativeImg.toJpeg(75); return MatrixImageFileResizedResponse( - bytes: jpegBytes, - width: nativeImg.width, - height: nativeImg.height, - blurhash: arguments.calcBlurhash ? nativeImg.toBlurhash(3, 3) : null, + bytes: imageBytes, + width: width, + height: height, + originalWidth: originalWidth, + originalHeight: originalHeight, + blurhash: blurhash, ); } diff --git a/lib/utils/file_description.dart b/lib/utils/file_description.dart new file mode 100644 index 000000000..a1d766aee --- /dev/null +++ b/lib/utils/file_description.dart @@ -0,0 +1,19 @@ +import 'package:matrix/matrix.dart'; + +extension FileDescriptionExtension on Event { + String? get fileDescription { + if (!{ + MessageTypes.File, + MessageTypes.Image, + }.contains(messageType)) { + return null; + } + final formattedBody = content.tryGet('formatted_body'); + if (formattedBody != null) return formattedBody; + + final filename = content.tryGet('filename'); + final body = content.tryGet('body'); + if (filename != body && body != null && filename != null) return body; + return null; + } +} diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart index f528f2cfd..015eca533 100644 --- a/lib/utils/file_selector.dart +++ b/lib/utils/file_selector.dart @@ -5,6 +5,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_lock.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; Future> selectFiles( BuildContext context, { @@ -14,14 +15,17 @@ Future> selectFiles( }) async { if (!PlatformInfos.isLinux) { final result = await AppLock.of(context).pauseWhile( - FilePicker.platform.pickFiles( - compressionQuality: 0, - allowMultiple: allowMultiple, - type: type.filePickerType, - allowedExtensions: type.extensions, + showFutureLoadingDialog( + context: context, + future: () => FilePicker.platform.pickFiles( + compressionQuality: 0, + allowMultiple: allowMultiple, + type: type.filePickerType, + allowedExtensions: type.extensions, + ), ), ); - return result?.xFiles ?? []; + return result.result?.xFiles ?? []; } if (allowMultiple) { diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 8bd493246..2f7ea5107 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -8,13 +8,14 @@ import 'package:http/http.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/utils/other_party_can_receive.dart'; import 'uia_request_manager.dart'; extension LocalizedExceptionExtension on Object { static String _formatFileSize(int size) { - if (size < 1024) return '$size B'; - final i = (log(size) / log(1024)).floor(); - final num = (size / pow(1024, i)); + if (size < 1000) return '$size B'; + final i = (log(size) / log(1000)).floor(); + final num = (size / pow(1000, i)); final round = num.round(); final numString = round < 10 ? num.toStringAsFixed(2) @@ -34,6 +35,9 @@ extension LocalizedExceptionExtension on Object { _formatFileSize(exception.maxFileSize), ); } + if (this is OtherPartyCanNotReceiveMessages) { + return L10n.of(context).otherPartyNotLoggedIn; + } if (this is MatrixException) { switch ((this as MatrixException).error) { case MatrixError.M_FORBIDDEN: @@ -44,6 +48,9 @@ extension LocalizedExceptionExtension on Object { case MatrixError.M_LIMIT_EXCEEDED: return L10n.of(context).tooManyRequestsWarning; default: + if (exceptionContext == ExceptionContext.joinRoom) { + return L10n.of(context).unableToJoinChat; + } return (this as MatrixException).errorMessage; } } diff --git a/lib/utils/markdown_context_builder.dart b/lib/utils/markdown_context_builder.dart index bf33f948b..ce649488e 100644 --- a/lib/utils/markdown_context_builder.dart +++ b/lib/utils/markdown_context_builder.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; + Widget markdownContextBuilder( BuildContext context, EditableTextState editableTextState, @@ -26,27 +27,21 @@ Widget markdownContextBuilder( title: l10n.addLink, okLabel: l10n.ok, cancelLabel: l10n.cancel, - textFields: [ - DialogTextField( - validator: (text) { - if (text == null || text.isEmpty) { - return l10n.pleaseFillOut; - } - try { - text.startsWith('http') - ? Uri.parse(text) - : Uri.https(text); - } catch (_) { - return l10n.invalidUrl; - } - return null; - }, - hintText: 'www...', - keyboardType: TextInputType.url, - ), - ], + validator: (text) { + if (text.isEmpty) { + return l10n.pleaseFillOut; + } + try { + text.startsWith('http') ? Uri.parse(text) : Uri.https(text); + } catch (_) { + return l10n.invalidUrl; + } + return null; + }, + hintText: 'www...', + keyboardType: TextInputType.url, ); - final urlString = input?.singleOrNull; + final urlString = input; if (urlString == null) return; final url = urlString.startsWith('http') ? Uri.parse(urlString) diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index d10a09bd3..9ad5ce5c0 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -109,7 +109,7 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { } @override - int get maxFileSize => supportsFileStoring ? 100 * 1024 * 1024 : 0; + int get maxFileSize => supportsFileStoring ? 100 * 1000 * 1000 : 0; @override bool get supportsFileStoring => !kIsWeb; diff --git a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart index a20ba712b..85bc517bc 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_matrix_dart_sdk_database/builder.dart @@ -143,7 +143,7 @@ Future _constructDatabase(Client client) async { return MatrixSdkDatabase( client.clientName, database: database, - maxFileSize: 1024 * 1024 * 10, + maxFileSize: 1000 * 1000 * 10, fileStorageLocation: fileStorageLocation?.uri, deleteFilesAfterDuration: const Duration(days: 30), ); diff --git a/lib/utils/other_party_can_receive.dart b/lib/utils/other_party_can_receive.dart new file mode 100644 index 000000000..1738785d2 --- /dev/null +++ b/lib/utils/other_party_can_receive.dart @@ -0,0 +1,21 @@ +import 'package:matrix/matrix.dart'; + +extension OtherPartyCanReceiveExtension on Room { + bool get otherPartyCanReceiveMessages { + if (!encrypted) return true; + final users = getParticipants() + .map((u) => u.id) + .where((userId) => userId != client.userID) + .toSet(); + if (users.isEmpty) return true; + + for (final userId in users) { + if (client.userDeviceKeys[userId]?.deviceKeys.values.isNotEmpty == true) { + return true; + } + } + return false; + } +} + +class OtherPartyCanNotReceiveMessages implements Exception {} diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index b8cb68ae7..5cd48f7a7 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -201,6 +201,7 @@ Future _tryPushHelper( final id = notification.roomId.hashCode; + final senderName = event.senderFromMemoryOrFallback.calcDisplayname(); // Show notification final newMessage = Message( @@ -209,7 +210,7 @@ Future _tryPushHelper( Person( bot: event.messageType == MessageTypes.Notice, key: event.senderId, - name: event.senderFromMemoryOrFallback.calcDisplayname(), + name: senderName, icon: senderAvatarFile == null ? null : ByteArrayAndroidIcon(senderAvatarFile), @@ -256,14 +257,14 @@ Future _tryPushHelper( styleInformation: messagingStyleInformation ?? MessagingStyleInformation( Person( - name: event.senderFromMemoryOrFallback.calcDisplayname(), + name: senderName, icon: roomAvatarFile == null ? null : ByteArrayAndroidIcon(roomAvatarFile), key: event.roomId, important: event.room.isFavourite, ), - conversationTitle: roomName, + conversationTitle: event.room.isDirectChat ? null : roomName, groupConversation: !event.room.isDirectChat, messages: [newMessage], ), diff --git a/lib/utils/show_scaffold_dialog.dart b/lib/utils/show_scaffold_dialog.dart new file mode 100644 index 000000000..0c09a7037 --- /dev/null +++ b/lib/utils/show_scaffold_dialog.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; + +Future showScaffoldDialog({ + required BuildContext context, + Color? barrierColor, + Color? containerColor, + double maxWidth = 480, + double maxHeight = 720, + required Widget Function(BuildContext context) builder, +}) => + showDialog( + context: context, + useSafeArea: false, + builder: FluffyThemes.isColumnMode(context) + ? (context) => Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: containerColor ?? + Theme.of(context).scaffoldBackgroundColor, + ), + clipBehavior: Clip.hardEdge, + margin: const EdgeInsets.all(16), + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + ), + child: builder(context), + ), + ) + : builder, + ); diff --git a/lib/utils/sync_status_localization.dart b/lib/utils/sync_status_localization.dart new file mode 100644 index 000000000..0cccbc7b9 --- /dev/null +++ b/lib/utils/sync_status_localization.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/utils/localized_exception_extension.dart'; + +extension SyncStatusLocalization on SyncStatusUpdate { + IconData get icon { + switch (status) { + case SyncStatus.waitingForResponse: + return Icons.hourglass_empty_outlined; + case SyncStatus.error: + return Icons.cloud_off_outlined; + case SyncStatus.processing: + return Icons.hourglass_top_outlined; + case SyncStatus.cleaningUp: + return Icons.hourglass_bottom_outlined; + case SyncStatus.finished: + return Icons.hourglass_full_outlined; + } + } + + String calcLocalizedString(BuildContext context) { + final progress = this.progress; + switch (status) { + case SyncStatus.waitingForResponse: + return L10n.of(context).waitingForServer; + case SyncStatus.error: + return ((error?.exception ?? Object()) as Object) + .toLocalizedString(context); + case SyncStatus.processing: + case SyncStatus.cleaningUp: + case SyncStatus.finished: + return progress == null + ? L10n.of(context).synchronizingPleaseWait + : L10n.of(context).synchronizingPleaseWaitCounter( + (progress * 100).round().toString(), + ); + } + } +} diff --git a/lib/utils/uia_request_manager.dart b/lib/utils/uia_request_manager.dart index 414e5e647..9187ed273 100644 --- a/lib/utils/uia_request_manager.dart +++ b/lib/utils/uia_request_manager.dart @@ -1,10 +1,11 @@ import 'dart:async'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -30,16 +31,11 @@ extension UiaRequestManager on MatrixState { title: l10n.pleaseEnterYourPassword, okLabel: l10n.ok, cancelLabel: l10n.cancel, - textFields: [ - const DialogTextField( - minLines: 1, - maxLines: 1, - obscureText: true, - hintText: '******', - ), - ], - )) - ?.single; + minLines: 1, + maxLines: 1, + obscureText: true, + hintText: '******', + )); if (input == null || input.isEmpty) { return uiaRequest.cancel(); } @@ -91,7 +87,7 @@ extension UiaRequestManager on MatrixState { if (OkCancelResult.ok == await showOkCancelAlertDialog( useRootNavigator: false, - message: l10n.pleaseFollowInstructionsOnWeb, + title: l10n.pleaseFollowInstructionsOnWeb, context: navigatorContext, okLabel: l10n.next, cancelLabel: l10n.cancel, diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index fad65b25a..2f98976fa 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; @@ -12,6 +11,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.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'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; @@ -52,7 +52,7 @@ class UrlLauncher { context: context, title: L10n.of(context).openLinkInBrowser, message: url, - okLabel: L10n.of(context).yes, + okLabel: L10n.of(context).open, cancelLabel: L10n.of(context).cancel, ); if (consent != OkCancelResult.ok) return; diff --git a/lib/utils/voip/video_renderer.dart b/lib/utils/voip/video_renderer.dart index 893553497..afdd9be2e 100644 --- a/lib/utils/voip/video_renderer.dart +++ b/lib/utils/voip/video_renderer.dart @@ -79,7 +79,7 @@ class _VideoRendererState extends State { filterQuality: FilterQuality.medium, objectFit: widget.fit, placeholderBuilder: (_) => - Container(color: Colors.white.withAlpha(50)), + Container(color: Colors.white.withAlpha(45)), ); }, ); diff --git a/lib/widgets/adaptive_dialog_action.dart b/lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart similarity index 66% rename from lib/widgets/adaptive_dialog_action.dart rename to lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart index 3df21c33b..12ce7c8ae 100644 --- a/lib/widgets/adaptive_dialog_action.dart +++ b/lib/widgets/adaptive_dialogs/adaptive_dialog_action.dart @@ -3,12 +3,14 @@ import 'package:flutter/material.dart'; class AdaptiveDialogAction extends StatelessWidget { final VoidCallback? onPressed; + final bool autofocus; final Widget child; const AdaptiveDialogAction({ super.key, required this.onPressed, required this.child, + this.autofocus = false, }); @override @@ -19,10 +21,18 @@ class AdaptiveDialogAction extends StatelessWidget { case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - return TextButton(onPressed: onPressed, child: child); + return TextButton( + onPressed: onPressed, + autofocus: autofocus, + child: child, + ); case TargetPlatform.iOS: case TargetPlatform.macOS: - return CupertinoDialogAction(onPressed: onPressed, child: child); + return CupertinoDialogAction( + onPressed: onPressed, + isDefaultAction: autofocus, + child: child, + ); } } } diff --git a/lib/widgets/adaptive_dialogs/show_modal_action_popup.dart b/lib/widgets/adaptive_dialogs/show_modal_action_popup.dart new file mode 100644 index 000000000..7e6709871 --- /dev/null +++ b/lib/widgets/adaptive_dialogs/show_modal_action_popup.dart @@ -0,0 +1,116 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +Future showModalActionPopup({ + required BuildContext context, + required List> actions, + String? title, + String? message, + String? cancelLabel, + bool useRootNavigator = true, +}) { + final theme = Theme.of(context); + + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + case TargetPlatform.linux: + return showModalBottomSheet( + isScrollControlled: true, + useRootNavigator: useRootNavigator, + context: context, + clipBehavior: Clip.hardEdge, + constraints: BoxConstraints( + maxWidth: 512, + maxHeight: MediaQuery.of(context).size.height - 32, + ), + builder: (context) => ListView( + shrinkWrap: true, + children: [ + if (title != null || message != null) ...[ + ListTile( + title: title == null + ? null + : Text( + title, + style: theme.textTheme.labelSmall, + ), + subtitle: message == null ? null : Text(message), + ), + const Divider(height: 1), + ], + ...actions.map( + (action) => ListTile( + leading: action.icon, + title: Text( + action.label, + maxLines: 1, + style: action.isDestructive + ? TextStyle( + color: theme.colorScheme.error, + fontWeight: + action.isDefaultAction ? FontWeight.bold : null, + ) + : null, + ), + onTap: () => Navigator.of(context).pop(action.value), + ), + ), + if (cancelLabel != null) ...[ + const Divider(height: 1), + ListTile( + title: Text(cancelLabel), + onTap: () => Navigator.of(context).pop(null), + ), + ], + ], + ), + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return showCupertinoModalPopup( + context: context, + useRootNavigator: useRootNavigator, + builder: (context) => ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 512), + child: CupertinoActionSheet( + title: title == null ? null : Text(title), + message: message == null ? null : Text(message), + cancelButton: cancelLabel == null + ? null + : CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(null), + child: Text(cancelLabel), + ), + actions: actions + .map( + (action) => CupertinoActionSheetAction( + isDestructiveAction: action.isDestructive, + isDefaultAction: action.isDefaultAction, + onPressed: () => Navigator.of(context).pop(action.value), + child: Text(action.label, maxLines: 1), + ), + ) + .toList(), + ), + ), + ); + } +} + +class AdaptiveModalAction { + final String label; + final T value; + Icon? icon; + final bool isDefaultAction; + final bool isDestructive; + + AdaptiveModalAction({ + required this.label, + required this.value, + this.icon, + this.isDefaultAction = false, + this.isDestructive = false, + }); +} diff --git a/lib/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart b/lib/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart new file mode 100644 index 000000000..c8fb53c38 --- /dev/null +++ b/lib/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; + +enum OkCancelResult { ok, cancel } + +Future showOkCancelAlertDialog({ + required BuildContext context, + required String title, + String? message, + String? okLabel, + String? cancelLabel, + bool isDestructive = false, + bool useRootNavigator = true, +}) => + showAdaptiveDialog( + context: context, + useRootNavigator: useRootNavigator, + builder: (context) => AlertDialog.adaptive( + title: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Text(title), + ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: message == null ? null : Text(message), + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => Navigator.of(context) + .pop(OkCancelResult.cancel), + child: Text(cancelLabel ?? L10n.of(context).cancel), + ), + AdaptiveDialogAction( + onPressed: () => + Navigator.of(context).pop(OkCancelResult.ok), + autofocus: true, + child: Text( + okLabel ?? L10n.of(context).ok, + style: isDestructive + ? TextStyle(color: Theme.of(context).colorScheme.error) + : null, + ), + ), + ], + ), + ); + +Future showOkAlertDialog({ + required BuildContext context, + required String title, + String? message, + String? okLabel, + bool useRootNavigator = true, +}) => + showAdaptiveDialog( + context: context, + useRootNavigator: useRootNavigator, + builder: (context) => AlertDialog.adaptive( + title: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Text(title), + ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: message == null ? null : Text(message), + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => + Navigator.of(context).pop(OkCancelResult.ok), + autofocus: true, + child: Text(okLabel ?? L10n.of(context).close), + ), + ], + ), + ); diff --git a/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart b/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart new file mode 100644 index 000000000..38a15cf54 --- /dev/null +++ b/lib/widgets/adaptive_dialogs/show_text_input_dialog.dart @@ -0,0 +1,155 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; + +Future showTextInputDialog({ + required BuildContext context, + required String title, + String? message, + String? okLabel, + String? cancelLabel, + bool useRootNavigator = true, + String? hintText, + String? labelText, + String? initialText, + String? prefixText, + String? suffixText, + bool obscureText = false, + bool isDestructive = false, + int? minLines, + int? maxLines, + String? Function(String input)? validator, + TextInputType? keyboardType, + int? maxLength, + bool autocorrect = true, + // #Pangea + bool autoSubmit = false, + // Pangea# +}) { + final theme = Theme.of(context); + return showAdaptiveDialog( + context: context, + useRootNavigator: useRootNavigator, + builder: (context) { + final controller = TextEditingController(text: initialText); + final error = ValueNotifier(null); + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 512), + child: AlertDialog.adaptive( + title: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Text(title), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (message != null) + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 256), + child: Text(message), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: error, + builder: (context, error, _) { + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return TextField( + controller: controller, + obscureText: obscureText, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + keyboardType: keyboardType, + autocorrect: autocorrect, + decoration: InputDecoration( + errorText: error, + hintText: hintText, + labelText: labelText, + prefixText: prefixText, + suffixText: suffixText, + ), + // #Pangea + onSubmitted: autoSubmit + ? (_) { + final input = controller.text; + final errorText = validator?.call(input); + if (errorText != null) { + error = errorText; + return; + } + Navigator.of(context).pop(input); + } + : null, + // Pangea# + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CupertinoTextField( + controller: controller, + obscureText: obscureText, + minLines: minLines, + maxLines: maxLines, + maxLength: maxLength, + keyboardType: keyboardType, + autocorrect: autocorrect, + prefix: + prefixText != null ? Text(prefixText) : null, + suffix: + suffixText != null ? Text(suffixText) : null, + placeholder: labelText ?? hintText, + ), + if (error != null) + Text( + error, + style: TextStyle( + fontSize: 11, + color: theme.colorScheme.error, + ), + textAlign: TextAlign.left, + ), + ], + ); + } + }, + ), + ], + ), + actions: [ + AdaptiveDialogAction( + onPressed: () => Navigator.of(context).pop(null), + child: Text(cancelLabel ?? L10n.of(context).cancel), + ), + AdaptiveDialogAction( + onPressed: () { + final input = controller.text; + final errorText = validator?.call(input); + if (errorText != null) { + error.value = errorText; + return; + } + Navigator.of(context).pop(input); + }, + autofocus: true, + child: Text( + okLabel ?? L10n.of(context).ok, + style: isDestructive + ? TextStyle(color: Theme.of(context).colorScheme.error) + : null, + ), + ), + ], + ), + ); + }, + ); +} diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 398d606dd..a31b9e5a3 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'matrix.dart'; @@ -62,6 +62,7 @@ class ChatSettingsPopupMenuState extends State { okLabel: L10n.of(context).ok, cancelLabel: L10n.of(context).cancel, message: L10n.of(context).archiveRoomDescription, + isDestructive: true, ); if (confirmed == OkCancelResult.ok) { final success = await showFutureLoadingDialog( diff --git a/lib/widgets/connection_status_header.dart b/lib/widgets/connection_status_header.dart deleted file mode 100644 index d12e252d4..000000000 --- a/lib/widgets/connection_status_header.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -import '../config/themes.dart'; -import '../utils/localized_exception_extension.dart'; -import 'matrix.dart'; - -class ConnectionStatusHeader extends StatefulWidget { - const ConnectionStatusHeader({super.key}); - - @override - ConnectionStatusHeaderState createState() => ConnectionStatusHeaderState(); -} - -class ConnectionStatusHeaderState extends State { - late final StreamSubscription _onSyncSub; - - @override - void initState() { - _onSyncSub = Matrix.of(context).client.onSyncStatus.stream.listen( - (_) => setState(() {}), - ); - super.initState(); - } - - @override - void dispose() { - _onSyncSub.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final client = Matrix.of(context).client; - final status = client.onSyncStatus.value ?? - const SyncStatusUpdate(SyncStatus.waitingForResponse); - final hide = client.onSync.value != null && - status.status != SyncStatus.error && - client.prevBatch != null; - - return AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - height: hide ? 0 : 36, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(color: Colors.transparent), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - value: hide ? 1.0 : status.progress, - ), - ), - const SizedBox(width: 12), - Text( - status.toLocalizedString(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: theme.colorScheme.onSurface), - ), - ], - ), - ); - } -} - -extension on SyncStatusUpdate { - String toLocalizedString(BuildContext context) { - switch (status) { - case SyncStatus.waitingForResponse: - return L10n.of(context).loadingPleaseWait; - case SyncStatus.error: - return ((error?.exception ?? Object()) as Object) - .toLocalizedString(context); - case SyncStatus.processing: - case SyncStatus.cleaningUp: - case SyncStatus.finished: - return L10n.of(context).synchronizingPleaseWait; - } - } -} diff --git a/lib/widgets/error_widget.dart b/lib/widgets/error_widget.dart index af3f90248..3bbab55e6 100644 --- a/lib/widgets/error_widget.dart +++ b/lib/widgets/error_widget.dart @@ -39,7 +39,7 @@ class _FluffyChatErrorWidgetState extends State { child: Placeholder( child: Center( child: Material( - color: Colors.white.withAlpha(225), + color: Colors.white.withAlpha(230), borderRadius: BorderRadius.circular(8), ), ), diff --git a/lib/widgets/future_loading_dialog.dart b/lib/widgets/future_loading_dialog.dart index feffd3b3e..61a137c25 100644 --- a/lib/widgets/future_loading_dialog.dart +++ b/lib/widgets/future_loading_dialog.dart @@ -6,7 +6,7 @@ import 'package:async/async.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; /// Displays a loading dialog which reacts to the given [future]. The dialog /// will be dismissed and the value will be returned when the future completes. diff --git a/lib/widgets/local_notifications_extension.dart b/lib/widgets/local_notifications_extension.dart index a59a540fa..672e3163d 100644 --- a/lib/widgets/local_notifications_extension.dart +++ b/lib/widgets/local_notifications_extension.dart @@ -20,8 +20,8 @@ extension LocalNotificationsExtension on MatrixState { ..src = 'assets/assets/sounds/notification.ogg' ..load(); - void showLocalNotification(EventUpdate eventUpdate) async { - final roomId = eventUpdate.roomID; + void showLocalNotification(Event event) async { + final roomId = event.room.id; if (activeRoomId == roomId) { if (kIsWeb && webHasFocus) return; if (PlatformInfos.isDesktop && @@ -29,19 +29,13 @@ extension LocalNotificationsExtension on MatrixState { return; } } - final room = client.getRoomById(roomId); - if (room == null) { - Logs().w('Can not display notification for unknown room $roomId'); - return; - } - if (room.notificationCount == 0) return; - final event = Event.fromJson(eventUpdate.content, room); - final title = room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); + final title = + event.room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); final body = await event.calcLocalizedBody( MatrixLocals(L10n.of(context)), - withSenderNamePrefix: - !room.isDirectChat || room.lastEvent?.senderId == client.userID, + withSenderNamePrefix: !event.room.isDirectChat || + event.room.lastEvent?.senderId == client.userID, plaintextBody: true, hideReply: true, hideEdit: true, @@ -107,14 +101,14 @@ extension LocalNotificationsExtension on MatrixState { .singleWhere((a) => a.name == actionStr); switch (action) { case DesktopNotificationActions.seen: - room.setReadMarker( + event.room.setReadMarker( event.eventId, mRead: event.eventId, public: AppConfig.sendPublicReadReceipts, ); break; case DesktopNotificationActions.openChat: - context.go('/rooms/${room.id}'); + context.go('/rooms/${event.room.id}'); break; } }); diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 6b3a92af5..40ae171b8 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -19,7 +18,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/any_state_holder.dart'; import 'package:fluffychat/utils/client_manager.dart'; @@ -27,14 +25,15 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dar import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/uia_request_manager.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/local_notifications_extension.dart'; import '../config/app_config.dart'; import '../config/setting_keys.dart'; import '../pages/key_verification/key_verification_dialog.dart'; import '../utils/account_bundles.dart'; import '../utils/background_push.dart'; -import 'local_notifications_extension.dart'; // import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -194,18 +193,6 @@ class MatrixState extends State with WidgetsBindingObserver { Client? getClientByName(String name) => widget.clients.firstWhereOrNull((c) => c.clientName == name); - Map? get shareContent => _shareContent; - - set shareContent(Map? content) { - _shareContent = content; - onShareContentChanged.add(_shareContent); - } - - Map? _shareContent; - - final StreamController?> onShareContentChanged = - StreamController.broadcast(); - final onRoomKeyRequestSub = {}; final onKeyVerificationRequestSub = {}; final onNotification = {}; @@ -368,19 +355,8 @@ class MatrixState extends State with WidgetsBindingObserver { if (PlatformInfos.isWeb || PlatformInfos.isLinux) { c.onSync.stream.first.then((s) { html.Notification.requestPermission(); - onNotification[name] ??= c.onEvent.stream - .where( - (e) => - e.type == EventUpdateType.timeline && - [EventTypes.Message, EventTypes.Sticker, EventTypes.Encrypted] - .contains(e.content['type']) && - e.content['sender'] != c.userID - // #Pangea - && - !e.content['content']?.containsKey(ModelKey.transcription), - // Pangea#, - ) - .listen(showLocalNotification); + onNotification[name] ??= + c.onNotification.stream.listen(showLocalNotification); }); } } @@ -411,16 +387,11 @@ class MatrixState extends State with WidgetsBindingObserver { this, onFcmError: (errorMsg, {Uri? link}) async { final result = await showOkCancelAlertDialog( - // #Pangea - useRootNavigator: false, - // Pangea# - barrierDismissible: true, context: FluffyChatApp .router.routerDelegate.navigatorKey.currentContext ?? context, title: L10n.of(context).pushNotificationsNotAvailable, message: errorMsg, - fullyCapitalizedForMaterial: false, okLabel: link == null ? L10n.of(context).ok : L10n.of(context).learnMore, cancelLabel: L10n.of(context).doNotShowAgain, @@ -540,7 +511,7 @@ class MatrixState extends State with WidgetsBindingObserver { Future dehydrateAction(BuildContext context) async { final response = await showOkCancelAlertDialog( context: context, - isDestructiveAction: true, + isDestructive: true, title: L10n.of(context).dehydrate, message: L10n.of(context).dehydrateWarning, ); diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 1d5625998..230f3eb09 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -110,7 +111,7 @@ class _MxcImageState extends State { } try { await _load(); - } catch (_) { + } on IOException catch (_) { if (!mounted) return; await Future.delayed(widget.retryDuration); _tryLoad(_); diff --git a/lib/widgets/permission_slider_dialog.dart b/lib/widgets/permission_slider_dialog.dart index e568717aa..cdff7b5cb 100644 --- a/lib/widgets/permission_slider_dialog.dart +++ b/lib/widgets/permission_slider_dialog.dart @@ -1,9 +1,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; + Future showPermissionChooser( BuildContext context, { int currentLevel = 0, @@ -11,24 +12,20 @@ Future showPermissionChooser( final customLevel = await showTextInputDialog( context: context, title: L10n.of(context).setPermissionsLevel, - textFields: [ - DialogTextField( - initialText: currentLevel.toString(), - keyboardType: TextInputType.number, - autocorrect: false, - validator: (text) { - if (text == null) { - return L10n.of(context).pleaseEnterANumber; - } - final level = int.tryParse(text); - if (level == null) { - return L10n.of(context).pleaseEnterANumber; - } - return null; - }, - ), - ], + initialText: currentLevel.toString(), + keyboardType: TextInputType.number, + autocorrect: false, + validator: (text) { + if (text.isEmpty) { + return L10n.of(context).pleaseEnterANumber; + } + final level = int.tryParse(text); + if (level == null) { + return L10n.of(context).pleaseEnterANumber; + } + return null; + }, ); if (customLevel == null) return null; - return int.tryParse(customLevel.first); + return int.tryParse(customLevel); } diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index bd9e27a68..65efae6eb 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -135,27 +135,26 @@ class PublicRoomBottomSheetState extends State { chunk?.name ?? roomAlias ?? chunk?.roomId ?? 'Unknown', overflow: TextOverflow.fade, ), - leading: IconButton( - icon: const Icon(Icons.arrow_downward_outlined), - onPressed: Navigator.of(context, rootNavigator: false).pop, - tooltip: L10n.of(context).close, + leading: Center( + child: CloseButton( + onPressed: Navigator.of(context, rootNavigator: false).pop, + ), ), // #Pangea - // actions: [ - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16.0), - // child: IconButton( - // icon: Icon(Icons.adaptive.share_outlined), - // onPressed: () => FluffyShare.share( - // // #Pangea - // // 'https://matrix.to/#/${roomAlias ?? chunk?.roomId}', - // '${Environment.frontendURL}/#/rooms/${chunk?.roomId}', - // // Pangea# - // context, - // ), - // ), - // ), - // ], + // actions: roomAlias == null + // ? null + // : [ + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 8.0), + // child: IconButton( + // icon: const Icon(Icons.qr_code_rounded), + // onPressed: () => showQrCodeViewer( + // context, + // roomAlias, + // ), + // ), + // ), + // ], // Pangea# ), body: FutureBuilder( @@ -201,6 +200,7 @@ class PublicRoomBottomSheetState extends State { ), style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, + iconColor: theme.colorScheme.onSurface, ), label: Text( roomLink ?? '...', @@ -216,6 +216,7 @@ class PublicRoomBottomSheetState extends State { ), style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, + iconColor: theme.colorScheme.onSurface, ), label: Text( L10n.of(context).countParticipants( diff --git a/lib/widgets/qr_code_viewer.dart b/lib/widgets/qr_code_viewer.dart new file mode 100644 index 000000000..25fd1f531 --- /dev/null +++ b/lib/widgets/qr_code_viewer.dart @@ -0,0 +1,142 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:image/image.dart'; +import 'package:matrix/matrix.dart'; +import 'package:pretty_qr_code/pretty_qr_code.dart'; +import 'package:qr_image/qr_image.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import '../config/themes.dart'; + +Future showQrCodeViewer( + BuildContext context, + String content, +) => + showDialog( + context: context, + builder: (context) => QrCodeViewer(content: content), + ); + +class QrCodeViewer extends StatelessWidget { + final String content; + + const QrCodeViewer({required this.content, super.key}); + + void _save(BuildContext context) async { + final imageResult = await showFutureLoadingDialog( + context: context, + future: () async { + // #Pangea + // final inviteLink = 'https://matrix.to/#/$content'; + final inviteLink = '${Environment.frontendURL}/#/rooms/$content'; + // Pangea# + final image = QRImage( + inviteLink, + size: 256, + radius: 1, + ).generate(); + return compute(encodePng, image); + }, + ); + final bytes = imageResult.result; + if (bytes == null) return; + if (!context.mounted) return; + + MatrixImageFile( + bytes: bytes, + name: 'QR_Code_$content.png', + mimeType: 'image/png', + ).save(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final inviteLink = 'https://matrix.to/#/$content'; + return Scaffold( + backgroundColor: Colors.black.withAlpha(128), + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + leading: IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withAlpha(128), + ), + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, + color: Colors.white, + tooltip: L10n.of(context).close, + ), + backgroundColor: Colors.transparent, + actions: [ + IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withAlpha(128), + ), + icon: Icon(Icons.adaptive.share_outlined), + onPressed: () => FluffyShare.share( + inviteLink, + context, + ), + color: Colors.white, + tooltip: L10n.of(context).share, + ), + const SizedBox(width: 8), + IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withAlpha(128), + ), + icon: const Icon(Icons.download_outlined), + onPressed: () => _save(context), + color: Colors.white, + tooltip: L10n.of(context).downloadFile, + ), + const SizedBox(width: 8), + ], + ), + body: Center( + child: Container( + margin: const EdgeInsets.all(32.0), + padding: const EdgeInsets.all(32.0), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: FluffyThemes.columnWidth), + child: PrettyQrView.data( + data: inviteLink, + decoration: PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + roundFactor: 1, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), + ), + const SizedBox(height: 8.0), + SelectableText( + content, + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/share_scaffold_dialog.dart b/lib/widgets/share_scaffold_dialog.dart new file mode 100644 index 000000000..21d6b4020 --- /dev/null +++ b/lib/widgets/share_scaffold_dialog.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +abstract class ShareItem {} + +class TextShareItem extends ShareItem { + final String value; + TextShareItem(this.value); +} + +class ContentShareItem extends ShareItem { + final Map value; + ContentShareItem(this.value); +} + +class FileShareItem extends ShareItem { + final XFile value; + FileShareItem(this.value); +} + +class ShareScaffoldDialog extends StatefulWidget { + final List items; + + const ShareScaffoldDialog({required this.items, super.key}); + + @override + State createState() => _ShareScaffoldDialogState(); +} + +class _ShareScaffoldDialogState extends State { + final TextEditingController _filterController = TextEditingController(); + + String? selectedRoomId; + + void _toggleRoom(String roomId) { + setState(() { + selectedRoomId = roomId; + }); + } + + void _forwardAction() async { + final roomId = selectedRoomId; + if (roomId == null) { + throw Exception( + 'Started forward action before room was selected. This should never happen.', + ); + } + while (context.canPop()) { + context.pop(); + } + context.go('/rooms/$roomId', extra: widget.items); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final rooms = Matrix.of(context) + .client + .rooms + .where( + (room) => + room.canSendDefaultMessages && + !room.isSpace && + room.membership == Membership.join, + ) + .toList(); + final filter = _filterController.text.trim().toLowerCase(); + return Scaffold( + appBar: AppBar( + leading: Center(child: CloseButton(onPressed: context.pop)), + title: Text(L10n.of(context).share), + ), + body: CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + toolbarHeight: 72, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + title: TextField( + controller: _filterController, + onChanged: (_) => setState(() {}), + textInputAction: TextInputAction.search, + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context).search, + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: IconButton( + onPressed: () {}, + icon: Icon( + Icons.search_outlined, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), + ), + ), + SliverList.builder( + itemCount: rooms.length, + itemBuilder: (context, i) { + final room = rooms[i]; + final displayname = room.getLocalizedDisplayname( + MatrixLocals(L10n.of(context)), + ); + final value = selectedRoomId == room.id; + final filterOut = !displayname.toLowerCase().contains(filter); + if (!value && filterOut) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Opacity( + opacity: filterOut ? 0.5 : 1, + child: CheckboxListTile.adaptive( + checkboxShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(90), + ), + controlAffinity: ListTileControlAffinity.trailing, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + secondary: Avatar( + mxContent: room.avatar, + name: displayname, + size: Avatar.defaultSize * 0.75, + ), + title: Text(displayname), + value: selectedRoomId == room.id, + onChanged: (_) => _toggleRoom(room.id), + ), + ), + ); + }, + ), + ], + ), + bottomNavigationBar: AnimatedSize( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: selectedRoomId == null + ? const SizedBox.shrink() + : Material( + elevation: 8, + shadowColor: theme.appBarTheme.shadowColor, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _forwardAction, + child: Text(L10n.of(context).forward), + ), + ), + ), + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 011b37ee1..0487a6c5b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,6 @@ import FlutterMacOS import Foundation -import appkit_ui_element_colors import audio_session import audioplayers_darwin import device_info_plus @@ -24,8 +23,6 @@ import flutter_webrtc import geolocator_apple import in_app_purchase_storekit import just_audio -import macos_ui -import macos_window_utils import package_info_plus import pasteboard import path_provider_foundation @@ -45,7 +42,6 @@ import wakelock_plus import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) @@ -64,8 +60,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) - MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) - MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 9471ada4a..c4386aaae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.46" - adaptive_dialog: - dependency: "direct main" - description: - name: adaptive_dialog - sha256: "817ff9b4bb441434d1fcb39a8d4492e50be456cd3507e4f19c5c7455c9e279e0" - url: "https://pub.dev" - source: hosted - version: "2.1.0" analyzer: dependency: transitive description: @@ -57,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - appkit_ui_element_colors: - dependency: transitive - description: - name: appkit_ui_element_colors - sha256: c3e50f900aae314d339de489535736238627071457c4a4a2dbbb1545b4f04f22 - url: "https://pub.dev" - source: hosted - version: "1.0.0" archive: dependency: "direct main" description: @@ -362,14 +346,13 @@ packages: source: hosted version: "1.0.1" dart_webrtc: - dependency: "direct overridden" + dependency: transitive description: - path: "." - ref: f27d27c7af41ceeebe31b295af4fb38e7b4d793e - resolved-ref: f27d27c7af41ceeebe31b295af4fb38e7b4d793e - url: "https://github.com/flutter-webrtc/dart-webrtc.git" - source: git - version: "1.4.9" + name: dart_webrtc + sha256: e65506edb452148220efab53d8d2f8bb9d827bd8bcd53cf3a3e6df70b27f3d86 + url: "https://pub.dev" + source: hosted + version: "1.4.10" dbus: dependency: transitive description: @@ -462,10 +445,10 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" excel: dependency: "direct main" description: @@ -715,15 +698,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.1" - flutter_html: - dependency: "direct main" - description: - path: "." - ref: master - resolved-ref: e07b904d70a98d678882ac5c4c2da896a67a688c - url: "https://github.com/SherpaMiguel/flutter_html" - source: git - version: "3.0.0-beta.2" flutter_keyboard_visibility: dependency: transitive description: @@ -1097,14 +1071,6 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.0" - gradient_borders: - dependency: transitive - description: - name: gradient_borders - sha256: b1cd969552c83f458ff755aa68e13a0327d09f06c3f42f471b423b01427f21f8 - url: "https://pub.dev" - source: hosted - version: "1.0.1" graphs: dependency: transitive description: @@ -1310,14 +1276,6 @@ packages: description: flutter source: sdk version: "0.0.0" - intersperse: - dependency: transitive - description: - name: intersperse - sha256: "2f8a905c96f6cbba978644a3d5b31b8d86ddc44917662df7d27a61f3df66a576" - url: "https://pub.dev" - source: hosted - version: "2.0.0" intl: dependency: "direct main" description: @@ -1446,14 +1404,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - list_counter: - dependency: transitive - description: - name: list_counter - sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.dev" - source: hosted - version: "1.0.2" lists: dependency: transitive description: @@ -1478,22 +1428,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - macos_ui: - dependency: transitive - description: - name: macos_ui - sha256: "91c7f3427f763fd96b65831342b896b18751140e6bf55f8572fcb41f7b30bcab" - url: "https://pub.dev" - source: hosted - version: "2.0.7" - macos_window_utils: - dependency: transitive - description: - name: macos_window_utils - sha256: "230be594d26f6dee92c5a1544f4242d25138a5bfb9f185b27f14de3949ef0be8" - url: "https://pub.dev" - source: hosted - version: "1.5.0" markdown: dependency: transitive description: @@ -1531,10 +1465,10 @@ packages: description: path: "." ref: main - resolved-ref: "16a089ad229671f2367c8927b9edae776004a3ae" + resolved-ref: "03be44cc13cb15a1b6fa586589eb8c243979d381" url: "https://github.com/pangeachat/matrix-dart-sdk.git" source: git - version: "0.36.0" + version: "0.37.0" meta: dependency: transitive description: @@ -1595,10 +1529,10 @@ packages: dependency: transitive description: name: olm - sha256: "37948a6576949256f3ee1d0063d5b408634ff7e452b9a5c2f6410f9d7ced1c20" + sha256: "3306bf534ceb914fd148b3b4a3d603fb5e067b2e6da8304025b47c24cfdf6b46" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" open_file: dependency: "direct main" description: @@ -1667,10 +1601,10 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1943,6 +1877,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + qr_image: + dependency: "direct main" + description: + name: qr_image + sha256: c3cd2ac2c6cd6b14604c97b45c477b18988b6518f72120fa04418fc54e3b0d76 + url: "https://pub.dev" + source: hosted + version: "1.0.0" random_string: dependency: transitive description: @@ -2328,10 +2270,10 @@ packages: dependency: "direct main" description: name: swipe_to_action - sha256: "0914f78df07a15b5fd97e800036fd63a2bcd4dbe67a4a514a597303806a361ea" + sha256: "05289a2bff6a0227deeff382afa1c46643d1f077e678d78f76440e153ea1ef7d" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.0" sync_http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 337bedc18..fbe619add 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,6 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - adaptive_dialog: ^2.1.0 animations: ^2.0.11 archive: ^3.6.1 async: ^2.11.0 @@ -42,15 +41,6 @@ dependencies: flutter_cache_manager: ^3.4.1 flutter_foreground_task: ^6.1.3 flutter_highlighter: ^0.1.1 - # #Pangea - # Fix for https://github.com/Sub6Resources/flutter_html/issues/1314 - # flutter_html: ^3.0.0-beta.2 - flutter_html: - git: - url: https://github.com/SherpaMiguel/flutter_html - ref: master - # flutter_html_table: ^3.0.0-beta.2 - # Pangea# flutter_linkify: ^6.0.0 flutter_local_notifications: ^17.2.3 flutter_localizations: @@ -83,7 +73,7 @@ dependencies: git: url: https://github.com/pangeachat/matrix-dart-sdk.git # repo ref: main # branch - # matrix: ^0.35.0 + # matrix: ^0.37.0 # Pangea# mime: ^1.0.6 native_imaging: ^0.1.1 @@ -97,6 +87,7 @@ dependencies: provider: ^6.0.2 punycode: ^1.0.0 qr_code_scanner: ^1.0.1 + qr_image: ^1.0.0 receive_sharing_intent: ^1.8.1 record: ^5.1.2 scroll_to_index: ^3.0.1 @@ -105,7 +96,7 @@ dependencies: slugify: ^2.0.0 sqflite_common_ffi: ^2.3.3 sqlcipher_flutter_libs: ^0.6.1 - swipe_to_action: ^0.2.0 + swipe_to_action: ^0.3.0 tor_detector_web: ^1.1.0 uni_links: ^0.5.1 unifiedpush: ^5.0.1 @@ -183,16 +174,19 @@ flutter: - assets/js/package/ fonts: - - family: Roboto + - family: Ubuntu fonts: - - asset: fonts/Roboto/Roboto-Regular.ttf - - asset: fonts/Roboto/Roboto-Italic.ttf + - asset: fonts/Ubuntu/Ubuntu-Regular.ttf + - asset: fonts/Ubuntu/Ubuntu-Bold.ttf + weight: 600 + - asset: fonts/Ubuntu/Ubuntu-Italic.ttf style: italic - - asset: fonts/Roboto/Roboto-Bold.ttf - weight: 700 - - family: RobotoMono + - asset: fonts/Ubuntu/Ubuntu-BoldItalic.ttf + weight: 600 + style: italic + - family: UbuntuMono fonts: - - asset: fonts/Roboto/RobotoMono-Regular.ttf + - asset: fonts/Ubuntu/UbuntuMono-Regular.ttf - family: NotoEmoji fonts: - asset: fonts/NotoEmoji/NotoColorEmoji.ttf @@ -223,9 +217,4 @@ msix_config: install_certificate: false dependency_overrides: - # For Flutter 3.27 until https://github.com/flutter-webrtc/dart-webrtc/pull/54 is merged - dart_webrtc: - git: - url: https://github.com/flutter-webrtc/dart-webrtc.git - ref: f27d27c7af41ceeebe31b295af4fb38e7b4d793e win32: 5.5.3 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 717f4ecb2..87efdcd88 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -69,13 +69,13 @@ parts: rm -rf build craftctl default build-packages: - - libjsoncpp-dev - - curl - stage-packages: - libsecret-1-dev - libjsoncpp-dev - libssl-dev - #- libwebkit2gtk-4.1-dev + - curl + stage-packages: + - libsecret-1-0 + - libjsoncpp25 slots: dbus-svc: diff --git a/web/index.html b/web/index.html index 59ba23f0e..44446e712 100644 --- a/web/index.html +++ b/web/index.html @@ -25,7 +25,7 @@ - + diff --git a/web/splash/style.css b/web/splash/style.css index 5543680c4..2fce8888b 100644 --- a/web/splash/style.css +++ b/web/splash/style.css @@ -41,7 +41,6 @@ html { margin: 0; height: 100%; background: #000000; - background-image: url("img/dark-background.png"); background-size: 100% 100%; } }