Fluffychat merge (#1685)

chore: Merge upstream changes

---------

Signed-off-by: Krille <c.kussowski@famedly.com>
Co-authored-by: krille-chan <christian-kussowski@posteo.de>
Co-authored-by: Krille <c.kussowski@famedly.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: fadelkon <fadelkon@posteo.net>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Edgars Andersons <Edgars+Weblate@gaitenis.id.lv>
Co-authored-by: josé m <correoxm@disroot.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Christian <christian-pauly@posteo.de>
Co-authored-by: - <hitekex@yandex.ru>
Co-authored-by: Angelo Schirinzi <Odi-3@users.noreply.hosted.weblate.org>
Co-authored-by: xabirequejo <xabi.rn@gmail.com>
Co-authored-by: Piotr Orzechowski <piotr@orzechowski.tech>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Tewuzij <tenajeza@outlook.com>
Co-authored-by: goknarbahceli <goknarbahceli@proton.me>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: Erin <erin@erindesu.cz>
Co-authored-by: EpicKiwi <me@epickiwi.fr>
Co-authored-by: Christian Tietze <me@christiantietze.de>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
ggurdin 2025-02-03 12:36:46 -05:00 committed by GitHub
parent 6dd984a23b
commit 49e586a7ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
152 changed files with 7483 additions and 5214 deletions

View file

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

View file

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

View file

@ -86,15 +86,6 @@
<data android:scheme="com.talktolearn.chat" android:host="chat" />
</intent-filter>
<!-- App can receive shared files -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="*/*"
android:scheme="content" />
</intent-filter>
<!-- App can receive shared text -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
@ -102,32 +93,16 @@
<data android:mimeType="text/*" />
</intent-filter>
<!-- App can receive shared documents -->
<!-- App can receive shared any type of files -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="document/*" />
<data android:mimeType="*/*" />
</intent-filter>
<!-- App can receive shared images -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<!-- App can receive shared videos -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<!-- App can receive shared audios -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>

View file

@ -2885,5 +2885,14 @@
"pleaseFillOut": "من فضلك قم بتعبئته",
"@pleaseFillOut": {},
"unableToJoinChat": "يتعذر الانضمام إلى الدردشة. ربما يكون الطرف الآخر قد أغلق المحادثة بالفعل.",
"@unableToJoinChat": {}
"@unableToJoinChat": {},
"sendImages": "إرسال {count} صورة",
"@sendImages": {
"type": "text",
"placeholders": {
"count": {}
}
},
"compress": "ضغط",
"@compress": {}
}

View file

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

View file

@ -2437,5 +2437,12 @@
"@presencesToggle": {
"type": "text",
"placeholders": {}
},
"aboutHomeserver": "O {homeserver}",
"@aboutHomeserver": {
"type": "text",
"placeholders": {
"homeserver": {}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2888,5 +2888,14 @@
"invalidUrl": "URL neamhbhailí",
"@invalidUrl": {},
"unableToJoinChat": "Ní féidir páirt a ghlacadh sa chomhrá. Bfhé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": {}
}
}
}

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -2883,5 +2883,14 @@
"version": "Версия",
"@version": {},
"website": "Сайт",
"@website": {}
"@website": {},
"sendImages": "Отправить {count} изображений",
"@sendImages": {
"type": "text",
"placeholders": {
"count": {}
}
},
"compress": "Сжатие",
"@compress": {}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -2885,5 +2885,14 @@
"addLink": "Додати посилання",
"@addLink": {},
"unableToJoinChat": "Неможливо приєднатися до чату. Можливо, інша сторона вже закрила розмову.",
"@unableToJoinChat": {}
"@unableToJoinChat": {},
"sendImages": "Надіслати {count} зображення",
"@sendImages": {
"type": "text",
"placeholders": {
"count": {}
}
},
"compress": "Стиснути",
"@compress": {}
}

View file

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

View file

@ -2885,5 +2885,14 @@
"invalidUrl": "无效 url",
"@invalidUrl": {},
"unableToJoinChat": "无法加入聊天。可能其他方面已经关闭了对话。",
"@unableToJoinChat": {}
"@unableToJoinChat": {},
"sendImages": "发送 {count} 张图片",
"@sendImages": {
"type": "text",
"placeholders": {
"count": {}
}
},
"compress": "压缩",
"@compress": {}
}

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

96
fonts/Ubuntu/UFL.txt Normal file
View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -27,11 +27,11 @@
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>1</integer>
<integer>10</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<integer>10</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>

View file

@ -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 = "<group>"; };
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 = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
@ -78,6 +81,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
C1005C47261071B5002F4F32 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };

View file

@ -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<String?> 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<ShareItem>
? state.extra as List<ShareItem>
: 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(

View file

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

View file

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

View file

@ -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<bool?> show(BuildContext context) => showAdaptiveBottomSheet(
context: context,
builder: (context) => this,
maxHeight: 600,
);
@override
@ -133,7 +132,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
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<BootstrapDialog> {
? 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<BootstrapDialog> {
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<BootstrapDialog> {
.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<BootstrapDialog> {
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<BootstrapDialog> {
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<BootstrapDialog> {
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<bool>(false),
child: Text(L10n.of(context).close),
@ -470,7 +470,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
],
);
buttons.add(
OutlinedButton(
ElevatedButton(
onPressed: () =>
Navigator.of(context, rootNavigator: false).pop<bool>(false),
child: Text(L10n.of(context).close),
@ -491,13 +491,17 @@ class BootstrapDialogState extends State<BootstrapDialog> {
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,
],
),
),
),
);

View file

@ -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<ShareItem>? 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<ShareItem>? 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<ChatPageWithRoom>
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<ChatPageWithRoom>
// ),
// );
// }
// 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<ChatPageWithRoom>
setReadMarker(eventId: mostRecentEventId);
}
void updateScrollController() {
void _updateScrollController() {
if (!mounted) {
return;
}
@ -267,22 +264,63 @@ class ChatController extends State<ChatPageWithRoom>
}
}
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<FileShareItem>()
.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<ChatPageWithRoom>
),
);
// 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<ChatPageWithRoom>
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<ChatPageWithRoom>
scrollUpBannerEventId = null;
});
void showScrollUpMaterialBanner(String eventId) => setState(() {
void _showScrollUpMaterialBanner(String eventId) => setState(() {
scrollUpBannerEventId = eventId;
});
@ -448,7 +486,7 @@ class ChatController extends State<ChatPageWithRoom>
);
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<ChatPageWithRoom>
setReadMarker();
}
Future<void>? setReadMarkerFuture;
Future<void>? _setReadMarkerFuture;
void setReadMarker({String? eventId}) {
// #Pangea
@ -485,7 +523,7 @@ class ChatController extends State<ChatPageWithRoom>
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<ChatPageWithRoom>
}
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<ChatPageWithRoom>
);
});
// Pangea#
if (eventId == null || eventId == timeline.room.lastEvent?.eventId) {
Matrix.of(context).backgroundPush?.cancelNotification(roomId);
}
@ -554,7 +589,7 @@ class ChatController extends State<ChatPageWithRoom>
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<ChatPageWithRoom>
}) 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<ChatPageWithRoom>
// 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<ChatPageWithRoom>
setState(() {
sendController.text = pendingText;
inputTextIsEmpty = pendingText.isEmpty;
_inputTextIsEmpty = pendingText.isEmpty;
replyEvent = null;
editEvent = null;
pendingText = '';
@ -945,7 +980,7 @@ class ChatController extends State<ChatPageWithRoom>
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<ChatPageWithRoom>
);
}
String getSelectedEventString() {
String _getSelectedEventString() {
var copyString = '';
if (selectedEvents.length == 1) {
return selectedEvents.first
@ -977,7 +1012,7 @@ class ChatController extends State<ChatPageWithRoom>
}
void copyEventsAction() {
Clipboard.setData(ClipboardData(text: getSelectedEventString()));
Clipboard.setData(ClipboardData(text: _getSelectedEventString()));
setState(() {
showEmojiPicker = false;
// #Pangea
@ -992,23 +1027,22 @@ class ChatController extends State<ChatPageWithRoom>
// #Pangea
clearSelectedEvents();
// Pangea#
final score = await showConfirmationDialog<int>(
final score = await showModalActionPopup<int>(
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<ChatPageWithRoom>
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<ChatPageWithRoom>
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<ChatPageWithRoom>
}
// 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<ChatPageWithRoom>
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#
)
: <String>[];
: 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<ChatPageWithRoom>
}
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<ChatPageWithRoom>
duration: FluffyThemes.animationDuration,
preferPosition: AutoScrollPosition.middle,
);
updateScrollController();
_updateScrollController();
}
void scrollDown() async {
@ -1397,18 +1427,20 @@ class ChatController extends State<ChatPageWithRoom>
}
// 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<ChatPageWithRoom>
}
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<ChatPageWithRoom>
);
}
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<ChatPageWithRoom>
}
}
var inputTextIsEmpty = true;
bool _inputTextIsEmpty = true;
bool get isArchived =>
{Membership.leave, Membership.ban}.contains(room.membership);
@ -1664,21 +1696,21 @@ class ChatController extends State<ChatPageWithRoom>
}
});
}
final callType = await showModalActionSheet<CallType>(
final callType = await showModalActionPopup<CallType>(
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<ChatPageWithRoom>
onSelectMessage(event);
});
}
// Pangea#
// final List<int> 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<bool> displayChatDetailsColumn;
late final ValueNotifier<bool> _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<ChatPageWithRoom>
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: ValueListenableBuilder(
valueListenable: displayChatDetailsColumn,
valueListenable: _displayChatDetailsColumn,
builder: (context, displayChatDetailsColumn, _) {
if (!FluffyThemes.isThreeColumnMode(context) ||
room.membership != Membership.join ||

View file

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

View file

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

View file

@ -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: <Widget>[
// #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: <Widget>[
// 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) =>
<PopupMenuEntry<String>>[
//#Pangea
if (controller.pangeaController.permissionsController
.canShareFile(controller.roomId))
//Pangea#
PopupMenuItem<String>(
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<String>(
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<String>(
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<String>(
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<String>(
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<String>(
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<String>(
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,
// ),
// ),

View file

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

View file

@ -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<AudioPlayerWidget> {
: _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<AudioPlayerWidget> {
final statusText = this.statusText ??= _durationString ?? '00:00';
final audioPlayer = this.audioPlayer;
final body = widget.event?.content.tryGet<String>('body') ??
widget.event?.content.tryGet<String>('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<AudioPlayerWidget> {
),
),
// #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(),
),

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<EventVideoPlayer> {
final blurHash = (widget.event.infoMap as Map<String, dynamic>)
.tryGet<String>('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(),
),
),
],
);
}
}

View file

@ -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<String>(
: await showModalActionPopup<String>(
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,

View file

@ -206,7 +206,7 @@ class RecordingDialogState extends State<RecordingDialog> {
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<RecordingDialog> {
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<RecordingDialog> {
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: <Widget>[
Text(L10n.of(context).send.toUpperCase()),
const SizedBox(width: 4),
const Icon(Icons.send_outlined, size: 15),
],
),
child: Text(L10n.of(context).send),
),
],
);

View file

@ -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<SendFileDialog> {
bool compress = true;
/// Images smaller than 20kb don't need compression.
static const int minSizeToCompress = 20 * 1024;
static const int minSizeToCompress = 20 * 1000;
Future<void> _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<SendFileDialog> {
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<SendFileDialog> {
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,
),

View file

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

View file

@ -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<ChatAccessSettings> {
);
final capabilities = capabilitiesResult.result;
if (capabilities == null) return;
final newVersion = await showConfirmationDialog<String>(
final newVersion = await showModalActionPopup<String>(
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<ChatAccessSettings> {
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<ChatAccessSettings> {
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';

View file

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

View file

@ -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<ChatDetails> {
// 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<ChatDetails> {
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<ChatDetails> {
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<AvatarAction>(
? actions.single.value
: await showModalActionPopup<AvatarAction>(
context: context,
title: L10n.of(context).editRoomAvatar,
cancelLabel: L10n.of(context).cancel,
actions: actions,
);
if (action == null) return;

View file

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

View file

@ -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<ChatEncryptionSettings> {
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]!

View file

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

View file

@ -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<ChatList>
setState(() {
_activeSpaceId = spaceId;
});
// #Pangea
if (FluffyThemes.isColumnMode(context)) {
context.go('/rooms/$spaceId/details');
}
// Pangea#
}
// #Pangea
@ -139,50 +130,6 @@ class ChatListController extends State<ChatList>
void onChatTap(Room room) async {
if (room.membership == Membership.invite) {
final inviterId =
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
final inviteAction = await showModalActionSheet<InviteActions>(
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<ChatList>
setActiveSpace(room.id);
return;
}
// Share content into this room
final shareContent = Matrix.of(context).shareContent;
if (shareContent != null) {
final shareFile = shareContent.tryGet<XFile>('file');
if (shareContent.tryGet<String>('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<ChatList>
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<ChatList>
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<ChatList>
String? get activeChat => widget.activeChat;
SelectMode get selectMode => Matrix.of(context).shareContent != null
? SelectMode.share
: SelectMode.normal;
void _processIncomingSharedMedia(List<SharedMediaFile> 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<ChatList>
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<ChatList>
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<ChatList>
],
),
),
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<ChatList>
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<ChatList>
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<ChatList>
);
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<ChatList>
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<ChatList>
future: () => client.setPresence(
client.userID!,
PresenceType.online,
statusMsg: input.single,
statusMsg: input,
),
);
}
@ -1118,7 +1033,9 @@ class ChatListController extends State<ChatList>
// 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<ChatList>
}
// 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<ChatList>
final client = Matrix.of(context)
.widget
.clients[Matrix.of(context).getClientIndexByMatrixId(userId!)];
final action = await showConfirmationDialog<EditBundleAction>(
final action = await showModalActionPopup<EditBundleAction>(
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<ChatList>
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#

View file

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

View file

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

View file

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

View file

@ -22,133 +22,119 @@ class ChatListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
return StreamBuilder<Object?>(
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(),
),
),
),
],
),
),
);
},
],
),
);
}
}

View file

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

View file

@ -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<NaviRailItem> createState() => _NaviRailItemState();
}
class _NaviRailItemState extends State<NaviRailItem> {
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,
),
),
),
),
),
),
],
),
);
}
}

View file

@ -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<SpaceView> {
// 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<SpaceView> {
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<SpaceView> {
// 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<SpaceView> {
// #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<SpaceView> {
// 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<SpaceView> {
enableEncryption: false,
// Pangea#
);
// }
await activeSpace.setSpaceChild(roomId);
},
);

View file

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

View file

@ -99,6 +99,7 @@ class DevicesSettingsView extends StatelessWidget {
L10n.of(context).removeAllOtherDevices,
),
style: TextButton.styleFrom(
iconColor: theme.colorScheme.onErrorContainer,
foregroundColor:
theme.colorScheme.onErrorContainer,
backgroundColor:

View file

@ -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<UserDeviceListItemAction>(
final action = await showModalActionPopup<UserDeviceListItemAction>(
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,
),
],
);

View file

@ -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<HomeserverPicker> {
bool isLoading = false;
bool isLoggingIn = false;
final TextEditingController homeserverController = TextEditingController(
text: AppConfig.defaultHomeserver,
@ -61,58 +60,29 @@ class HomeserverPickerController extends State<HomeserverPicker> {
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<void> checkHomeserverAction([_]) async {
Future<void> 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<HomeserverPicker> {
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<HomeserverPicker> {
setState(() {
error = null;
isLoading = isLoggingIn = true;
isLoading = true;
});
try {
await Matrix.of(context).getLoginClient().login(
@ -191,27 +176,16 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} 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<HomeserverPicker> {
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<HomeserverPicker> {
} finally {
if (mounted) {
setState(() {
isLoading = isLoggingIn = false;
isLoading = false;
});
}
}
@ -245,7 +219,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
void onMoreAction(MoreLoginActions action) {
switch (action) {
case MoreLoginActions.passwordLogin:
login();
checkHomeserverAction(legacyPasswordLogin: true);
case MoreLoginActions.privacy:
launchUrlString(AppConfig.privacyUrl);
case MoreLoginActions.about:

View file

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

View file

@ -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<ImageViewer> {
/// 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<Event> 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;

View file

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

View file

@ -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<KeyVerificationDialog> {
await showOkAlertDialog(
useRootNavigator: false,
context: context,
message: L10n.of(context).incorrectPassphraseOrKey,
title: L10n.of(context).incorrectPassphraseOrKey,
);
}
}

View file

@ -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<Login> {
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<Login> {
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<Login> {
future: () =>
Matrix.of(context).getLoginClient().requestTokenToResetPasswordEmail(
clientSecret,
input.single,
input,
sendAttempt++,
),
);
@ -318,15 +314,10 @@ class LoginController extends State<Login> {
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<Login> {
title: L10n.of(context).weSentYouAnEmail,
message: L10n.of(context).pleaseClickOnLink,
okLabel: L10n.of(context).iHaveClickedOnLink,
fullyCapitalizedForMaterial: false,
);
if (ok != OkCancelResult.ok) return;
final data = <String, dynamic>{
'new_password': password.single,
'new_password': password,
'logout_devices': false,
"auth": AuthenticationThreePidCreds(
type: AuthenticationTypes.emailIdentity,
@ -361,8 +351,8 @@ class LoginController extends State<Login> {
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();
}
}

View file

@ -35,7 +35,7 @@ class NewGroupController extends State<NewGroup> {
bool requiredCodeToJoin = false;
// bool publicGroup = false;
// Pangea#
bool groupCanBeFound = true;
bool groupCanBeFound = false;
Uint8List? avatar;
@ -54,7 +54,8 @@ class NewGroupController extends State<NewGroup> {
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#

View file

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

View file

@ -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<Settings> {
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<Settings> {
// 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<Settings> {
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<AvatarAction>(
? actions.single.value
: await showModalActionPopup<AvatarAction>(
context: context,
title: L10n.of(context).changeYourAvatar,
cancelLabel: L10n.of(context).cancel,
actions: actions,
);
if (action == null) return;

View file

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

View file

@ -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<Settings3Pid> {
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<Settings3Pid> {
context: context,
future: () => Matrix.of(context).client.requestTokenToRegisterEmail(
clientSecret,
input.single,
input,
Settings3Pid.sendAttempt++,
),
);

View file

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

View file

@ -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<EmotesSettings> {
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<EmotesSettings> {
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<EmotesSettings> {
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<EmotesSettings> {
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<EmotesSettings> {
await showOkAlertDialog(
useRootNavigator: false,
context: context,
message: L10n.of(context).emoteInvalid,
title: L10n.of(context).emoteInvalid,
okLabel: L10n.of(context).ok,
);
return;

View file

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

View file

@ -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<SettingsNotifications> {
}
void onPusherTap(Pusher pusher) async {
final delete = await showModalActionSheet<bool>(
final delete = await showModalActionPopup<bool>(
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,
),
],
);

View file

@ -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<SettingsSecurity> {
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<SettingsSecurity> {
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<SettingsSecurity> {
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<SettingsSecurity> {
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!,
),

View file

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

View file

@ -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<UserBottomSheet> {
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<int>(
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<int>(
// 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<UserBottomSheet> {
}
}
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(

Some files were not shown because too many files have changed in this diff Show more