Merge branch 'krille-chan:main' into main

This commit is contained in:
dlyrsk 2024-08-07 23:33:11 +09:30 committed by GitHub
commit c277e73faf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 24811 additions and 27829 deletions

View file

@ -1,2 +1,2 @@
FLUTTER_VERSION=3.22.2
FLUTTER_VERSION=3.22.3
JAVA_VERSION=17

View file

@ -1,3 +1,10 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
//id "com.google.gms.google-services"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,11 +13,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@ -21,10 +23,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
@ -45,7 +43,7 @@ android {
defaultConfig {
applicationId "chat.fluffy.fluffychat"
minSdkVersion 21
targetSdkVersion 33
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -85,9 +83,6 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
implementation 'androidx.multidex:multidex:2.0.1'
}
//apply plugin: 'com.google.gms.google-services'

View file

@ -18,14 +18,12 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-sdk

View file

@ -1,17 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//classpath 'com.google.gms:google-services:4.3.8'
}
}
allprojects {
repositories {
google()

View file

@ -1,11 +1,26 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.1.2" apply false
id "org.jetbrains.kotlin.android" version "1.8.0" apply false
// id "com.google.gms.google-services" version "4.3.8" apply false
}
include ":app"

View file

@ -736,7 +736,7 @@
"type": "text",
"placeholders": {}
},
"oopsSomethingWentWrong": "عذراً، هناك خطأ ما…",
"oopsSomethingWentWrong": "عفوًا، حدث خطأ ما…",
"@oopsSomethingWentWrong": {
"type": "text",
"placeholders": {}
@ -1821,7 +1821,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "مستوى الأذونات الإفتراضي",
"defaultPermissionLevel": "مستوى الأذونات الافتراضية للمستخدمين الجدد",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -2260,7 +2260,7 @@
},
"jump": "قفز",
"@jump": {},
"report": "التقرير",
"report": "تقرير",
"@report": {},
"noKeyForThisMessage": "يمكن أن يحدث هذا إذا تم إرسال الرسالة قبل تسجيل الدخول إلى حسابك على هذا الجهاز.\n\nمن الممكن أيضا أن يكون المرسل قد حظر جهازك أو حدث خطأ ما في الاتصال بالإنترنت.\n\nهل يمكنك قراءة الرسالة في جلسة أخرى؟ ثم يمكنك نقل الرسالة منه! انتقل إلى الإعدادات > الأجهزة وتأكد من أن أجهزتك قد تحققت من بعضها البعض. عندما تفتح الغرفة في المرة التالية وتكون كلتا الجلستين في المقدمة ، سيتم إرسال المفاتيح تلقائيا.\n\nألا تريد أن تفقد المفاتيح عند تسجيل الخروج أو تبديل الأجهزة؟ تأكد من تمكين النسخ الاحتياطي للدردشة في الإعدادات.",
"@noKeyForThisMessage": {},
@ -2709,8 +2709,68 @@
"@gallery": {},
"swipeRightToLeftToReply": "اسحب من اليمين إلى اليسار للرد",
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "خاطئ",
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
},
"countChatsAndCountParticipants": "{chats} دردشات و {participants} مشاركين",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"noMoreChatsFound": "لم يتم العثور على دردشات...",
"@noMoreChatsFound": {},
"joinedChats": "انضم إلى الدردشة",
"@joinedChats": {},
"unread": "غير المقروءة",
"@unread": {},
"space": "المساحة",
"@space": {},
"spaces": "المساحات",
"@spaces": {},
"markAsUnread": "تحديد كغير مقروء",
"@markAsUnread": {},
"goToSpace": "انتقل إلى المساحة: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"userLevel": "{level} - مستخدم",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - مشرف",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - مدير",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeGeneralChatSettings": "تغيير إعدادات الدردشة العامة",
"@changeGeneralChatSettings": {},
"inviteOtherUsers": "دعوة مستخدمين آخرين إلى هذه الدردشة",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "تغيير أذونات الدردشة",
"@changeTheChatPermissions": {},
"changeTheVisibilityOfChatHistory": "تغيير رؤية سجل الدردشة",
"@changeTheVisibilityOfChatHistory": {},
"changeTheCanonicalRoomAlias": "تغيير عنوان الدردشة العامة الرئيسي",
"@changeTheCanonicalRoomAlias": {},
"sendRoomNotifications": "إرسال إشعارات @room",
"@sendRoomNotifications": {},
"changeTheDescriptionOfTheGroup": "تغيير وصف الدردشة",
"@changeTheDescriptionOfTheGroup": {}
}

View file

@ -2349,48 +2349,11 @@
"count": {}
}
},
"@reportErrorDescription": {},
"@banUserDescription": {},
"@removeDevicesDescription": {},
"@unbanUserDescription": {},
"@pushNotificationsNotAvailable": {},
"@makeAdminDescription": {},
"@archiveRoomDescription": {},
"@invalidInput": {},
"@report": {},
"@hasKnocked": {
"placeholders": {
"user": {}
}
},
"@wrongPinEntered": {
"type": "text",
"placeholders": {
"seconds": {}
}
},
"@inviteGroupChat": {},
"@invitePrivateChat": {},
"@learnMore": {},
"@roomUpgradeDescription": {},
"@pleaseEnterANumber": {},
"emoteKeyboardNoRecents": "Naposledy použité emoce se zobrazí zde...",
"@emoteKeyboardNoRecents": {
"type": "text",
"placeholders": {}
},
"@kickUserDescription": {},
"@invite": {},
"@indexedDbErrorLong": {},
"@callingAccount": {},
"@enterSpace": {},
"@noKeyForThisMessage": {},
"@readUpToHere": {},
"@appearOnTopDetails": {},
"@enterRoom": {},
"@hideUnimportantStateEvents": {},
"@noBackupWarning": {},
"@indexedDbErrorTitle": {},
"appLockDescription": "Zamknout aplikaci pomocí PIN kódu když není používána",
"@appLockDescription": {},
"globalChatId": "Globální ID chatu",

View file

@ -2,8 +2,8 @@
"@@locale": "de",
"@@last_modified": "2021-08-14 12:41:10.119255",
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"about": "Über",
"@about": {
@ -360,7 +360,7 @@
"type": "text",
"description": "Usage hint for the command /invite"
},
"commandHint_join": "Betrete den ausgewählten Raum",
"commandHint_join": "Betritt den ausgewählten Raum",
"@commandHint_join": {
"type": "text",
"description": "Usage hint for the command /join"
@ -1084,7 +1084,7 @@
"type": "text",
"placeholders": {}
},
"noGoogleServicesWarning": "Firebase Cloud Messaging scheint auf deinem Gerät nicht verfügbar zu sein. Um trotzdem Push-Benachrichtigungen zu erhalten, empfehlen wir die Installation von ntfy. Mit ntfy oder einem anderen Unified Push Anbieter kannst du Push-Benachrichtigungen datensicher empfangen. Du kannst ntfy im PlayStore oder bei F-Droid herunterladen.",
"noGoogleServicesWarning": "Firebase Cloud Messaging scheint auf deinem Gerät nicht verfügbar zu sein. Um trotzdem Push-Benachrichtigungen zu erhalten, empfehlen wir die Installation von ntfy. Mit ntfy oder einem anderen Unified-Push-Anbieter kannst du Push-Benachrichtigungen datensicher empfangen. Du kannst ntfy im PlayStore oder bei F-Droid herunterladen.",
"@noGoogleServicesWarning": {
"type": "text",
"placeholders": {}
@ -1667,7 +1667,7 @@
"type": "text",
"placeholders": {}
},
"tryToSendAgain": "Nochmal versuchen zu senden",
"tryToSendAgain": "Noch mal versuchen zu senden",
"@tryToSendAgain": {
"type": "text",
"placeholders": {}
@ -1712,7 +1712,7 @@
"type": "text",
"placeholders": {}
},
"unpin": "Abpinnen",
"unpin": "Nicht mehr anpinnen",
"@unpin": {
"type": "text",
"placeholders": {}
@ -1965,7 +1965,7 @@
"@start": {},
"repeatPassword": "Passwort wiederholen",
"@repeatPassword": {},
"commandHint_dm": "Starte einen direkten Chat\nBenutze --no-encryption um die Verschlüsselung auszuschalten",
"commandHint_dm": "Starte einen direkten Chat\nBenutze --no-encryption, um die Verschlüsselung auszuschalten",
"@commandHint_dm": {
"type": "text",
"description": "Usage hint for the command /dm"
@ -1980,7 +1980,7 @@
"type": "text",
"description": "Usage hint for the command /clearcache"
},
"commandHint_create": "Erstelle ein leeren Gruppenchat\nBenutze --no-encryption um die Verschlüsselung auszuschalten",
"commandHint_create": "Erstelle ein leeren Gruppenchat\nBenutze --no-encryption, um die Verschlüsselung auszuschalten",
"@commandHint_create": {
"type": "text",
"description": "Usage hint for the command /create"
@ -2013,7 +2013,7 @@
"@videoCallsBetaWarning": {},
"emailOrUsername": "E-Mail oder Benutzername",
"@emailOrUsername": {},
"unsupportedAndroidVersionLong": "Diese Funktion erfordert eine neuere Android-Version. Bitte suche nach Updates oder Lineage OS-Unterstützung.",
"unsupportedAndroidVersionLong": "Diese Funktion erfordert eine neuere Android-Version. Bitte suche nach Updates oder prüfe die Lineage-OS-Unterstützung.",
"@unsupportedAndroidVersionLong": {},
"experimentalVideoCalls": "Experimentelle Videoanrufe",
"@experimentalVideoCalls": {},
@ -2151,7 +2151,7 @@
"@saveKeyManuallyDescription": {},
"hydrateTorLong": "Hast du deine Sitzung das letzte Mal auf TOR exportiert? Importiere sie schnell und chatte weiter.",
"@hydrateTorLong": {},
"pleaseEnterRecoveryKey": "Bitte gebe deinen Wiederherstellungsschlüssel ein:",
"pleaseEnterRecoveryKey": "Bitte gib deinen Wiederherstellungsschlüssel ein:",
"@pleaseEnterRecoveryKey": {},
"countFiles": "{count} Dateien",
"@countFiles": {
@ -2265,7 +2265,7 @@
"senderName": {}
}
},
"commandHint_googly": "Googly Eyes senden",
"commandHint_googly": "Glupschaugen senden",
"@commandHint_googly": {},
"disableEncryptionWarning": "Aus Sicherheitsgründen können Sie die Verschlüsselung in einem Chat nicht deaktivieren, wo sie zuvor aktiviert wurde.",
"@disableEncryptionWarning": {},
@ -2273,7 +2273,7 @@
"@reopenChat": {},
"fileIsTooBigForServer": "Der Server meldet, dass die Datei zu groß ist für eine Übermittlung ist.",
"@fileIsTooBigForServer": {},
"noBackupWarning": "Achtung! Ohne Aktivierung des Chat-Backups verlierst du den Zugriff auf deine verschlüsselten Nachrichten. Vor dem Ausloggen wird dringend empfohlen den Chat-Backup zu aktivieren.",
"noBackupWarning": "Achtung! Ohne Aktivierung des Chat-Backups verlierst du den Zugriff auf deine verschlüsselten Nachrichten. Vor dem Ausloggen wird dringend empfohlen, das Chat-Backup zu aktivieren.",
"@noBackupWarning": {},
"noOtherDevicesFound": "Keine anderen Geräte anwesend",
"@noOtherDevicesFound": {},
@ -2589,7 +2589,7 @@
"sender": {}
}
},
"verifyOtherDeviceDescription": "Wenn Sie ein anderes Gerät verifizieren, können diese Geräteschlüssel austauschen, was Ihre Sicherheit insgesamt erhöht. 💪 Wenn Sie eine Verifizierung starten, erscheint ein Pop-up in der App auf beiden Geräten. Dort sehen Sie dann eine Reihe von Emojis oder Zahlen, die Sie miteinander vergleichen müssen. Am besten hältst du beide Geräte bereit, bevor du die Verifizierung startest. 🤳",
"verifyOtherDeviceDescription": "Wenn Sie ein anderes Gerät verifizieren, können diese Geräteschlüssel austauschen, was Ihre Sicherheit insgesamt erhöht. 💪 Wenn Sie eine Verifizierung starten, erscheint ein Pop-up in der App auf beiden Geräten. Dort sehen Sie dann eine Reihe von Emojis oder Zahlen, die Sie miteinander vergleichen müssen. Am besten halten Sie beide Geräte bereit, bevor Sie die Verifizierung starten. 🤳",
"@verifyOtherDeviceDescription": {},
"presenceStyle": "Statusmeldungen:",
"@presenceStyle": {
@ -2710,5 +2710,32 @@
}
},
"searchMore": "Weiter suchen ...",
"@searchMore": {}
"@searchMore": {},
"unread": "Ungelesen",
"@unread": {},
"noMoreChatsFound": "Keine weiteren Chats gefunden ...",
"@noMoreChatsFound": {},
"joinedChats": "Beigetretene Chats",
"@joinedChats": {},
"space": "Space",
"@space": {},
"spaces": "Spaces",
"@spaces": {},
"goToSpace": "Geh zum Space: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "Als ungelesen markieren",
"@markAsUnread": {},
"swipeRightToLeftToReply": "Wische von rechts nach links zum Antworten",
"@swipeRightToLeftToReply": {},
"countChatsAndCountParticipants": "{chats} Chats und {participants} Teilnehmer",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
}
}

View file

@ -196,6 +196,19 @@
"supportedVersions": {}
}
},
"countChatsAndCountParticipants": "{chats} chats and {participants} participants",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"noMoreChatsFound": "No more chats found...",
"joinedChats": "Joined chats",
"unread": "Unread",
"space": "Space",
"spaces": "Spaces",
"banFromChat": "Ban from chat",
"@banFromChat": {
"type": "text",
@ -660,7 +673,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "Default permission level",
"defaultPermissionLevel": "Default permission level for new users",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -2266,6 +2279,12 @@
"user": {}
}
},
"invitedBy": "📩 Invited by {user}",
"@invitedBy": {
"placeholders": {
"user": {}
}
},
"youInvitedUser": "📩 You invited {user}",
"@youInvitedUser": {
"placeholders": {
@ -2692,5 +2711,48 @@
"restricted": "Restricted",
"@restricted": {},
"knockRestricted": "Knock restricted",
"@knockRestricted": {}
"@knockRestricted": {},
"goToSpace": "Go to space: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "Mark as unread",
"userLevel": "{level} - User",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - Moderator",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - Admin",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeGeneralChatSettings": "Change general chat settings",
"inviteOtherUsers": "Invite other users to this chat",
"changeTheChatPermissions": "Change the chat permissions",
"changeTheVisibilityOfChatHistory": "Change the visibility of the chat history",
"changeTheCanonicalRoomAlias": "Change the main public chat address",
"sendRoomNotifications": "Send a @room notifications",
"changeTheDescriptionOfTheGroup": "Change the description of the chat",
"chatPermissionsDescription": "Define which power level is necessary for certain actions in this chat. The power levels 0, 50 and 100 are usually representing users, moderators and admins, but any gradation is possible.",
"updateInstalled": "🎉 Update {version} installed!",
"@updateInstalled": {
"type": "text",
"placeholders": {
"version": {}
}
},
"changelog": "Changelog"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -559,7 +559,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "Vaikimisi õigused",
"defaultPermissionLevel": "Vaikimisi õigused uutele kasutajatele",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -2709,8 +2709,68 @@
"@files": {},
"swipeRightToLeftToReply": "Vastamiseks viipa paremalt vasakule",
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "vale",
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
},
"noMoreChatsFound": "Rohkem vestlusi ei leidu...",
"@noMoreChatsFound": {},
"joinedChats": "Vestlusi, millega oled liitunud",
"@joinedChats": {},
"unread": "Lugemata",
"@unread": {},
"space": "Kogukond",
"@space": {},
"spaces": "Kogukonnad",
"@spaces": {},
"goToSpace": "Ava kogukond: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "Märgi mitteloetuks",
"@markAsUnread": {},
"countChatsAndCountParticipants": "{chats} vestlust ja {participants} osalejat",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"userLevel": "{level} - kasutaja",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - moderaator",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - peakasutaja",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeTheVisibilityOfChatHistory": "Muuda vestluse ajaloo nähtavust",
"@changeTheVisibilityOfChatHistory": {},
"sendRoomNotifications": "Saada @jututuba teavitusi",
"@sendRoomNotifications": {},
"changeTheCanonicalRoomAlias": "Muuda vestluse põhilist avalikult nähtavat aadressi",
"@changeTheCanonicalRoomAlias": {},
"changeGeneralChatSettings": "Muuda vestluse üldiseid seadistusi",
"@changeGeneralChatSettings": {},
"inviteOtherUsers": "Kutsu teisi osalejaid sellesse vestlusesse",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "Muuda vestluse õigusi",
"@changeTheChatPermissions": {},
"changeTheDescriptionOfTheGroup": "Muuda vestluse kirjeldust",
"@changeTheDescriptionOfTheGroup": {}
}

View file

@ -23,7 +23,7 @@
"type": "text",
"placeholders": {}
},
"activatedEndToEndEncryption": "🔐 {username}(e)k ertzetik ertzerako zifraketa gaitu du",
"activatedEndToEndEncryption": "🔐 {username}(e)k ertzetik ertzerako zifratzea gaitu du",
"@activatedEndToEndEncryption": {
"type": "text",
"placeholders": {
@ -226,7 +226,7 @@
"type": "text",
"placeholders": {}
},
"channelCorruptedDecryptError": "Zifraketa hondatu egin da",
"channelCorruptedDecryptError": "Zifratzea hondatu egin da",
"@channelCorruptedDecryptError": {
"type": "text",
"placeholders": {}
@ -412,17 +412,17 @@
"type": "text",
"placeholders": {}
},
"enableEncryptionWarning": "Ezingo duzu zifraketa ezgaitu. Ziur zaude?",
"enableEncryptionWarning": "Ezingo duzu zifratzea ezgaitu. Ziur zaude?",
"@enableEncryptionWarning": {
"type": "text",
"placeholders": {}
},
"encryption": "Zifraketa",
"encryption": "Zifratzea",
"@encryption": {
"type": "text",
"placeholders": {}
},
"encryptionNotEnabled": "Zifraketa ez dago gaituta",
"encryptionNotEnabled": "Zifratzea ez dago gaituta",
"@encryptionNotEnabled": {
"type": "text",
"placeholders": {}
@ -666,7 +666,7 @@
"type": "text",
"placeholders": {}
},
"needPantalaimonWarning": "Kontuan izan oraingoz Pantalaimon behar duzula puntuz puntuko zifraketarako.",
"needPantalaimonWarning": "Kontuan izan oraingoz Pantalaimon behar duzula ertzetik ertzerako zifratzerako.",
"@needPantalaimonWarning": {
"type": "text",
"placeholders": {}
@ -1036,7 +1036,7 @@
"type": "text",
"placeholders": {}
},
"unknownEncryptionAlgorithm": "Zifraketa-algoritmo ezezaguna",
"unknownEncryptionAlgorithm": "Zifratze-algoritmo ezezaguna",
"@unknownEncryptionAlgorithm": {
"type": "text",
"placeholders": {}
@ -1560,7 +1560,7 @@
"type": "text",
"placeholders": {}
},
"contentHasBeenReported": "Edukia zerbitzariko administrariei jakinarazi zaie",
"contentHasBeenReported": "Edukia zerbitzariko administratzaileei jakinarazi zaie",
"@contentHasBeenReported": {
"type": "text",
"placeholders": {}
@ -1649,7 +1649,7 @@
"type": "text",
"placeholders": {}
},
"enableEncryption": "Gaitu zifraketa",
"enableEncryption": "Gaitu zifratzea",
"@enableEncryption": {
"type": "text",
"placeholders": {}
@ -1781,7 +1781,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "Defektuzko botere-maila",
"defaultPermissionLevel": "Erabiltzaile berrien defektuzko botere-maila",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -1841,7 +1841,7 @@
"type": "text",
"placeholders": {}
},
"noEncryptionForPublicRooms": "Zifraketa aktiba dezakezu soilik gelak publikoa izateari utzi badio.",
"noEncryptionForPublicRooms": "Zifratzea aktiba dezakezu soilik gelak publikoa izateari utzi badio.",
"@noEncryptionForPublicRooms": {
"type": "text",
"placeholders": {}
@ -2238,7 +2238,7 @@
"@startFirstChat": {},
"newSpaceDescription": "Guneek txatak taldekatzea ahalbidetzen dute eta komunitate pribatu edo publikoak osatzea.",
"@newSpaceDescription": {},
"disableEncryptionWarning": "Segurtasun arrazoiak direla-eta, ezin duzu lehendik zifratuta zegoen txat bateko zifraketa ezgaitu.",
"disableEncryptionWarning": "Segurtasun arrazoiak direla-eta, ezin duzu lehendik zifratuta zegoen txat bateko zifratzea ezgaitu.",
"@disableEncryptionWarning": {},
"encryptThisChat": "Zifratu txata",
"@encryptThisChat": {},
@ -2357,7 +2357,7 @@
},
"redactMessageDescription": "Mezua elkarrizketa honetako partaide guztientzat botako da atzera. Ezin da desegin.",
"@redactMessageDescription": {},
"addChatDescription": "Gehitu txat honen deskribapena…",
"addChatDescription": "Gehitu txataren deskribapena…",
"@addChatDescription": {},
"directChat": "Banakako txata",
"@directChat": {},
@ -2520,7 +2520,7 @@
},
"transparent": "Gardena",
"@transparent": {},
"sendReadReceipts": "Bidali irakurri izanaren adierazlea",
"sendReadReceipts": "Bidali irakurri izanaren agiria",
"@sendReadReceipts": {},
"formattedMessages": "Formatua duten mezuak",
"@formattedMessages": {},
@ -2547,7 +2547,7 @@
"sender": {}
}
},
"sendReadReceiptsDescription": "Txateko beste kideek mezu bat irakurri duzula ikus dezakete.",
"sendReadReceiptsDescription": "Txateko beste partaideek mezu bat irakurri duzula ikus dezakete.",
"@sendReadReceiptsDescription": {},
"forwardMessageTo": "Birbidali mezua {roomName}(e)ra?",
"@forwardMessageTo": {
@ -2576,7 +2576,7 @@
"@verifyOtherUserDescription": {},
"formattedMessagesDescription": "Erakutsi mezu aberatsen edukia markdown erabiliz, testu lodia esaterako.",
"@formattedMessagesDescription": {},
"sendTypingNotificationsDescription": "Txateko beste kideek mezu berri bat idazten ari zarela ikus dezakete.",
"sendTypingNotificationsDescription": "Txateko beste partaideek mezu berri bat idazten ari zarela ikus dezakete.",
"@sendTypingNotificationsDescription": {},
"verifyOtherUser": "🔐 Egiaztatu beste erabiltzaile bat",
"@verifyOtherUser": {},
@ -2709,8 +2709,68 @@
"@knockRestricted": {},
"swipeRightToLeftToReply": "Herrestatu eskuin-ezker erantzuteko",
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "ez",
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
},
"noMoreChatsFound": "Ez da beste txatik aurkitu...",
"@noMoreChatsFound": {},
"unread": "Irakurri gabe",
"@unread": {},
"space": "Gunea",
"@space": {},
"joinedChats": "Batu zaren txatak",
"@joinedChats": {},
"goToSpace": "Joan {space} gunera",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "Markatu irakurri gabetzat",
"@markAsUnread": {},
"countChatsAndCountParticipants": "{chats} txat eta {participants} partaide",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"spaces": "Guneak",
"@spaces": {},
"adminLevel": "{level} - Administratzailea",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeTheChatPermissions": "Aldatu txataren baimenak",
"@changeTheChatPermissions": {},
"inviteOtherUsers": "Gonbidatu beste erabiltzaileak txat honetara",
"@inviteOtherUsers": {},
"userLevel": "{level} - Erabiltzailea",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - Moderatzailea",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"sendRoomNotifications": "Bidali @gela jakinarazpena",
"@sendRoomNotifications": {},
"changeTheDescriptionOfTheGroup": "Aldatu txataren deskribapena",
"@changeTheDescriptionOfTheGroup": {},
"changeGeneralChatSettings": "Aldatu txataren ezarpen orokorrak",
"@changeGeneralChatSettings": {},
"changeTheVisibilityOfChatHistory": "Aldatu txataren historiaren ikusgaitasuna",
"@changeTheVisibilityOfChatHistory": {},
"changeTheCanonicalRoomAlias": "Aldatu txataren helbide publiko nagusia",
"@changeTheCanonicalRoomAlias": {}
}

View file

@ -2300,78 +2300,6 @@
"@signInWithPassword": {},
"pleaseTryAgainLaterOrChooseDifferentServer": "لطفا بعدا تلاش کنید یا سرور دیگری انتخاب کنید.",
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
"@setColorTheme": {},
"@banUserDescription": {},
"@removeDevicesDescription": {},
"@tryAgain": {},
"@unbanUserDescription": {},
"@messagesStyle": {},
"@chatDescription": {},
"@pushNotificationsNotAvailable": {},
"@invalidServerName": {},
"@chatPermissions": {},
"@makeAdminDescription": {},
"@setChatDescription": {},
"@importFromZipFile": {},
"@redactedBy": {
"type": "text",
"placeholders": {
"username": {}
}
},
"@signInWith": {
"type": "text",
"placeholders": {
"provider": {}
}
},
"@optionalRedactReason": {},
"@archiveRoomDescription": {},
"@exportEmotePack": {},
"@inviteContactToGroupQuestion": {},
"@redactedByBecause": {
"type": "text",
"placeholders": {
"username": {},
"reason": {}
}
},
"@redactMessageDescription": {},
"@invalidInput": {},
"@addChatDescription": {},
"@hasKnocked": {
"placeholders": {
"user": {}
}
},
"@directChat": {},
"@wrongPinEntered": {
"type": "text",
"placeholders": {
"seconds": {}
}
},
"@sendTypingNotifications": {},
"@inviteGroupChat": {},
"@invitePrivateChat": {},
"@importEmojis": {},
"@noChatDescriptionYet": {},
"@learnMore": {},
"notAnImage": "یک فایل تصویری نیست.",
"@notAnImage": {},
"@chatDescriptionHasBeenChanged": {},
"@roomUpgradeDescription": {},
"@pleaseEnterANumber": {},
"@profileNotFound": {},
"@shareInviteLink": {},
"@emoteKeyboardNoRecents": {
"type": "text",
"placeholders": {}
},
"@setTheme": {},
"@replace": {},
"@createGroup": {},
"@kickUserDescription": {},
"@importNow": {},
"@invite": {}
"@notAnImage": {}
}

View file

@ -2402,12 +2402,5 @@
"importNow": "Tuo nyt",
"@importNow": {},
"invite": "Kutsu",
"@invite": {},
"@banUserDescription": {},
"@removeDevicesDescription": {},
"@unbanUserDescription": {},
"@pushNotificationsNotAvailable": {},
"@makeAdminDescription": {},
"@learnMore": {},
"@kickUserDescription": {}
"@invite": {}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -559,7 +559,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "Nivel de permisos por omisión",
"defaultPermissionLevel": "Nivel de permisos por defecto para novas usuarias",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -2708,5 +2708,69 @@
"restricted": "Non accesible",
"@restricted": {},
"swipeRightToLeftToReply": "Despraza hacia a esquerda para responder",
"@swipeRightToLeftToReply": {}
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "falso",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"noMoreChatsFound": "Non se atopan máis chats…",
"@noMoreChatsFound": {},
"joinedChats": "Chats nos que participas",
"@joinedChats": {},
"countChatsAndCountParticipants": "{chats} chats e {participants} participantes",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"unread": "Sen ler",
"@unread": {},
"space": "Espazo",
"@space": {},
"spaces": "Espazos",
"@spaces": {},
"goToSpace": "Ir ao espazo: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "Marcar como non lido",
"@markAsUnread": {},
"userLevel": "{level} - Usuaria",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - Moderadora",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - Administradora",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeGeneralChatSettings": "Cambiar os axustes xerais do chat",
"@changeGeneralChatSettings": {},
"inviteOtherUsers": "Convidar a outras usuarias a este chat",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "Cambiar os permisos no chat",
"@changeTheChatPermissions": {},
"changeTheVisibilityOfChatHistory": "Cambiar a visibilidade do historial do chat",
"@changeTheVisibilityOfChatHistory": {},
"changeTheCanonicalRoomAlias": "Cambiar o enderezo público principal do chat",
"@changeTheCanonicalRoomAlias": {},
"sendRoomNotifications": "Enviar notificacións a @room",
"@sendRoomNotifications": {},
"changeTheDescriptionOfTheGroup": "Cambiar a descrición do chat",
"@changeTheDescriptionOfTheGroup": {}
}

File diff suppressed because it is too large Load diff

View file

@ -2708,5 +2708,9 @@
"count": {}
},
"swipeRightToLeftToReply": "Za odgovaranje povuci prstom zdesna ulijevo",
"@swipeRightToLeftToReply": {}
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
}

File diff suppressed because it is too large Load diff

View file

@ -2707,5 +2707,34 @@
"knockRestricted": "스페이스 멤버만 참가 요청 가능",
"@knockRestricted": {},
"swipeRightToLeftToReply": "오른쪽에서 왼쪽으로 스와이프해서 답장",
"@swipeRightToLeftToReply": {}
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"unread": "읽지 않은",
"@unread": {},
"space": "스페이스",
"@space": {},
"spaces": "스페이스",
"@spaces": {},
"goToSpace": "스페이스로: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "읽지 않음으로 표시",
"@markAsUnread": {},
"countChatsAndCountParticipants": "{chats} 채팅과 {participants} 참여자",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"joinedChats": "참여한 채팅",
"@joinedChats": {},
"noMoreChatsFound": "채팅을 찾을 수 없습니다...",
"@noMoreChatsFound": {}
}

File diff suppressed because it is too large Load diff

View file

@ -1648,537 +1648,6 @@
"type": "text",
"placeholders": {}
},
"@showPassword": {
"type": "text",
"placeholders": {}
},
"@hugContent": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"@jumpToLastReadMessage": {},
"@allRooms": {
"type": "text",
"placeholders": {}
},
"@obtainingLocation": {
"type": "text",
"placeholders": {}
},
"@commandHint_cuddle": {},
"@chats": {
"type": "text",
"placeholders": {}
},
"@widgetVideo": {},
"@dismiss": {},
"@noEncryptionForPublicRooms": {
"type": "text",
"placeholders": {}
},
"@reportErrorDescription": {},
"@addAccount": {},
"@chatHasBeenAddedToThisSpace": {},
"@removeYourAvatar": {
"type": "text",
"placeholders": {}
},
"@unsupportedAndroidVersion": {},
"@commandHint_html": {
"type": "text",
"description": "Usage hint for the command /html"
},
"@widgetJitsi": {},
"@messageType": {},
"@indexedDbErrorLong": {},
"@oneClientLoggedOut": {},
"@startFirstChat": {},
"@callingAccount": {},
"@setColorTheme": {},
"@nextAccount": {},
"@commandHint_create": {
"type": "text",
"description": "Usage hint for the command /create"
},
"@singlesignon": {
"type": "text",
"placeholders": {}
},
"@allSpaces": {},
"@supposedMxid": {
"type": "text",
"placeholders": {
"mxid": {}
}
},
"@user": {},
"@roomVersion": {
"type": "text",
"placeholders": {}
},
"@youAcceptedTheInvitation": {},
"@noMatrixServer": {
"type": "text",
"placeholders": {
"server1": {},
"server2": {}
}
},
"@youInvitedBy": {
"placeholders": {
"user": {}
}
},
"@banUserDescription": {},
"@widgetEtherpad": {},
"@removeDevicesDescription": {},
"@separateChatTypes": {
"type": "text",
"placeholders": {}
},
"@tryAgain": {},
"@youKickedAndBanned": {
"placeholders": {
"user": {}
}
},
"@unbanUserDescription": {},
"@saveFile": {
"type": "text",
"placeholders": {}
},
"@youRejectedTheInvitation": {},
"@otherCallingPermissions": {},
"@messagesStyle": {},
"@link": {},
"@widgetUrlError": {},
"@emailOrUsername": {},
"@newSpaceDescription": {},
"@chatDescription": {},
"@callingAccountDetails": {},
"@editRoomAliases": {
"type": "text",
"placeholders": {}
},
"@enterSpace": {},
"@encryptThisChat": {},
"@previousAccount": {},
"@reopenChat": {},
"@pleaseEnterRecoveryKey": {},
"@widgetNameError": {},
"@addToBundle": {},
"@spaceIsPublic": {
"type": "text",
"placeholders": {}
},
"@addWidget": {},
"@countFiles": {
"placeholders": {
"count": {}
}
},
"@noKeyForThisMessage": {},
"@shareLocation": {
"type": "text",
"placeholders": {}
},
"@commandHint_markasgroup": {},
"@errorObtainingLocation": {
"type": "text",
"placeholders": {
"error": {}
}
},
"@hydrateTor": {},
"@pushNotificationsNotAvailable": {},
"@storeInAppleKeyChain": {},
"@hydrate": {},
"@invalidServerName": {},
"@chatPermissions": {},
"@wipeChatBackup": {
"type": "text",
"placeholders": {}
},
"@sender": {},
"@storeInAndroidKeystore": {},
"@signInWithPassword": {},
"@makeAdminDescription": {},
"@synchronizingPleaseWait": {
"type": "text",
"placeholders": {}
},
"@goToTheNewRoom": {
"type": "text",
"placeholders": {}
},
"@commandHint_clearcache": {
"type": "text",
"description": "Usage hint for the command /clearcache"
},
"@saveKeyManuallyDescription": {},
"@editBundlesForAccount": {},
"@whyIsThisMessageEncrypted": {},
"@setChatDescription": {},
"@spaceName": {
"type": "text",
"placeholders": {}
},
"@importFromZipFile": {},
"@or": {
"type": "text",
"placeholders": {}
},
"@dehydrateWarning": {},
"@noOtherDevicesFound": {},
"@yourChatBackupHasBeenSetUp": {},
"@redactedBy": {
"type": "text",
"placeholders": {
"username": {}
}
},
"@videoCallsBetaWarning": {},
"@signInWith": {
"type": "text",
"placeholders": {
"provider": {}
}
},
"@fileIsTooBigForServer": {},
"@homeserver": {},
"@people": {
"type": "text",
"placeholders": {}
},
"@verified": {
"type": "text",
"placeholders": {}
},
"@callingPermissions": {},
"@readUpToHere": {},
"@start": {},
"@register": {
"type": "text",
"placeholders": {}
},
"@unlockOldMessages": {},
"@numChats": {
"type": "number",
"placeholders": {
"number": {}
}
},
"@optionalRedactReason": {},
"@dehydrate": {},
"@locationPermissionDeniedNotice": {
"type": "text",
"placeholders": {}
},
"@sendAsText": {
"type": "text"
},
"@archiveRoomDescription": {},
"@exportEmotePack": {},
"@sendSticker": {
"type": "text",
"placeholders": {}
},
"@switchToAccount": {
"type": "number",
"placeholders": {
"number": {}
}
},
"@commandInvalid": {
"type": "text"
},
"@setAsCanonicalAlias": {
"type": "text",
"placeholders": {}
},
"@locationDisabledNotice": {
"type": "text",
"placeholders": {}
},
"@commandHint_plain": {
"type": "text",
"description": "Usage hint for the command /plain"
},
"@experimentalVideoCalls": {},
"@pleaseEnterRecoveryKeyDescription": {},
"@openInMaps": {
"type": "text",
"placeholders": {}
},
"@inviteContactToGroupQuestion": {},
"@redactedByBecause": {
"type": "text",
"placeholders": {
"username": {},
"reason": {}
}
},
"@youHaveWithdrawnTheInvitationFor": {
"placeholders": {
"user": {}
}
},
"@appearOnTopDetails": {},
"@enterRoom": {},
"@pleaseChooseAPasscode": {
"type": "text",
"placeholders": {}
},
"@reportUser": {},
"@commandHint_send": {
"type": "text",
"description": "Usage hint for the command /send"
},
"@confirmEventUnpin": {},
"@youInvitedUser": {
"placeholders": {
"user": {}
}
},
"@fileHasBeenSavedAt": {
"type": "text",
"placeholders": {
"path": {}
}
},
"@commandMissing": {
"type": "text",
"placeholders": {
"command": {}
},
"description": "State that {command} is not a valid /command."
},
"@redactMessageDescription": {},
"@recoveryKey": {},
"@redactMessage": {
"type": "text",
"placeholders": {}
},
"@commandHint_discardsession": {
"type": "text",
"description": "Usage hint for the command /discardsession"
},
"@invalidInput": {},
"@dehydrateTorLong": {},
"@commandHint_myroomnick": {
"type": "text",
"description": "Usage hint for the command /myroomnick"
},
"@doNotShowAgain": {},
"@report": {},
"@unverified": {},
"@serverRequiresEmail": {},
"@hideUnimportantStateEvents": {},
"@screenSharingTitle": {},
"@widgetCustom": {},
"@addToSpaceDescription": {},
"@googlyEyesContent": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"@youBannedUser": {
"placeholders": {
"user": {}
}
},
"@addChatDescription": {},
"@commandHint_leave": {
"type": "text",
"description": "Usage hint for the command /leave"
},
"@commandHint_myroomavatar": {
"type": "text",
"description": "Usage hint for the command /myroomavatar"
},
"@hasKnocked": {
"placeholders": {
"user": {}
}
},
"@publish": {},
"@openLinkInBrowser": {},
"@clearArchive": {},
"@commandHint_react": {
"type": "text",
"description": "Usage hint for the command /react"
},
"@commandHint_me": {
"type": "text",
"description": "Usage hint for the command /me"
},
"@messageInfo": {},
"@disableEncryptionWarning": {},
"@directChat": {},
"@wrongPinEntered": {
"type": "text",
"placeholders": {
"seconds": {}
}
},
"@sendTypingNotifications": {},
"@inviteGroupChat": {},
"@appearOnTop": {},
"@invitePrivateChat": {},
"@foregroundServiceRunning": {},
"@voiceCall": {},
"@commandHint_kick": {
"type": "text",
"description": "Usage hint for the command /kick"
},
"@createNewSpace": {
"type": "text",
"placeholders": {}
},
"@commandHint_unban": {
"type": "text",
"description": "Usage hint for the command /unban"
},
"@commandHint_ban": {
"type": "text",
"description": "Usage hint for the command /ban"
},
"@importEmojis": {},
"@wasDirectChatDisplayName": {
"type": "text",
"placeholders": {
"oldDisplayName": {}
}
},
"@noChatDescriptionYet": {},
"@removeFromBundle": {},
"@confirmMatrixId": {},
"@learnMore": {},
"notAnImage": "Ikke en bildefil.",
"@notAnImage": {},
"@users": {},
"@openGallery": {},
"@chatDescriptionHasBeenChanged": {},
"@newGroup": {},
"@bundleName": {},
"@dehydrateTor": {},
"@removeFromSpace": {},
"@commandHint_op": {
"type": "text",
"description": "Usage hint for the command /op"
},
"@commandHint_join": {
"type": "text",
"description": "Usage hint for the command /join"
},
"@roomUpgradeDescription": {},
"@commandHint_invite": {
"type": "text",
"description": "Usage hint for the command /invite"
},
"@scanQrCode": {},
"@pleaseEnterANumber": {},
"@youKicked": {
"placeholders": {
"user": {}
}
},
"@profileNotFound": {},
"@jump": {},
"@reactedWith": {
"type": "text",
"placeholders": {
"sender": {},
"reaction": {}
}
},
"@sorryThatsNotPossible": {},
"@videoWithSize": {
"type": "text",
"placeholders": {
"size": {}
}
},
"@shareInviteLink": {},
"@commandHint_markasdm": {},
"@recoveryKeyLost": {},
"@cuddleContent": {
"type": "text",
"placeholders": {
"senderName": {}
}
},
"@deviceKeys": {},
"@emoteKeyboardNoRecents": {
"type": "text",
"placeholders": {}
},
"@setTheme": {},
"@youJoinedTheChat": {},
"@openVideoCamera": {
"type": "text",
"placeholders": {}
},
"@markAsRead": {},
"@widgetName": {},
"@errorAddingWidget": {},
"@commandHint_dm": {
"type": "text",
"description": "Usage hint for the command /dm"
},
"@commandHint_hug": {},
"@replace": {},
"@oopsPushError": {
"type": "text",
"placeholders": {}
},
"@youUnbannedUser": {
"placeholders": {
"user": {}
}
},
"@pleaseEnter4Digits": {
"type": "text",
"placeholders": {}
},
"@newSpace": {},
"@emojis": {},
"@pleaseEnterYourPin": {
"type": "text",
"placeholders": {}
},
"@pleaseChoose": {
"type": "text",
"placeholders": {}
},
"@commandHint_googly": {},
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
"@createGroup": {},
"@hydrateTorLong": {},
"@time": {},
"@custom": {},
"@noBackupWarning": {},
"@storeInSecureStorageDescription": {},
"@openChat": {},
"@kickUserDescription": {},
"@importNow": {},
"@pinMessage": {},
"@invite": {},
"@enableMultiAccounts": {},
"@indexedDbErrorTitle": {},
"@unsupportedAndroidVersionLong": {},
"@storeSecurlyOnThisDevice": {},
"@screenSharingDetail": {},
"@waitingPartnerAcceptRequest": {
"type": "text",
"placeholders": {}
},
"@waitingPartnerEmoji": {
"type": "text",
"placeholders": {}
},
"@placeCall": {}
"@notAnImage": {}
}

View file

@ -2417,5 +2417,9 @@
"pleaseEnterANumber": "Vul een getal in groter dan 0",
"@pleaseEnterANumber": {},
"kickUserDescription": "De persoon is verwijderd uit de chat, maar is niet verbannen. In publieke chats kan de persoon op elk moment opnieuw deelnemen.",
"@kickUserDescription": {}
"@kickUserDescription": {},
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -563,7 +563,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "Öntanımlı izin seviyesi",
"defaultPermissionLevel": "Yeni kullanıcılar içi öntanımlı izin seviyesi",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -2709,8 +2709,68 @@
"@restricted": {},
"swipeRightToLeftToReply": "Yanıtlamak için sağdan sola kaydır",
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "yanlış",
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
},
"countChatsAndCountParticipants": "{chats} sohbet ve {participants} katılımcı",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"noMoreChatsFound": "Başka sohbet bulunamadı...",
"@noMoreChatsFound": {},
"goToSpace": "Alana git: {space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"joinedChats": "Katılınan sohbetler",
"@joinedChats": {},
"unread": "Okunmadı",
"@unread": {},
"markAsUnread": "Okunmadı olarak işaretle",
"@markAsUnread": {},
"space": "Alan",
"@space": {},
"spaces": "Alanlar",
"@spaces": {},
"inviteOtherUsers": "Diğer kullanıcıları bu sohbete davet et",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "Sohbet izinlerini değiştir",
"@changeTheChatPermissions": {},
"changeTheCanonicalRoomAlias": "Ana herkese açık sohbet adresini değiştir",
"@changeTheCanonicalRoomAlias": {},
"sendRoomNotifications": "@oda bildirimleri gönder",
"@sendRoomNotifications": {},
"changeTheDescriptionOfTheGroup": "Sohbetin açıklamasını değiştir",
"@changeTheDescriptionOfTheGroup": {},
"userLevel": "{level} - Kullanıcı",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - Moderatör",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - Yönetici",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"changeGeneralChatSettings": "Genel sohbet ayarlarını değiştir",
"@changeGeneralChatSettings": {},
"changeTheVisibilityOfChatHistory": "Sohbet geçmişinin görünürlüğünü değiştir",
"@changeTheVisibilityOfChatHistory": {}
}

View file

@ -537,7 +537,7 @@
"type": "text",
"placeholders": {}
},
"defaultPermissionLevel": "默认权限级别",
"defaultPermissionLevel": "新用户默认权限级别",
"@defaultPermissionLevel": {
"type": "text",
"placeholders": {}
@ -2709,8 +2709,68 @@
"@restricted": {},
"swipeRightToLeftToReply": "从右向左滑动进行回复",
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "",
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
}
},
"noMoreChatsFound": "找不到更多聊天…",
"@noMoreChatsFound": {},
"joinedChats": "已加入的聊天",
"@joinedChats": {},
"space": "空间",
"@space": {},
"spaces": "空间",
"@spaces": {},
"goToSpace": "转到空间:{space}",
"@goToSpace": {
"type": "text",
"space": {}
},
"markAsUnread": "标为未读",
"@markAsUnread": {},
"countChatsAndCountParticipants": "{chats} 个聊天和 {participants} 名参与者",
"@countChatsAndCountParticipants": {
"type": "text",
"placeholders": {
"chats": {},
"participants": {}
}
},
"unread": "未读",
"@unread": {},
"userLevel": "{level} - 用户",
"@userLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"moderatorLevel": "{level} - 主持人",
"@moderatorLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"adminLevel": "{level} - 管理员",
"@adminLevel": {
"type": "text",
"placeholders": {
"level": {}
}
},
"inviteOtherUsers": "邀请其他用户到这个聊天",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "更改聊天权限",
"@changeTheChatPermissions": {},
"changeTheVisibilityOfChatHistory": "更改聊天历史的可见性",
"@changeTheVisibilityOfChatHistory": {},
"changeTheCanonicalRoomAlias": "更改主公共聊天地址",
"@changeTheCanonicalRoomAlias": {},
"sendRoomNotifications": "发送通知聊天室所有人的通知",
"@sendRoomNotifications": {},
"changeTheDescriptionOfTheGroup": "更改聊天描述",
"@changeTheDescriptionOfTheGroup": {},
"changeGeneralChatSettings": "更改常规聊天设置",
"@changeGeneralChatSettings": {}
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
platform :ios, '12.1'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View file

@ -475,7 +475,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@ -564,7 +564,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -613,7 +613,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View file

@ -12,7 +12,7 @@ abstract class AppConfig {
static double fontSizeFactor = 1;
static const Color chatColor = primaryColor;
static Color? colorSchemeSeed = primaryColor;
static const double messageFontSize = 15.75;
static const double messageFontSize = 16.0;
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
static const Color primaryColor = Color(0xFF5625BA);
@ -35,6 +35,8 @@ abstract class AppConfig {
'https://github.com/krille-chan/fluffychat';
static const String supportUrl =
'https://github.com/krille-chan/fluffychat/issues';
static const String changelogUrl =
'https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md';
static final Uri newIssueUrl = Uri(
scheme: 'https',
host: 'github.com',

View file

@ -77,9 +77,6 @@ abstract class FluffyThemes {
? Typography.material2018().black.merge(fallbackTextTheme)
: Typography.material2018().white.merge(fallbackTextTheme)
: null,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
dividerColor: brightness == Brightness.light
? Colors.blueGrey.shade50
: Colors.blueGrey.shade900,

View file

@ -29,6 +29,7 @@ import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/app_lock.dart';
@ -292,7 +293,7 @@ class ChatController extends State<ChatPageWithRoom>
if (timeline?.events.any((event) => event.eventId == fullyRead) ??
false) {
Logs().v('Scroll up to visible event', fullyRead);
setReadMarker();
scrollToEventId(fullyRead, highlightEvent: false);
return;
}
if (!mounted) return;
@ -620,10 +621,10 @@ class ChatController extends State<ChatPageWithRoom>
builder: (c) => const RecordingDialog(),
);
if (result == null) return;
final audioFile = File(result.path);
final audioFile = XFile(result.path);
final file = MatrixAudioFile(
bytes: audioFile.readAsBytesSync(),
name: audioFile.path,
bytes: await audioFile.readAsBytes(),
name: result.fileName ?? audioFile.path,
);
await room.sendFileEvent(
file,
@ -900,8 +901,14 @@ class ChatController extends State<ChatPageWithRoom>
inputFocus.requestFocus();
}
void scrollToEventId(String eventId) async {
final eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId);
void scrollToEventId(
String eventId, {
bool highlightEvent = true,
}) async {
final eventIndex = timeline!.events
.where((event) => event.isVisibleInGui)
.toList()
.indexWhere((e) => e.eventId == eventId);
if (eventIndex == -1) {
setState(() {
timeline = null;
@ -917,11 +924,14 @@ class ChatController extends State<ChatPageWithRoom>
});
return;
}
setState(() {
scrollToEventIdMarker = eventId;
});
if (highlightEvent) {
setState(() {
scrollToEventIdMarker = eventId;
});
}
await scrollController.scrollToIndex(
eventIndex,
eventIndex + 1,
duration: FluffyThemes.animationDuration,
preferPosition: AutoScrollPosition.middle,
);
_updateScrollController();

View file

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/url_launcher.dart';
class ChatAppBarListTile extends StatelessWidget {
@ -11,6 +10,8 @@ class ChatAppBarListTile extends StatelessWidget {
final Widget? trailing;
final void Function()? onTap;
static const double fixedHeight = 40.0;
const ChatAppBarListTile({
super.key,
this.leading,
@ -23,38 +24,40 @@ class ChatAppBarListTile extends StatelessWidget {
Widget build(BuildContext context) {
final leading = this.leading;
final trailing = this.trailing;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
return InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null) leading,
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Linkify(
text: title,
options: const LinkifyOptions(humanize: false),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
return SizedBox(
height: fixedHeight,
child: InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null) leading,
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Linkify(
text: title,
options: const LinkifyOptions(humanize: false),
maxLines: 1,
overflow: TextOverflow.ellipsis,
fontSize: fontSize,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
overflow: TextOverflow.ellipsis,
fontSize: 14,
),
linkStyle: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 14,
decoration: TextDecoration.underline,
decorationColor:
Theme.of(context).colorScheme.onSurfaceVariant,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
linkStyle: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: fontSize,
decoration: TextDecoration.underline,
decorationColor:
Theme.of(context).colorScheme.onSurfaceVariant,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
),
),
if (trailing != null) trailing,
],
if (trailing != null) trailing,
],
),
),
);
}

View file

@ -158,15 +158,15 @@ class ChatView extends StatelessWidget {
builder: (BuildContext context, snapshot) {
var appbarBottomHeight = 0.0;
if (controller.room.pinnedEventIds.isNotEmpty) {
appbarBottomHeight += 42;
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
if (scrollUpBannerEventId != null) {
appbarBottomHeight += 42;
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
final tombstoneEvent =
controller.room.getState(EventTypes.RoomTombstone);
if (tombstoneEvent != null) {
appbarBottomHeight += 42;
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
return Scaffold(
appBar: AppBar(
@ -182,10 +182,17 @@ class ChatView extends StatelessWidget {
tooltip: L10n.of(context)!.close,
color: Theme.of(context).colorScheme.primary,
)
: UnreadRoomsBadge(
filter: (r) => r.id != controller.roomId,
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
child: const Center(child: BackButton()),
: StreamBuilder<Object>(
stream: Matrix.of(context)
.client
.onSync
.stream
.where((syncUpdate) => syncUpdate.hasRoomUpdate),
builder: (context, _) => UnreadRoomsBadge(
filter: (r) => r.id != controller.roomId,
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
child: const Center(child: BackButton()),
),
),
titleSpacing: 0,
title: ChatAppBarTitle(controller),

View file

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
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/utils/error_reporter.dart';
@ -70,7 +71,18 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
);
file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
await file.writeAsBytes(matrixFile.bytes);
if (Platform.isIOS &&
matrixFile.mimeType.toLowerCase() == 'audio/ogg') {
Logs().v('Convert ogg audio file for iOS...');
final convertedFile = File('${file.path}.caf');
if (await convertedFile.exists() == false) {
OpusCaf().convertOpusToCaf(file.path, convertedFile.path);
}
file = convertedFile;
}
}
setState(() {

View file

@ -280,7 +280,6 @@ class ImageExtension extends HtmlExtension {
uri: mxcUrl,
width: width ?? height ?? defaultDimension,
height: height ?? width ?? defaultDimension,
cacheKey: mxcUrl.toString(),
),
),
);

View file

@ -435,22 +435,20 @@ class Message extends StatelessWidget {
? const EdgeInsets.symmetric(vertical: 8.0)
: EdgeInsets.zero,
child: Center(
child: Material(
color: displayTime
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.surface.withOpacity(0.33),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
event.originServerTs.localizedTime(context),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12 * AppConfig.fontSizeFactor,
color: Theme.of(context).colorScheme.secondary,
),
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
event.originServerTs.localizedTime(context),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12 * AppConfig.fontSizeFactor,
color: Theme.of(context).colorScheme.secondary,
shadows: [
Shadow(
color: Theme.of(context).colorScheme.surface,
blurRadius: 3,
),
],
),
),
),

View file

@ -17,10 +17,6 @@ class StateMessage extends StatelessWidget {
child: Center(
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
child: Text(
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
@ -29,6 +25,12 @@ class StateMessage extends StatelessWidget {
style: TextStyle(
fontSize: 12 * AppConfig.fontSizeFactor,
decoration: event.redacted ? TextDecoration.lineThrough : null,
shadows: [
Shadow(
color: Theme.of(context).colorScheme.surface,
blurRadius: 3,
),
],
),
),
),

View file

@ -71,8 +71,8 @@ class PinnedEvents extends StatelessWidget {
) ??
L10n.of(context)!.loadingPleaseWait,
leading: IconButton(
splashRadius: 20,
iconSize: 20,
splashRadius: 18,
iconSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
icon: const Icon(Icons.push_pin),
tooltip: L10n.of(context)!.unpin,

View file

@ -1,9 +1,11 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:path/path.dart' as path_lib;
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@ -13,7 +15,6 @@ import 'package:fluffychat/utils/platform_infos.dart';
import 'events/audio_player.dart';
class RecordingDialog extends StatefulWidget {
static const String recordingFileType = 'm4a';
const RecordingDialog({
super.key,
});
@ -27,18 +28,32 @@ class RecordingDialogState extends State<RecordingDialog> {
Duration _duration = Duration.zero;
bool error = false;
String? _recordedPath;
final _audioRecorder = AudioRecorder();
final List<double> amplitudeTimeline = [];
String? fileName;
static const int bitRate = 64000;
static const int samplingRate = 44100;
Future<void> startRecording() async {
try {
final tempDir = await getTemporaryDirectory();
final path = _recordedPath =
'${tempDir.path}/recording${DateTime.now().microsecondsSinceEpoch}.${RecordingDialog.recordingFileType}';
final codec = kIsWeb
// Web seems to create webm instead of ogg when using opus encoder
// which does not play on iOS right now. So we use wav for now:
? AudioEncoder.wav
// Everywhere else we use opus if supported by the platform:
: await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
? AudioEncoder.opus
: AudioEncoder.aacLc;
fileName =
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
String? path;
if (!kIsWeb) {
final tempDir = await getTemporaryDirectory();
path = path_lib.join(tempDir.path, fileName);
}
final result = await _audioRecorder.hasPermission();
if (result != true) {
@ -46,16 +61,18 @@ class RecordingDialogState extends State<RecordingDialog> {
return;
}
await WakelockPlus.enable();
await _audioRecorder.start(
const RecordConfig(
RecordConfig(
bitRate: bitRate,
sampleRate: samplingRate,
numChannels: 1,
autoGain: true,
echoCancel: true,
noiseSuppress: true,
encoder: codec,
),
path: path,
path: path ?? '',
);
setState(() => _duration = Duration.zero);
_recorderSubscription?.cancel();
@ -91,8 +108,8 @@ class RecordingDialogState extends State<RecordingDialog> {
void _stopAndSend() async {
_recorderSubscription?.cancel();
await _audioRecorder.stop();
final path = _recordedPath;
final path = await _audioRecorder.stop();
if (path == null) throw ('Recording failed!');
const waveCount = AudioPlayerWidget.wavesCount;
final step = amplitudeTimeline.length < waveCount
@ -107,6 +124,7 @@ class RecordingDialogState extends State<RecordingDialog> {
path: path,
duration: _duration.inMilliseconds,
waveform: waveform,
fileName: fileName,
),
);
}
@ -217,23 +235,32 @@ class RecordingResult {
final String path;
final int duration;
final List<int> waveform;
final String? fileName;
const RecordingResult({
required this.path,
required this.duration,
required this.waveform,
required this.fileName,
});
factory RecordingResult.fromJson(Map<String, dynamic> json) =>
RecordingResult(
path: json['path'],
duration: json['duration'],
waveform: List<int>.from(json['waveform']),
);
Map<String, dynamic> toJson() => {
'path': path,
'duration': duration,
'waveform': waveform,
};
}
extension on AudioEncoder {
String get fileExtension {
switch (this) {
case AudioEncoder.aacLc:
case AudioEncoder.aacEld:
case AudioEncoder.aacHe:
return 'm4a';
case AudioEncoder.opus:
return 'ogg';
case AudioEncoder.wav:
return 'wav';
case AudioEncoder.amrNb:
case AudioEncoder.amrWb:
case AudioEncoder.flac:
case AudioEncoder.pcm16bits:
throw UnsupportedError('Not yet used');
}
}
}

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -6,7 +8,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/size_string.dart';
import '../../utils/resize_image.dart';
@ -42,19 +44,20 @@ class SendFileDialogState extends State<SendFileDialog> {
},
);
}
final scaffoldMessenger = ScaffoldMessenger.of(context);
widget.room
.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600,
)
.catchError((e) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text((e as Object).toLocalizedString(context))),
try {
await widget.room.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600,
);
return null;
});
} on IOException catch (_) {
} on FileTooBigMatrixException catch (_) {
} catch (e, s) {
if (mounted) {
ErrorReporter(context, 'Unable to send file').onErrorCallback(e, s);
}
rethrow;
}
}
Navigator.of(context, rootNavigator: false).pop();

View file

@ -24,6 +24,36 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
bool guestAccessLoading = false;
Room get room => Matrix.of(context).client.getRoomById(widget.roomId)!;
String get roomVersion =>
room
.getState(EventTypes.RoomCreate)!
.content
.tryGet<String>('room_version') ??
'Unknown';
/// Calculates which join rules are available based on the information on
/// https://spec.matrix.org/v1.11/rooms/#feature-matrix
List<JoinRules> get availableJoinRules {
final joinRules = Set<JoinRules>.from(JoinRules.values);
final roomVersionInt = int.tryParse(roomVersion);
// Knock is only supported for rooms up from version 7:
if (roomVersionInt != null && roomVersionInt <= 6) {
joinRules.remove(JoinRules.knock);
}
// Not yet supported in FluffyChat:
joinRules.remove(JoinRules.restricted);
joinRules.remove(JoinRules.knockRestricted);
// If an unsupported join rule is the current join rule, display it:
final currentJoinRule = room.joinRules;
if (currentJoinRule != null) joinRules.add(currentJoinRule);
return joinRules.toList();
}
void setJoinRule(JoinRules? newJoinRules) async {
if (newJoinRules == null) return;
setState(() {

View file

@ -66,7 +66,7 @@ class ChatAccessSettingsPageView extends StatelessWidget {
),
),
),
for (final joinRule in JoinRules.values)
for (final joinRule in controller.availableJoinRules)
if (joinRule != JoinRules.private)
RadioListTile<JoinRules>.adaptive(
title: Text(

View file

@ -85,33 +85,16 @@ class ChatDetailsView extends StatelessWidget {
padding: const EdgeInsets.all(32.0),
child: Stack(
children: [
Material(
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
shadowColor: Theme.of(context)
.appBarTheme
.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
),
),
child: Hero(
tag: controller
.widget.embeddedCloseButton !=
null
? 'embedded_content_banner'
: 'content_banner',
child: Avatar(
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
Hero(
tag:
controller.widget.embeddedCloseButton !=
null
? 'embedded_content_banner'
: 'content_banner',
child: Avatar(
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
),
if (!room.isDirectChat &&
@ -170,7 +153,7 @@ class ChatDetailsView extends StatelessWidget {
: displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
style: const TextStyle(fontSize: 18),
),
),
TextButton.icon(
@ -202,10 +185,7 @@ class ChatDetailsView extends StatelessWidget {
),
],
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
if (!room.canChangeStateEvent(EventTypes.RoomTopic))
ListTile(
title: Text(
@ -261,10 +241,7 @@ class ChatDetailsView extends StatelessWidget {
),
),
const SizedBox(height: 16),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
leading: CircleAvatar(
backgroundColor:
@ -316,10 +293,7 @@ class ChatDetailsView extends StatelessWidget {
onTap: () => context
.push('/rooms/${room.id}/details/permissions'),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
L10n.of(context)!.countParticipants(

View file

@ -10,16 +10,19 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_shortcuts/flutter_shortcuts.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package: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/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_update_snackbar.dart';
import 'package:fluffychat/widgets/avatar.dart';
import '../../../utils/account_bundles.dart';
import '../../config/setting_keys.dart';
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
@ -35,7 +38,6 @@ import 'package:fluffychat/utils/tor_stub.dart'
enum SelectMode {
normal,
share,
select,
}
enum PopupMenuAction {
@ -49,20 +51,38 @@ enum PopupMenuAction {
enum ActiveFilter {
allChats,
groups,
messages,
groups,
unread,
spaces,
}
extension LocalizedActiveFilter on ActiveFilter {
String toLocalizedString(BuildContext context) {
switch (this) {
case ActiveFilter.allChats:
return L10n.of(context)!.all;
case ActiveFilter.messages:
return L10n.of(context)!.messages;
case ActiveFilter.unread:
return L10n.of(context)!.unread;
case ActiveFilter.groups:
return L10n.of(context)!.groups;
case ActiveFilter.spaces:
return L10n.of(context)!.spaces;
}
}
}
class ChatList extends StatefulWidget {
static BuildContext? contextForVoip;
final bool displayNavigationRail;
final String? activeChat;
final bool displayNavigationRail;
const ChatList({
super.key,
this.displayNavigationRail = false,
required this.activeChat,
this.displayNavigationRail = false,
});
@override
@ -77,85 +97,155 @@ class ChatListController extends State<ChatList>
StreamSubscription? _intentUriStreamSubscription;
bool get displayNavigationBar =>
!FluffyThemes.isColumnMode(context) &&
(spaces.isNotEmpty || AppConfig.separateChatTypes);
String? activeSpaceId;
void resetActiveSpaceId() {
setState(() {
selectedRoomIds.clear();
activeSpaceId = null;
});
}
void setActiveSpace(String? spaceId) {
setState(() {
selectedRoomIds.clear();
activeSpaceId = spaceId;
activeFilter = ActiveFilter.spaces;
});
}
void createNewSpace() async {
final spaceId = await context.push<String?>('/rooms/newspace');
if (spaceId != null) {
setActiveSpace(spaceId);
}
}
int get selectedIndex {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
return 0;
case ActiveFilter.groups:
return 1;
case ActiveFilter.spaces:
return AppConfig.separateChatTypes ? 2 : 1;
}
}
ActiveFilter getActiveFilterByDestination(int? i) {
switch (i) {
case 1:
if (AppConfig.separateChatTypes) {
return ActiveFilter.groups;
}
return ActiveFilter.spaces;
case 2:
return ActiveFilter.spaces;
case 0:
default:
if (AppConfig.separateChatTypes) {
return ActiveFilter.messages;
}
return ActiveFilter.allChats;
}
}
void onDestinationSelected(int? i) {
setState(() {
selectedRoomIds.clear();
activeFilter = getActiveFilterByDestination(i);
});
void createNewSpace() {
context.push<String?>('/rooms/newspace');
}
ActiveFilter activeFilter = AppConfig.separateChatTypes
? ActiveFilter.messages
: ActiveFilter.allChats;
String? _activeSpaceId;
String? get activeSpaceId => _activeSpaceId;
void setActiveSpace(String spaceId) async {
await Matrix.of(context).client.getRoomById(spaceId)!.postLoad();
setState(() {
_activeSpaceId = spaceId;
});
}
void clearActiveSpace() => setState(() {
_activeSpaceId = null;
});
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
: L10n.of(context)!.inviteGroupChat,
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 {
final waitForRoom = room.client.waitForRoomInSync(
room.id,
join: true,
);
await room.join();
await waitForRoom;
},
);
if (joinResult.error != null) return;
}
if (room.membership == Membership.ban) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat),
),
);
return;
}
if (room.membership == Membership.leave) {
context.go('/rooms/archive/${room.id}');
return;
}
if (room.isSpace) {
setActiveSpace(room.id);
return;
}
// Share content into this room
final shareContent = Matrix.of(context).shareContent;
if (shareContent != null) {
final shareFile = shareContent.tryGet<MatrixFile>('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,
),
);
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}');
}
bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) {
switch (activeFilter) {
case ActiveFilter.allChats:
return (room) => !room.isSpace;
case ActiveFilter.groups:
return (room) => !room.isSpace && !room.isDirectChat;
return (room) => true;
case ActiveFilter.messages:
return (room) => !room.isSpace && room.isDirectChat;
case ActiveFilter.groups:
return (room) => !room.isSpace && !room.isDirectChat;
case ActiveFilter.unread:
return (room) => room.isUnreadOrInvited;
case ActiveFilter.spaces:
return (r) => r.isSpace;
return (room) => room.isSpace;
}
}
@ -331,15 +421,11 @@ class ChatListController extends State<ChatList>
List<Room> get spaces =>
Matrix.of(context).client.rooms.where((r) => r.isSpace).toList();
final selectedRoomIds = <String>{};
String? get activeChat => widget.activeChat;
SelectMode get selectMode => Matrix.of(context).shareContent != null
? SelectMode.share
: selectedRoomIds.isEmpty
? SelectMode.normal
: SelectMode.select;
: SelectMode.normal;
void _processIncomingSharedFiles(List<SharedMediaFile> files) {
if (files.isEmpty) return;
@ -426,6 +512,7 @@ class ChatListController extends State<ChatList>
searchServer =
Matrix.of(context).store.getString(_serverStoreNamespace);
Matrix.of(context).backgroundPush?.setupPush();
UpdateNotifier.showUpdateSnackBar(context);
}
// Workaround for system UI overlay style not applied on app start
@ -448,80 +535,195 @@ class ChatListController extends State<ChatList>
super.dispose();
}
void toggleSelection(String roomId) {
setState(
() => selectedRoomIds.contains(roomId)
? selectedRoomIds.remove(roomId)
: selectedRoomIds.add(roomId),
);
}
void chatContextAction(
Room room,
BuildContext posContext, [
Room? space,
]) async {
if (room.membership == Membership.invite) {
return onChatTap(room);
}
Future<void> toggleUnread() async {
await showFutureLoadingDialog(
context: context,
future: () async {
final markUnread = anySelectedRoomNotMarkedUnread;
final client = Matrix.of(context).client;
for (final roomId in selectedRoomIds) {
final room = client.getRoomById(roomId)!;
if (room.markedUnread == markUnread) continue;
await client.getRoomById(roomId)!.markUnread(markUnread);
}
},
);
cancelAction();
}
final overlay =
Overlay.of(posContext).context.findRenderObject() as RenderBox;
Future<void> toggleFavouriteRoom() async {
await showFutureLoadingDialog(
context: context,
future: () async {
final makeFavorite = anySelectedRoomNotFavorite;
final client = Matrix.of(context).client;
for (final roomId in selectedRoomIds) {
final room = client.getRoomById(roomId)!;
if (room.isFavourite == makeFavorite) continue;
await client.getRoomById(roomId)!.setFavourite(makeFavorite);
}
},
);
cancelAction();
}
final button = posContext.findRenderObject() as RenderBox;
Future<void> toggleMuted() async {
await showFutureLoadingDialog(
context: context,
future: () async {
final newState = anySelectedRoomNotMuted
? PushRuleState.mentionsOnly
: PushRuleState.notify;
final client = Matrix.of(context).client;
for (final roomId in selectedRoomIds) {
final room = client.getRoomById(roomId)!;
if (room.pushRuleState == newState) continue;
await client.getRoomById(roomId)!.setPushRuleState(newState);
}
},
final position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(const Offset(0, -65), ancestor: overlay),
button.localToGlobal(
button.size.bottomRight(Offset.zero) + const Offset(-50, 0),
ancestor: overlay,
),
),
Offset.zero & overlay.size,
);
cancelAction();
}
Future<void> archiveAction() async {
final confirmed = await showOkCancelAlertDialog(
final displayname =
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!));
final action = await showMenu<ChatContextAction>(
context: posContext,
position: position,
items: [
PopupMenuItem(
value: ChatContextAction.open,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
mxContent: room.avatar,
size: Avatar.defaultSize / 2,
name: displayname,
),
const SizedBox(width: 12),
Text(
displayname,
style:
TextStyle(color: Theme.of(context).colorScheme.onSurface),
),
],
),
),
const PopupMenuDivider(),
if (space != null)
PopupMenuItem(
value: ChatContextAction.goToSpace,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
mxContent: space.avatar,
size: Avatar.defaultSize / 2,
name: space.getLocalizedDisplayname(),
),
const SizedBox(width: 12),
Expanded(
child: Text(
L10n.of(context)!
.goToSpace(space.getLocalizedDisplayname()),
),
),
],
),
),
PopupMenuItem(
value: ChatContextAction.mute,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
room.pushRuleState == PushRuleState.notify
? Icons.notifications_off_outlined
: Icons.notifications_off,
),
const SizedBox(width: 12),
Text(
room.pushRuleState == PushRuleState.notify
? L10n.of(context)!.muteChat
: L10n.of(context)!.unmuteChat,
),
],
),
),
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,
),
],
),
),
PopupMenuItem(
value: ChatContextAction.leave,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.delete_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
),
),
],
);
if (action == null) return;
if (!mounted) return;
switch (action) {
case ChatContextAction.open:
onChatTap(room);
return;
case ChatContextAction.goToSpace:
setActiveSpace(space!.id);
return;
case ChatContextAction.favorite:
await showFutureLoadingDialog(
context: context,
future: () => room.setFavourite(!room.isFavourite),
);
return;
case ChatContextAction.markUnread:
await showFutureLoadingDialog(
context: context,
future: () => room.markUnread(!room.markedUnread),
);
return;
case ChatContextAction.mute:
await showFutureLoadingDialog(
context: context,
future: () => room.setPushRuleState(
room.pushRuleState == PushRuleState.notify
? PushRuleState.mentionsOnly
: PushRuleState.notify,
),
);
return;
case ChatContextAction.leave:
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
okLabel: L10n.of(context)!.leave,
cancelLabel: L10n.of(context)!.no,
message: L10n.of(context)!.archiveRoomDescription,
) ==
OkCancelResult.ok;
if (!confirmed) return;
await showFutureLoadingDialog(
context: context,
future: () => _archiveSelectedRooms(),
);
setState(() {});
isDestructiveAction: true,
);
if (confirmed == OkCancelResult.cancel) return;
if (!mounted) return;
await showFutureLoadingDialog(context: context, future: room.leave);
return;
}
}
void dismissStatusList() async {
@ -568,76 +770,6 @@ class ChatListController extends State<ChatList>
);
}
Future<void> _archiveSelectedRooms() async {
final client = Matrix.of(context).client;
while (selectedRoomIds.isNotEmpty) {
final roomId = selectedRoomIds.first;
try {
await client.getRoomById(roomId)!.leave();
} finally {
toggleSelection(roomId);
}
}
}
Future<void> addToSpace() async {
final selectedSpace = await showConfirmationDialog<String>(
context: context,
title: L10n.of(context)!.addToSpace,
message: L10n.of(context)!.addToSpaceDescription,
fullyCapitalizedForMaterial: false,
actions: Matrix.of(context)
.client
.rooms
.where((r) => r.isSpace)
.map(
(space) => AlertDialogAction(
key: space.id,
label: space
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
),
)
.toList(),
);
if (selectedSpace == null) return;
final result = await showFutureLoadingDialog(
context: context,
future: () async {
final space = Matrix.of(context).client.getRoomById(selectedSpace)!;
if (space.canSendDefaultStates) {
for (final roomId in selectedRoomIds) {
await space.setSpaceChild(roomId);
}
}
},
);
if (result.error == null) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.chatHasBeenAddedToThisSpace),
),
);
}
setState(() => selectedRoomIds.clear());
}
bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any(
(roomId) =>
!Matrix.of(context).client.getRoomById(roomId)!.markedUnread,
);
bool get anySelectedRoomNotFavorite => selectedRoomIds.any(
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite,
);
bool get anySelectedRoomNotMuted => selectedRoomIds.any(
(roomId) =>
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
PushRuleState.notify,
);
bool waitForFirstSync = false;
Future<void> _waitForFirstSync() async {
@ -666,19 +798,20 @@ class ChatListController extends State<ChatList>
void cancelAction() {
if (selectMode == SelectMode.share) {
setState(() => Matrix.of(context).shareContent = null);
} else {
setState(() => selectedRoomIds.clear());
}
}
void setActiveFilter(ActiveFilter filter) {
setState(() {
activeFilter = filter;
});
}
void setActiveClient(Client client) {
context.go('/rooms');
setState(() {
activeFilter = AppConfig.separateChatTypes
? ActiveFilter.messages
: ActiveFilter.allChats;
activeSpaceId = null;
selectedRoomIds.clear();
activeFilter = ActiveFilter.allChats;
_activeSpaceId = null;
Matrix.of(context).setActiveClient(client);
});
_clientStream.add(client);
@ -687,7 +820,7 @@ class ChatListController extends State<ChatList>
void setActiveBundle(String bundle) {
context.go('/rooms');
setState(() {
selectedRoomIds.clear();
_activeSpaceId = null;
Matrix.of(context).activeBundle = bundle;
if (!Matrix.of(context)
.currentBundle!
@ -780,3 +913,18 @@ class ChatListController extends State<ChatList>
}
enum EditBundleAction { addToBundle, removeFromBundle }
enum InviteActions {
accept,
decline,
block,
}
enum ChatContextAction {
open,
goToSpace,
favorite,
markUnread,
mute,
leave,
}

View file

@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
@ -11,11 +10,11 @@ import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/space_view.dart';
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
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';
@ -29,6 +28,29 @@ class ChatListViewBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
final activeSpace = controller.activeSpaceId;
if (activeSpace != null) {
return SpaceView(
spaceId: activeSpace,
onBack: controller.clearActiveSpace,
onChatTab: (room) => controller.onChatTap(room),
onChatContext: (room, context) =>
controller.chatContextAction(room, context),
activeChat: controller.activeChat,
toParentSpace: controller.setActiveSpace,
);
}
final spaces = client.rooms.where((r) => r.isSpace);
final spaceDelegateCandidates = <String, Room>{};
for (final space in spaces) {
for (final spaceChild in space.spaceChildren) {
final roomId = spaceChild.roomId;
if (roomId == null) continue;
spaceDelegateCandidates[roomId] = space;
}
}
final publicRooms = controller.roomSearchResult?.chunk
.where((room) => room.roomType != 'm.space')
.toList();
@ -36,231 +58,283 @@ class ChatListViewBody extends StatelessWidget {
.where((room) => room.roomType == 'm.space')
.toList();
final userSearchResult = controller.userSearchResult;
final client = Matrix.of(context).client;
const dummyChatCount = 4;
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
final filter = controller.searchController.text.toLowerCase();
return PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.vertical,
fillColor: Theme.of(context).scaffoldBackgroundColor,
child: child,
);
},
child: StreamBuilder(
key: ValueKey(
client.userID.toString() +
controller.activeFilter.toString() +
controller.activeSpaceId.toString(),
),
stream: client.onSync.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, _) {
if (controller.activeFilter == ActiveFilter.spaces) {
return SpaceView(
controller,
scrollController: controller.scrollController,
key: Key(controller.activeSpaceId ?? 'Spaces'),
);
}
final rooms = controller.filteredRooms;
return SafeArea(
child: CustomScrollView(
controller: controller.scrollController,
slivers: [
ChatListHeader(controller: controller),
SliverList(
delegate: SliverChildListDelegate(
[
if (controller.isSearchMode) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
icon: const Icon(Icons.explore_outlined),
return StreamBuilder(
key: ValueKey(
client.userID.toString(),
),
stream: client.onSync.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, _) {
final rooms = controller.filteredRooms;
return SafeArea(
child: CustomScrollView(
controller: controller.scrollController,
slivers: [
ChatListHeader(controller: controller),
SliverList(
delegate: SliverChildListDelegate(
[
if (controller.isSearchMode) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
icon: const Icon(Icons.explore_outlined),
),
PublicRoomsHorizontalList(publicRooms: publicRooms),
SearchTitle(
title: L10n.of(context)!.publicSpaces,
icon: const Icon(Icons.workspaces_outlined),
),
PublicRoomsHorizontalList(publicRooms: publicSpaces),
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title:
userSearchResult.results[i].displayName ??
userSearchResult
.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar: userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
profile: userSearchResult.results[i],
outerContext: context,
),
),
),
),
),
],
if (!controller.isSearchMode && AppConfig.showPresences)
GestureDetector(
onLongPress: () => controller.dismissStatusList(),
child: StatusMessageList(
onStatusEdit: controller.setStatus,
),
PublicRoomsHorizontalList(publicRooms: publicRooms),
SearchTitle(
title: L10n.of(context)!.publicSpaces,
icon: const Icon(Icons.workspaces_outlined),
),
const ConnectionStatusHeader(),
AnimatedContainer(
height: controller.isTorBrowser ? 64 : 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(L10n.of(context)!.dehydrateTor),
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: controller.dehydrate,
),
PublicRoomsHorizontalList(publicRooms: publicSpaces),
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult
.results[i].displayName ??
userSearchResult
.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar:
userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
profile: userSearchResult.results[i],
outerContext: context,
),
),
if (client.rooms.isNotEmpty && !controller.isSearchMode)
SizedBox(
height: 44,
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6,
),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
if (AppConfig.separateChatTypes)
ActiveFilter.messages
else
ActiveFilter.allChats,
ActiveFilter.groups,
ActiveFilter.unread,
if (spaceDelegateCandidates.isNotEmpty &&
!controller.widget.displayNavigationRail)
ActiveFilter.spaces,
]
.map(
(filter) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4),
child: HoverBuilder(
builder: (context, hovered) =>
AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: InkWell(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
onTap: () =>
controller.setActiveFilter(filter),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: filter ==
controller.activeFilter
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.secondaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
alignment: Alignment.center,
child: Text(
filter.toLocalizedString(context),
style: TextStyle(
fontWeight: filter ==
controller.activeFilter
? FontWeight.bold
: FontWeight.normal,
color: filter ==
controller.activeFilter
? Theme.of(context)
.colorScheme
.onPrimary
: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
),
),
),
),
),
),
],
if (!controller.isSearchMode &&
controller.activeFilter != ActiveFilter.groups &&
AppConfig.showPresences)
GestureDetector(
onLongPress: () => controller.dismissStatusList(),
child: StatusMessageList(
onStatusEdit: controller.setStatus,
),
),
const ConnectionStatusHeader(),
AnimatedContainer(
height: controller.isTorBrowser ? 64 : 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: const Icon(Icons.vpn_key),
title: Text(L10n.of(context)!.dehydrateTor),
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: controller.dehydrate,
),
)
.toList(),
),
),
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.chats,
icon: const Icon(Icons.forum_outlined),
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.chats,
icon: const Icon(Icons.forum_outlined),
),
if (client.prevBatch != null &&
rooms.isEmpty &&
!controller.isSearchMode) ...[
Padding(
padding: const EdgeInsets.all(32.0),
child: Icon(
CupertinoIcons.chat_bubble_2,
size: 128,
color: Theme.of(context).colorScheme.secondary,
),
if (client.prevBatch != null &&
rooms.isEmpty &&
!controller.isSearchMode) ...[
Padding(
padding: const EdgeInsets.all(32.0),
child: Icon(
CupertinoIcons.chat_bubble_2,
size: 128,
color:
Theme.of(context).colorScheme.onInverseSurface,
),
],
],
),
),
if (client.prevBatch == null)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
],
],
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
childCount: dummyChatCount,
),
),
if (client.prevBatch == null)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color:
Theme.of(context).textTheme.bodyLarge!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
childCount: dummyChatCount,
),
),
if (client.prevBatch != null)
SliverList.builder(
itemCount: rooms.length,
itemBuilder: (BuildContext context, int i) {
return ChatListItem(
rooms[i],
key: Key('chat_list_item_${rooms[i].id}'),
filter: filter,
selected:
controller.selectedRoomIds.contains(rooms[i].id),
onTap: controller.selectMode == SelectMode.select
? () => controller.toggleSelection(rooms[i].id)
: () => onChatTap(rooms[i], context),
onLongPress: () =>
controller.toggleSelection(rooms[i].id),
activeChat: controller.activeChat == rooms[i].id,
);
},
),
],
),
);
},
),
if (client.prevBatch != null)
SliverList.builder(
itemCount: rooms.length,
itemBuilder: (BuildContext context, int i) {
final room = rooms[i];
final space = spaceDelegateCandidates[room.id];
return ChatListItem(
room,
space: space,
key: Key('chat_list_item_${room.id}'),
filter: filter,
onTap: () => controller.onChatTap(room),
onLongPress: (context) =>
controller.chatContextAction(room, context, space),
activeChat: controller.activeChat == room.id,
);
},
),
],
),
);
},
);
}
}

View file

@ -43,88 +43,77 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
L10n.of(context)!.share,
key: const ValueKey(SelectMode.share),
)
: selectMode == SelectMode.select
? Text(
controller.selectedRoomIds.length.toString(),
key: const ValueKey(SelectMode.select),
)
: TextField(
controller: controller.searchController,
focusNode: controller.searchFocusNode,
textInputAction: TextInputAction.search,
onChanged: (text) => controller.onSearchEnter(
text,
globalSearch: globalSearch,
),
decoration: InputDecoration(
fillColor: Theme.of(context).colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context)!.searchChatsRooms,
hintStyle: TextStyle(
color: Theme.of(context).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.of(context)
.colorScheme
.onPrimaryContainer,
)
: IconButton(
onPressed: controller.startSearch,
icon: Icon(
Icons.search_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
suffixIcon: controller.isSearchMode && globalSearch
? controller.isSearching
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
: TextButton.icon(
onPressed: controller.setServer,
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
),
textStyle: const TextStyle(fontSize: 12),
),
icon: const Icon(Icons.edit_outlined, size: 16),
label: Text(
controller.searchServer ??
Matrix.of(context)
.client
.homeserver!
.host,
maxLines: 2,
),
)
: SizedBox(
width: 0,
child: ClientChooserButton(controller),
),
),
: TextField(
controller: controller.searchController,
focusNode: controller.searchFocusNode,
textInputAction: TextInputAction.search,
onChanged: (text) => controller.onSearchEnter(
text,
globalSearch: globalSearch,
),
decoration: InputDecoration(
fillColor: Theme.of(context).colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context)!.searchChatsRooms,
hintStyle: TextStyle(
color: Theme.of(context).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.of(context).colorScheme.onPrimaryContainer,
)
: IconButton(
onPressed: controller.startSearch,
icon: Icon(
Icons.search_outlined,
color:
Theme.of(context).colorScheme.onPrimaryContainer,
),
),
suffixIcon: controller.isSearchMode && globalSearch
? controller.isSearching
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
: TextButton.icon(
onPressed: controller.setServer,
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
),
textStyle: const TextStyle(fontSize: 12),
),
icon: const Icon(Icons.edit_outlined, size: 16),
label: Text(
controller.searchServer ??
Matrix.of(context).client.homeserver!.host,
maxLines: 2,
),
)
: SizedBox(
width: 0,
child: ClientChooserButton(controller),
),
),
),
actions: selectMode == SelectMode.share
? [
Padding(
@ -135,48 +124,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
child: ClientChooserButton(controller),
),
]
: selectMode == SelectMode.select
? [
if (controller.spaces.isNotEmpty)
IconButton(
tooltip: L10n.of(context)!.addToSpace,
icon: const Icon(Icons.workspaces_outlined),
onPressed: controller.addToSpace,
),
IconButton(
tooltip: L10n.of(context)!.toggleUnread,
icon: Icon(
controller.anySelectedRoomNotMarkedUnread
? Icons.mark_chat_unread_outlined
: Icons.mark_chat_read_outlined,
),
onPressed: controller.toggleUnread,
),
IconButton(
tooltip: L10n.of(context)!.toggleFavorite,
icon: Icon(
controller.anySelectedRoomNotFavorite
? Icons.push_pin
: Icons.push_pin_outlined,
),
onPressed: controller.toggleFavouriteRoom,
),
IconButton(
icon: Icon(
controller.anySelectedRoomNotMuted
? Icons.notifications_off_outlined
: Icons.notifications_outlined,
),
tooltip: L10n.of(context)!.toggleMuted,
onPressed: controller.toggleMuted,
),
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context)!.archive,
onPressed: controller.archiveAction,
),
]
: null,
: null,
);
}

View file

@ -17,9 +17,9 @@ enum ArchivedRoomAction { delete, rejoin }
class ChatListItem extends StatelessWidget {
final Room room;
final Room? space;
final bool activeChat;
final bool selected;
final void Function()? onLongPress;
final void Function(BuildContext context)? onLongPress;
final void Function()? onForget;
final void Function() onTap;
final String? filter;
@ -27,11 +27,11 @@ class ChatListItem extends StatelessWidget {
const ChatListItem(
this.room, {
this.activeChat = false,
this.selected = false,
required this.onTap,
this.onLongPress,
this.onForget,
this.filter,
this.space,
super.key,
});
@ -77,11 +77,8 @@ class ChatListItem extends StatelessWidget {
: 14.0
: 0.0;
final hasNotifications = room.notificationCount > 0;
final backgroundColor = selected
? theme.colorScheme.primaryContainer
: activeChat
? theme.colorScheme.secondaryContainer
: null;
final backgroundColor =
activeChat ? theme.colorScheme.secondaryContainer : null;
final displayname = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
@ -93,6 +90,7 @@ class ChatListItem extends StatelessWidget {
final needLastEventSender = lastEvent == null
? false
: room.getState(EventTypes.RoomMember, lastEvent.senderId) == null;
final space = this.space;
return Padding(
padding: const EdgeInsets.symmetric(
@ -106,47 +104,89 @@ class ChatListItem extends StatelessWidget {
child: FutureBuilder(
future: room.loadHeroUsers(),
builder: (context, snapshot) => HoverBuilder(
builder: (context, hovered) => ListTile(
builder: (context, listTileHovered) => ListTile(
visualDensity: const VisualDensity(vertical: -0.5),
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
onLongPress: onLongPress,
leading: Stack(
clipBehavior: Clip.none,
children: [
HoverBuilder(
builder: (context, hovered) => AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: Avatar(
mxContent: room.avatar,
name: displayname,
presenceUserId: directChatMatrixId,
presenceBackgroundColor: backgroundColor,
onTap: onLongPress,
),
),
),
Positioned(
bottom: -2,
right: -2,
child: AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: (hovered || selected) ? 1.0 : 0.0,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
child: Icon(
selected
? Icons.check_circle
: Icons.check_circle_outlined,
size: 18,
onLongPress: () => onLongPress?.call(context),
leading: HoverBuilder(
builder: (context, hovered) => AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: SizedBox(
width: Avatar.defaultSize,
height: Avatar.defaultSize,
child: Stack(
children: [
if (space != null)
Positioned(
top: 0,
left: 0,
child: Avatar(
border: BorderSide(
width: 2,
color: backgroundColor ??
Theme.of(context).colorScheme.surface,
),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
mxContent: space.avatar,
size: Avatar.defaultSize * 0.75,
name: space.getLocalizedDisplayname(),
onTap: () => onLongPress?.call(context),
),
),
Positioned(
bottom: 0,
right: 0,
child: Avatar(
border: space == null
? null
: BorderSide(
width: 2,
color: backgroundColor ??
Theme.of(context).colorScheme.surface,
),
borderRadius: room.isSpace
? BorderRadius.circular(
AppConfig.borderRadius / 4,
)
: null,
mxContent: room.avatar,
size: space != null
? Avatar.defaultSize * 0.75
: Avatar.defaultSize,
name: displayname,
presenceUserId: directChatMatrixId,
presenceBackgroundColor: backgroundColor,
onTap: () => onLongPress?.call(context),
),
),
),
Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: () => onLongPress?.call(context),
child: AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: listTileHovered ? 1.0 : 0.0,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
child: const Icon(
Icons.arrow_drop_down_circle_outlined,
size: 18,
),
),
),
),
),
],
),
),
],
),
),
title: Row(
children: <Widget>[
@ -180,7 +220,9 @@ class ChatListItem extends StatelessWidget {
color: theme.colorScheme.primary,
),
),
if (lastEvent != null && room.membership != Membership.invite)
if (!room.isSpace &&
lastEvent != null &&
room.membership != Membership.invite)
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
@ -193,6 +235,11 @@ class ChatListItem extends StatelessWidget {
),
),
),
if (room.isSpace)
const Icon(
Icons.arrow_circle_right_outlined,
size: 18,
),
],
),
subtitle: Row(
@ -222,62 +269,70 @@ class ChatListItem extends StatelessWidget {
),
),
Expanded(
child: typingText.isNotEmpty
child: room.isSpace && room.membership == Membership.join
? Text(
typingText,
style: TextStyle(
color: theme.colorScheme.primary,
L10n.of(context)!.countChatsAndCountParticipants(
room.spaceChildren.length.toString(),
(room.summary.mJoinedMemberCount ?? 1).toString(),
),
maxLines: 1,
softWrap: false,
)
: FutureBuilder(
key: ValueKey(
'${lastEvent?.eventId}_${lastEvent?.type}',
),
future: needLastEventSender
? lastEvent.calcLocalizedBody(
MatrixLocals(L10n.of(context)!),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: !isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId,
)
: null,
initialData: lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: !isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId,
),
builder: (context, snapshot) => Text(
room.membership == Membership.invite
? isDirectChat
? L10n.of(context)!.invitePrivateChat
: L10n.of(context)!.inviteGroupChat
: snapshot.data ??
L10n.of(context)!.emptyChat,
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: unread || room.hasNewMessages
? FontWeight.bold
: null,
color: theme.colorScheme.onSurfaceVariant,
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: typingText.isNotEmpty
? Text(
typingText,
style: TextStyle(
color: theme.colorScheme.primary,
),
maxLines: 1,
softWrap: false,
)
: FutureBuilder(
key: ValueKey(
'${lastEvent?.eventId}_${lastEvent?.type}',
),
future: needLastEventSender
? lastEvent.calcLocalizedBody(
MatrixLocals(L10n.of(context)!),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId),
)
: null,
initialData:
lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId),
),
builder: (context, snapshot) => Text(
room.membership == Membership.invite
? isDirectChat
? L10n.of(context)!.invitePrivateChat
: L10n.of(context)!.inviteGroupChat
: snapshot.data ??
L10n.of(context)!.emptyChat,
softWrap: false,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: unread || room.hasNewMessages
? FontWeight.bold
: null,
color: theme.colorScheme.onSurfaceVariant,
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
),
),
),
),
),
const SizedBox(width: 8),
AnimatedContainer(

View file

@ -1,84 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:badges/badges.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/navi_rail_item.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
import '../../widgets/matrix.dart';
import 'chat_list_body.dart';
import 'start_chat_fab.dart';
class ChatListView extends StatelessWidget {
final ChatListController controller;
const ChatListView(this.controller, {super.key});
List<NavigationDestination> getNavigationDestinations(BuildContext context) {
final badgePosition = BadgePosition.topEnd(top: -12, end: -8);
return [
if (AppConfig.separateChatTypes) ...[
NavigationDestination(
icon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter:
controller.getRoomFilterByActiveFilter(ActiveFilter.messages),
child: const Icon(Icons.chat_outlined),
),
selectedIcon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter:
controller.getRoomFilterByActiveFilter(ActiveFilter.messages),
child: const Icon(Icons.chat),
),
label: L10n.of(context)!.messages,
),
NavigationDestination(
icon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups),
child: const Icon(Icons.group_outlined),
),
selectedIcon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups),
child: const Icon(Icons.group),
),
label: L10n.of(context)!.groups,
),
] else
NavigationDestination(
icon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter:
controller.getRoomFilterByActiveFilter(ActiveFilter.allChats),
child: const Icon(Icons.chat_outlined),
),
selectedIcon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter:
controller.getRoomFilterByActiveFilter(ActiveFilter.allChats),
child: const Icon(Icons.chat),
),
label: L10n.of(context)!.chats,
),
if (controller.spaces.isNotEmpty)
const NavigationDestination(
icon: Icon(Icons.workspaces_outlined),
selectedIcon: Icon(Icons.workspaces),
label: 'Spaces',
),
];
}
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
@ -89,12 +31,13 @@ class ChatListView extends StatelessWidget {
return PopScope(
canPop: controller.selectMode == SelectMode.normal &&
!controller.isSearchMode &&
controller.activeFilter ==
(AppConfig.separateChatTypes
? ActiveFilter.messages
: ActiveFilter.allChats),
onPopInvoked: (pop) async {
controller.activeSpaceId == null,
onPopInvoked: (pop) {
if (pop) return;
if (controller.activeSpaceId != null) {
controller.clearActiveSpace();
return;
}
final selMode = controller.selectMode;
if (controller.isSearchMode) {
controller.cancelSearch();
@ -104,23 +47,23 @@ class ChatListView extends StatelessWidget {
controller.cancelAction();
return;
}
if (controller.activeFilter !=
(AppConfig.separateChatTypes
? ActiveFilter.messages
: ActiveFilter.allChats)) {
controller
.onDestinationSelected(AppConfig.separateChatTypes ? 1 : 0);
return;
}
},
child: Row(
children: [
if (FluffyThemes.isColumnMode(context) &&
controller.widget.displayNavigationRail) ...[
Builder(
builder: (context) {
final allSpaces =
client.rooms.where((room) => room.isSpace);
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(
@ -129,40 +72,53 @@ class ChatListView extends StatelessWidget {
),
)
.toList();
final destinations = getNavigationDestinations(context);
return SizedBox(
width: FluffyThemes.navRailWidth,
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: rootSpaces.length + destinations.length,
itemCount: rootSpaces.length + 2,
itemBuilder: (context, i) {
if (i < destinations.length) {
if (i == 0) {
return NaviRailItem(
isSelected: i == controller.selectedIndex,
onTap: () => controller.onDestinationSelected(i),
icon: destinations[i].icon,
selectedIcon: destinations[i].selectedIcon,
toolTip: destinations[i].label,
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 -= destinations.length;
final isSelected =
controller.activeFilter == ActiveFilter.spaces &&
rootSpaces[i].id == controller.activeSpaceId;
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: rootSpaces[i].getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
isSelected: isSelected,
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: rootSpaces[i].getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
name: displayname,
size: 32,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
);
},
@ -182,23 +138,6 @@ class ChatListView extends StatelessWidget {
behavior: HitTestBehavior.translucent,
child: Scaffold(
body: ChatListViewBody(controller),
bottomNavigationBar: controller.displayNavigationBar
? NavigationBar(
elevation: 4,
labelBehavior:
NavigationDestinationLabelBehavior.alwaysShow,
shadowColor:
Theme.of(context).colorScheme.onSurface,
backgroundColor:
Theme.of(context).colorScheme.surface,
surfaceTintColor:
Theme.of(context).colorScheme.surface,
selectedIndex: controller.selectedIndex,
onDestinationSelected:
controller.onDestinationSelected,
destinations: getNavigationDestinations(context),
)
: null,
floatingActionButton: KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
@ -207,12 +146,16 @@ class ChatListView extends StatelessWidget {
onKeysPressed: () => context.go('/rooms/newprivatechat'),
helpLabel: L10n.of(context)!.newChat,
child: selectMode == SelectMode.normal &&
!controller.isSearchMode
? StartChatFloatingActionButton(
activeFilter: controller.activeFilter,
roomsIsEmpty: false,
scrolledToTop: controller.scrolledToTop,
createNewSpace: controller.createNewSpace,
!controller.isSearchMode &&
controller.activeSpaceId == null
? FloatingActionButton.extended(
onPressed: () =>
context.go('/rooms/newprivatechat'),
icon: const Icon(Icons.add_outlined),
label: Text(
L10n.of(context)!.chat,
overflow: TextOverflow.fade,
),
)
: const SizedBox.shrink(),
),

View file

@ -68,7 +68,9 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
PopupMenuItem(
// Currently disabled because of:
// https://github.com/matrix-org/matrix-react-sdk/pull/12286
/*PopupMenuItem(
value: SettingsAction.archive,
child: Row(
children: [
@ -77,7 +79,7 @@ class ClientChooserButton extends StatelessWidget {
Text(L10n.of(context)!.archive),
],
),
),
),*/
PopupMenuItem(
value: SettingsAction.settings,
child: Row(

View file

@ -0,0 +1,95 @@
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 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.of(context).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.of(context).colorScheme.primaryContainer
: Theme.of(context).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

@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:badges/badges.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
import '../../config/themes.dart';
class NaviRailItem extends StatefulWidget {
class NaviRailItem extends StatelessWidget {
final String toolTip;
final bool isSelected;
final void Function() onTap;
final Widget icon;
final Widget? selectedIcon;
final bool Function(Room)? unreadBadgeFilter;
const NaviRailItem({
required this.toolTip,
@ -16,80 +22,78 @@ class NaviRailItem extends StatefulWidget {
required this.onTap,
required this.icon,
this.selectedIcon,
this.unreadBadgeFilter,
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 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.of(context).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.of(context).colorScheme.primaryContainer
: Theme.of(context).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,
final icon = isSelected ? selectedIcon ?? this.icon : this.icon;
final unreadBadgeFilter = this.unreadBadgeFilter;
return HoverBuilder(
builder: (context, hovered) {
return SizedBox(
height: FluffyThemes.navRailWidth,
width: FluffyThemes.navRailWidth,
child: Stack(
children: [
Positioned(
top: 16,
bottom: 16,
left: 0,
child: AnimatedContainer(
width: isSelected ? 4 : 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
decoration: BoxDecoration(
color: Theme.of(context).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: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surface,
child: Tooltip(
message: toolTip,
child: InkWell(
borderRadius: borderRadius,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 8.0,
),
child: unreadBadgeFilter == null
? icon
: UnreadRoomsBadge(
filter: unreadBadgeFilter,
badgePosition: BadgePosition.topEnd(
top: -12,
end: -8,
),
child: icon,
),
),
),
),
),
),
),
],
),
],
),
);
},
);
}
}

View file

@ -8,23 +8,34 @@ import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as sdk;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.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/avatar.dart';
import '../../utils/localized_exception_extension.dart';
import '../../widgets/matrix.dart';
import 'chat_list_header.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
enum AddRoomType { chat, subspace }
class SpaceView extends StatefulWidget {
final ChatListController controller;
final ScrollController scrollController;
const SpaceView(
this.controller, {
final String spaceId;
final void Function() onBack;
final void Function(String spaceId) toParentSpace;
final void Function(Room room) onChatTab;
final void Function(Room room, BuildContext context) onChatContext;
final String? activeChat;
const SpaceView({
required this.spaceId,
required this.onBack,
required this.onChatTab,
required this.activeChat,
required this.toParentSpace,
required this.onChatContext,
super.key,
required this.scrollController,
});
@override
@ -32,157 +43,114 @@ class SpaceView extends StatefulWidget {
}
class _SpaceViewState extends State<SpaceView> {
static final Map<String, GetSpaceHierarchyResponse> _lastResponse = {};
String? prevBatch;
Object? error;
bool loading = false;
final List<SpaceRoomsChunk> _discoveredChildren = [];
final TextEditingController _filterController = TextEditingController();
String? _nextBatch;
bool _noMoreRooms = false;
bool _isLoading = false;
@override
void initState() {
loadHierarchy();
_loadHierarchy();
super.initState();
}
void _refresh() {
_lastResponse.remove(widget.controller.activeSpaceId);
loadHierarchy();
}
Future<GetSpaceHierarchyResponse?> loadHierarchy([String? prevBatch]) async {
final activeSpaceId = widget.controller.activeSpaceId;
if (activeSpaceId == null) return null;
final client = Matrix.of(context).client;
final activeSpace = client.getRoomById(activeSpaceId);
await activeSpace?.postLoad();
void _loadHierarchy() async {
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
if (room == null) return;
setState(() {
error = null;
loading = true;
_isLoading = true;
});
try {
final response = await client.getSpaceHierarchy(
activeSpaceId,
maxDepth: 1,
from: prevBatch,
final hierarchy = await room.client.getSpaceHierarchy(
widget.spaceId,
suggestedOnly: false,
maxDepth: 2,
from: _nextBatch,
);
if (prevBatch != null) {
response.rooms.insertAll(0, _lastResponse[activeSpaceId]?.rooms ?? []);
}
if (!mounted) return;
setState(() {
_lastResponse[activeSpaceId] = response;
_nextBatch = hierarchy.nextBatch;
if (hierarchy.nextBatch == null) {
_noMoreRooms = true;
}
_discoveredChildren.addAll(
hierarchy.rooms
.where((c) => room.client.getRoomById(c.roomId) == null),
);
_isLoading = false;
});
return _lastResponse[activeSpaceId]!;
} catch (e) {
} catch (e, s) {
Logs().w('Unable to load hierarchy', e, s);
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
setState(() {
error = e;
});
rethrow;
} finally {
setState(() {
loading = false;
_isLoading = false;
});
}
}
void _onJoinSpaceChild(SpaceRoomsChunk spaceChild) async {
void _joinChildRoom(SpaceRoomsChunk item) async {
final client = Matrix.of(context).client;
final space = client.getRoomById(widget.controller.activeSpaceId!);
if (client.getRoomById(spaceChild.roomId) == null) {
final result = await showFutureLoadingDialog(
context: context,
future: () async {
await client.joinRoom(
spaceChild.roomId,
serverName: space?.spaceChildren
.firstWhereOrNull(
(child) => child.roomId == spaceChild.roomId,
)
?.via,
);
if (client.getRoomById(spaceChild.roomId) == null) {
// Wait for room actually appears in sync
await client.waitForRoomInSync(spaceChild.roomId, join: true);
}
},
);
if (result.error != null) return;
_refresh();
}
if (spaceChild.roomType == 'm.space') {
if (spaceChild.roomId == widget.controller.activeSpaceId) {
context.go('/rooms/${spaceChild.roomId}');
} else {
widget.controller.setActiveSpace(spaceChild.roomId);
}
return;
}
context.go('/rooms/${spaceChild.roomId}');
}
final space = client.getRoomById(widget.spaceId);
void _onSpaceChildContextMenu([
SpaceRoomsChunk? spaceChild,
Room? room,
]) async {
final client = Matrix.of(context).client;
final activeSpaceId = widget.controller.activeSpaceId;
final activeSpace =
activeSpaceId == null ? null : client.getRoomById(activeSpaceId);
final action = await showModalActionSheet<SpaceChildContextAction>(
final joined = await showAdaptiveBottomSheet<bool>(
context: context,
title: spaceChild?.name ??
room?.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
message: spaceChild?.topic ?? room?.topic,
actions: [
if (room == null)
SheetAction(
key: SpaceChildContextAction.join,
label: L10n.of(context)!.joinRoom,
icon: Icons.send_outlined,
),
if (spaceChild != null &&
(activeSpace?.canChangeStateEvent(EventTypes.SpaceChild) ?? false))
SheetAction(
key: SpaceChildContextAction.removeFromSpace,
label: L10n.of(context)!.removeFromSpace,
icon: Icons.delete_sweep_outlined,
),
if (room != null)
SheetAction(
key: SpaceChildContextAction.leave,
label: L10n.of(context)!.leave,
icon: Icons.delete_outlined,
isDestructiveAction: true,
),
],
builder: (_) => PublicRoomBottomSheet(
outerContext: context,
chunk: item,
via: space?.spaceChildren
.firstWhereOrNull(
(child) => child.roomId == item.roomId,
)
?.via,
),
);
if (action == null) return;
if (mounted && joined == true) {
setState(() {
_discoveredChildren.remove(item);
});
}
}
void _onSpaceAction(SpaceActions action) async {
final space = Matrix.of(context).client.getRoomById(widget.spaceId);
switch (action) {
case SpaceChildContextAction.join:
_onJoinSpaceChild(spaceChild!);
case SpaceActions.settings:
await space?.postLoad();
context.push('/rooms/${widget.spaceId}/details');
break;
case SpaceChildContextAction.leave:
await showFutureLoadingDialog(
case SpaceActions.invite:
await space?.postLoad();
context.push('/rooms/${widget.spaceId}/invite');
break;
case SpaceActions.leave:
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
future: room!.leave,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
message: L10n.of(context)!.archiveRoomDescription,
);
break;
case SpaceChildContextAction.removeFromSpace:
await showFutureLoadingDialog(
if (!mounted) return;
if (confirmed != OkCancelResult.ok) return;
final success = await showFutureLoadingDialog(
context: context,
future: () => activeSpace!.removeSpaceChild(spaceChild!.roomId),
future: () async => await space?.leave(),
);
break;
if (!mounted) return;
if (success.error != null) return;
widget.onBack();
}
}
void _addChatOrSubSpace() async {
void _addChatOrSubspace() async {
final roomType = await showConfirmationDialog(
context: context,
title: L10n.of(context)!.addChatOrSubSpace,
@ -235,9 +203,8 @@ class _SpaceViewState extends State<SpaceView> {
context: context,
future: () async {
late final String roomId;
final activeSpace = client.getRoomById(
widget.controller.activeSpaceId!,
)!;
final activeSpace = client.getRoomById(widget.spaceId)!;
await activeSpace.postLoad();
if (roomType == AddRoomType.subspace) {
roomId = await client.createSpace(
@ -250,10 +217,16 @@ class _SpaceViewState extends State<SpaceView> {
} else {
roomId = await client.createGroupChat(
groupName: names.first,
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
? [
sdk.StateEvent(
type: sdk.EventTypes.RoomTopic,
StateEvent(
type: EventTypes.RoomTopic,
content: {'topic': names.last},
),
]
@ -264,311 +237,356 @@ class _SpaceViewState extends State<SpaceView> {
},
);
if (result.error != null) return;
_refresh();
}
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
final activeSpaceId = widget.controller.activeSpaceId;
final activeSpace = activeSpaceId == null
? null
: client.getRoomById(
activeSpaceId,
);
final allSpaces = client.rooms.where((room) => room.isSpace);
if (activeSpaceId == null) {
final rootSpaces = allSpaces
.where(
(space) =>
!allSpaces.any(
(parentSpace) => parentSpace.spaceChildren
.any((child) => child.roomId == space.id),
) &&
space
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
.toLowerCase()
.contains(
widget.controller.searchController.text.toLowerCase(),
),
)
.toList();
return SafeArea(
child: CustomScrollView(
controller: widget.scrollController,
slivers: [
ChatListHeader(controller: widget.controller),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) {
final rootSpace = rootSpaces[i];
final displayname = rootSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
return Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: Avatar(
mxContent: rootSpace.avatar,
name: displayname,
),
title: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
L10n.of(context)!.numChats(
rootSpace.spaceChildren.length.toString(),
),
),
onTap: () =>
widget.controller.setActiveSpace(rootSpace.id),
onLongPress: () =>
_onSpaceChildContextMenu(null, rootSpace),
trailing: const Icon(Icons.chevron_right_outlined),
),
);
},
childCount: rootSpaces.length,
),
),
],
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
final displayname =
room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound;
return Scaffold(
appBar: AppBar(
leading: Center(
child: CloseButton(
onPressed: widget.onBack,
),
),
);
}
final parentSpace = allSpaces.firstWhereOrNull(
(space) =>
space.spaceChildren.any((child) => child.roomId == activeSpaceId),
);
return PopScope(
canPop: parentSpace == null,
onPopInvoked: (pop) async {
if (pop) return;
if (parentSpace != null) {
widget.controller.setActiveSpace(parentSpace.id);
}
},
child: SafeArea(
child: CustomScrollView(
controller: widget.scrollController,
slivers: [
ChatListHeader(controller: widget.controller, globalSearch: false),
SliverAppBar(
automaticallyImplyLeading: false,
primary: false,
titleSpacing: 0,
title: ListTile(
leading: BackButton(
onPressed: () =>
widget.controller.setActiveSpace(parentSpace?.id),
titleSpacing: 0,
title: ListTile(
contentPadding: EdgeInsets.zero,
leading: Avatar(
mxContent: room?.avatar,
name: displayname,
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
title: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: room == null
? null
: Text(
L10n.of(context)!.countChatsAndCountParticipants(
room.spaceChildren.length,
room.summary.mJoinedMemberCount ?? 1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
title: Text(
parentSpace == null
? L10n.of(context)!.allSpaces
: parentSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
),
trailing: IconButton(
icon: loading
? const CircularProgressIndicator.adaptive(strokeWidth: 2)
: const Icon(Icons.refresh_outlined),
onPressed: loading ? null : _refresh,
),
actions: [
PopupMenuButton<SpaceActions>(
onSelected: _onSpaceAction,
itemBuilder: (context) => [
PopupMenuItem(
value: SpaceActions.settings,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.settings),
],
),
),
),
Builder(
builder: (context) {
final response = _lastResponse[activeSpaceId];
final error = this.error;
if (error != null) {
return SliverFillRemaining(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(error.toLocalizedString(context)),
),
IconButton(
onPressed: _refresh,
icon: const Icon(Icons.refresh_outlined),
),
],
),
);
}
if (response == null) {
return SliverFillRemaining(
child: Center(
child: Text(L10n.of(context)!.loadingPleaseWait),
),
);
}
final spaceChildren = response.rooms;
final canLoadMore = response.nextBatch != null;
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) {
if (canLoadMore && i == spaceChildren.length) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: OutlinedButton.icon(
label: loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.loadMore),
icon: const Icon(Icons.chevron_right_outlined),
onPressed: loading
? null
: () {
loadHierarchy(response.nextBatch);
},
PopupMenuItem(
value: SpaceActions.invite,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.person_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.invite),
],
),
),
PopupMenuItem(
value: SpaceActions.leave,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.delete_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.leave),
],
),
),
],
),
],
),
body: room == null
? const Center(
child: Icon(
Icons.search_outlined,
size: 80,
),
)
: StreamBuilder(
stream: room.client.onSync.stream
.where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) {
final childrenIds = room.spaceChildren
.map((c) => c.roomId)
.whereType<String>()
.toSet();
final joinedRooms = room.client.rooms
.where((room) => childrenIds.remove(room.id))
.toList();
final joinedParents = room.spaceParents
.map((parent) {
final roomId = parent.roomId;
if (roomId == null) return null;
return room.client.getRoomById(roomId);
})
.whereType<Room>()
.toList();
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
toolbarHeight: 72,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
title: TextField(
controller: _filterController,
onChanged: (_) => setState(() {}),
textInputAction: TextInputAction.search,
decoration: InputDecoration(
fillColor:
Theme.of(context).colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
);
}
final spaceChild = spaceChildren[i];
final room = client.getRoomById(spaceChild.roomId);
if (room != null && !room.isSpace) {
return ChatListItem(
room,
onLongPress: () =>
_onSpaceChildContextMenu(spaceChild, room),
activeChat: widget.controller.activeChat == room.id,
onTap: () => onChatTap(room, context),
);
}
final isSpace = spaceChild.roomType == 'm.space';
final topic = spaceChild.topic?.isEmpty ?? true
? null
: spaceChild.topic;
if (spaceChild.roomId == activeSpaceId) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SearchTitle(
title: spaceChild.name ??
spaceChild.canonicalAlias ??
'Space',
icon: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
),
child: Avatar(
size: 24,
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context)!.search,
hintStyle: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withAlpha(128),
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.edit_outlined),
),
onTap: () => _onJoinSpaceChild(spaceChild),
.onPrimaryContainer,
),
if (activeSpace?.canChangeStateEvent(
EventTypes.SpaceChild,
) ==
true)
Material(
child: ListTile(
leading: const CircleAvatar(
child: Icon(Icons.group_add_outlined),
),
title:
Text(L10n.of(context)!.addChatOrSubSpace),
trailing:
const Icon(Icons.chevron_right_outlined),
onTap: _addChatOrSubSpace,
),
),
],
);
}
final name = spaceChild.name ??
spaceChild.canonicalAlias ??
L10n.of(context)!.chat;
if (widget.controller.isSearchMode &&
!name.toLowerCase().contains(
widget.controller.searchController.text
.toLowerCase(),
)) {
return const SizedBox.shrink();
}
return Material(
child: ListTile(
leading: Avatar(
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
),
title: Row(
children: [
Expanded(
child: Text(
name,
maxLines: 1,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
),
SliverList.builder(
itemCount: joinedParents.length,
itemBuilder: (context, i) {
final displayname =
joinedParents[i].getLocalizedDisplayname();
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
leading: Icon(
Icons.adaptive.arrow_back_outlined,
size: 16,
),
if (!isSpace) ...[
const Icon(
Icons.people_outline,
size: 16,
),
const SizedBox(width: 4),
Text(
spaceChild.numJoinedMembers.toString(),
style: const TextStyle(fontSize: 14),
title: Row(
children: [
Avatar(
mxContent: joinedParents[i].avatar,
name: displayname,
size: Avatar.defaultSize / 2,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
),
const SizedBox(width: 8),
Expanded(child: Text(displayname)),
],
),
onTap: () =>
widget.toParentSpace(joinedParents[i].id),
),
),
);
},
),
SliverList.builder(
itemCount: joinedRooms.length + 1,
itemBuilder: (context, i) {
if (i == 0) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (room.canChangeStateEvent(
EventTypes.SpaceChild,
) &&
filter.isEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: ListTile(
onTap: _addChatOrSubspace,
leading: const CircleAvatar(
radius: Avatar.defaultSize / 2,
child: Icon(Icons.add_outlined),
),
title: Text(
L10n.of(context)!.addChatOrSubSpace,
style: const TextStyle(fontSize: 14),
),
),
),
),
],
SearchTitle(
title: L10n.of(context)!.joinedChats,
icon: const Icon(Icons.chat_outlined),
),
],
);
}
i--;
final joinedRoom = joinedRooms[i];
return ChatListItem(
joinedRoom,
filter: filter,
onTap: () => widget.onChatTab(joinedRoom),
onLongPress: (context) => widget.onChatContext(
joinedRoom,
context,
),
onTap: () => room?.isSpace == true
? widget.controller.setActiveSpace(room!.id)
: _onSpaceChildContextMenu(spaceChild, room),
onLongPress: () =>
_onSpaceChildContextMenu(spaceChild, room),
subtitle: Text(
topic ??
(isSpace
? L10n.of(context)!.enterSpace
: L10n.of(context)!.enterRoom),
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
activeChat: widget.activeChat == joinedRoom.id,
);
},
),
SliverList.builder(
itemCount: _discoveredChildren.length + 2,
itemBuilder: (context, i) {
if (i == 0) {
return SearchTitle(
title: L10n.of(context)!.discover,
icon: const Icon(Icons.explore_outlined),
);
}
i--;
if (i == _discoveredChildren.length) {
if (_noMoreRooms) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: Text(
L10n.of(context)!.noMoreChatsFound,
style: const TextStyle(fontSize: 13),
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 2.0,
),
child: TextButton(
onPressed: _isLoading ? null : _loadHierarchy,
child: _isLoading
? LinearProgressIndicator(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
)
: Text(L10n.of(context)!.loadMore),
),
);
}
final item = _discoveredChildren[i];
final displayname = item.name ??
item.canonicalAlias ??
L10n.of(context)!.emptyChat;
if (!displayname.toLowerCase().contains(filter)) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
child: ListTile(
onTap: () => _joinChildRoom(item),
leading: Avatar(
mxContent: item.avatarUrl,
name: displayname,
borderRadius: item.roomType == 'm.space'
? BorderRadius.circular(
AppConfig.borderRadius / 2,
)
: null,
),
title: Row(
children: [
Expanded(
child: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
const Icon(
Icons.add_circle_outline_outlined,
),
],
),
subtitle: Text(
item.topic ??
L10n.of(context)!.countParticipants(
item.numJoinedMembers,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
trailing: isSpace
? const Icon(Icons.chevron_right_outlined)
: null,
),
);
},
childCount: spaceChildren.length + (canLoadMore ? 1 : 0),
),
);
},
),
],
);
},
),
],
),
),
);
}
}
enum SpaceChildContextAction {
join,
enum SpaceActions {
settings,
invite,
leave,
removeFromSpace,
}
enum AddRoomType { chat, subspace }

View file

@ -1,88 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import '../../config/themes.dart';
import 'chat_list.dart';
class StartChatFloatingActionButton extends StatelessWidget {
final ActiveFilter activeFilter;
final ValueNotifier<bool> scrolledToTop;
final bool roomsIsEmpty;
final void Function() createNewSpace;
const StartChatFloatingActionButton({
super.key,
required this.activeFilter,
required this.scrolledToTop,
required this.roomsIsEmpty,
required this.createNewSpace,
});
void _onPressed(BuildContext context) async {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
context.go('/rooms/newprivatechat');
break;
case ActiveFilter.groups:
context.go('/rooms/newgroup');
break;
case ActiveFilter.spaces:
createNewSpace();
break;
}
}
IconData get icon {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
return Icons.add_outlined;
case ActiveFilter.groups:
return Icons.group_add_outlined;
case ActiveFilter.spaces:
return Icons.workspaces_outlined;
}
}
String getLabel(BuildContext context) {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
return roomsIsEmpty
? L10n.of(context)!.startFirstChat
: L10n.of(context)!.newChat;
case ActiveFilter.groups:
return L10n.of(context)!.newGroup;
case ActiveFilter.spaces:
return L10n.of(context)!.newSpace;
}
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: scrolledToTop,
builder: (context, scrolledToTop, _) => AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.none,
child: scrolledToTop
? FloatingActionButton.extended(
onPressed: () => _onPressed(context),
icon: Icon(icon),
label: Text(
getLabel(context),
overflow: TextOverflow.fade,
),
)
: FloatingActionButton(
onPressed: () => _onPressed(context),
child: Icon(icon),
),
),
);
}
}

View file

@ -1,127 +0,0 @@
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/matrix.dart';
void onChatTap(Room room, BuildContext context) 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
: L10n.of(context)!.inviteGroupChat,
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 {
final waitForRoom = room.client.waitForRoomInSync(
room.id,
join: true,
);
await room.join();
await waitForRoom;
},
);
if (joinResult.error != null) return;
}
if (room.membership == Membership.ban) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat),
),
);
return;
}
if (room.membership == Membership.leave) {
context.go('/rooms/archive/${room.id}');
return;
}
// Share content into this room
final shareContent = Matrix.of(context).shareContent;
if (shareContent != null) {
final shareFile = shareContent.tryGet<MatrixFile>('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,
),
);
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}');
}
enum InviteActions {
accept,
decline,
block,
}

View file

@ -41,6 +41,22 @@ class ChatPermissionsSettingsView extends StatelessWidget {
)..removeWhere((k, v) => v is! int);
return Column(
children: [
ListTile(
leading: const Icon(Icons.info_outlined),
subtitle: Text(
L10n.of(context)!.chatPermissionsDescription,
),
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
L10n.of(context)!.chatPermissions,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [

View file

@ -29,7 +29,7 @@ class PermissionsListTile extends StatelessWidget {
case 'events_default':
return L10n.of(context)!.sendMessages;
case 'state_default':
return L10n.of(context)!.configureChat;
return L10n.of(context)!.changeGeneralChatSettings;
case 'ban':
return L10n.of(context)!.banFromChat;
case 'kick':
@ -37,23 +37,25 @@ class PermissionsListTile extends StatelessWidget {
case 'redact':
return L10n.of(context)!.deleteMessage;
case 'invite':
return L10n.of(context)!.inviteContact;
return L10n.of(context)!.inviteOtherUsers;
}
} else if (category == 'notifications') {
switch (permissionKey) {
case 'rooms':
return L10n.of(context)!.notifications;
return L10n.of(context)!.sendRoomNotifications;
}
} else if (category == 'events') {
switch (permissionKey) {
case EventTypes.RoomName:
return L10n.of(context)!.changeTheNameOfTheGroup;
case EventTypes.RoomTopic:
return L10n.of(context)!.changeTheDescriptionOfTheGroup;
case EventTypes.RoomPowerLevels:
return L10n.of(context)!.chatPermissions;
return L10n.of(context)!.changeTheChatPermissions;
case EventTypes.HistoryVisibility:
return L10n.of(context)!.visibilityOfTheChatHistory;
return L10n.of(context)!.changeTheVisibilityOfChatHistory;
case EventTypes.RoomCanonicalAlias:
return L10n.of(context)!.setInvitationLink;
return L10n.of(context)!.changeTheCanonicalRoomAlias;
case EventTypes.RoomAvatar:
return L10n.of(context)!.editRoomAvatar;
case EventTypes.RoomTombstone:
@ -69,32 +71,46 @@ class PermissionsListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = permission >= 100
? Colors.orangeAccent
: permission >= 50
? Colors.blueAccent
: Colors.greenAccent;
return ListTile(
title: Text(getLocalizedPowerLevelString(context)),
subtitle: Text(
L10n.of(context)!.minimumPowerLevel(permission.toString()),
title: Text(
getLocalizedPowerLevelString(context),
style: Theme.of(context).textTheme.titleSmall,
),
trailing: Material(
color: color.withAlpha(32),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
color: Theme.of(context).colorScheme.onInverseSurface,
child: DropdownButton<int>(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
underline: const SizedBox.shrink(),
onChanged: canEdit ? onChanged : null,
value: {0, 50, 100}.contains(permission) ? permission : null,
value: permission,
items: [
DropdownMenuItem(
value: 0,
child: Text(L10n.of(context)!.user),
value: permission < 50 ? permission : 0,
child: Text(
L10n.of(context)!.userLevel(permission < 50 ? permission : 0),
),
),
DropdownMenuItem(
value: 50,
child: Text(L10n.of(context)!.moderator),
value: permission < 100 && permission >= 50 ? permission : 50,
child: Text(
L10n.of(context)!.moderatorLevel(
permission < 100 && permission >= 50 ? permission : 50,
),
),
),
DropdownMenuItem(
value: 100,
child: Text(L10n.of(context)!.admin),
value: permission >= 100 ? permission : 100,
child: Text(
L10n.of(context)!
.adminLevel(permission >= 100 ? permission : 100),
),
),
DropdownMenuItem(
value: null,

View file

@ -19,8 +19,6 @@ class NewGroup extends StatefulWidget {
class NewGroupController extends State<NewGroup> {
TextEditingController nameController = TextEditingController();
TextEditingController topicController = TextEditingController();
bool publicGroup = false;
bool groupCanBeFound = true;
@ -71,11 +69,6 @@ class NewGroupController extends State<NewGroup> {
: sdk.CreateRoomPreset.privateChat,
groupName: nameController.text.isNotEmpty ? nameController.text : null,
initialState: [
if (topicController.text.isNotEmpty)
sdk.StateEvent(
type: sdk.EventTypes.RoomTopic,
content: {'topic': topicController.text},
),
if (avatar != null)
sdk.StateEvent(
type: sdk.EventTypes.RoomAvatar,

View file

@ -31,54 +31,35 @@ class NewGroupView extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: controller.loading ? null : controller.selectPhoto,
child: CircleAvatar(
radius: Avatar.defaultSize / 2,
child: avatar == null
? const Icon(Icons.camera_alt_outlined)
: ClipRRect(
borderRadius: BorderRadius.circular(90),
child: Image.memory(
avatar,
width: Avatar.defaultSize,
height: Avatar.defaultSize,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: controller.nameController,
autocorrect: false,
readOnly: controller.loading,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.groupName,
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: controller.loading ? null : controller.selectPhoto,
child: CircleAvatar(
radius: Avatar.defaultSize,
child: avatar == null
? const Icon(Icons.add_a_photo_outlined)
: ClipRRect(
borderRadius: BorderRadius.circular(90),
child: Image.memory(
avatar,
width: Avatar.defaultSize,
height: Avatar.defaultSize,
fit: BoxFit.cover,
),
),
),
),
],
),
),
const SizedBox(height: 16),
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
controller: controller.topicController,
minLines: 4,
maxLines: 4,
maxLength: 255,
autofocus: true,
controller: controller.nameController,
autocorrect: false,
readOnly: controller.loading,
decoration: InputDecoration(
hintText: L10n.of(context)!.addChatDescription,
prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.groupName,
),
),
),
@ -121,10 +102,6 @@ class NewGroupView extends StatelessWidget {
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
onPressed:
controller.loading ? null : controller.submitAction,
child: controller.loading

View file

@ -22,65 +22,37 @@ class NewSpaceView extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.info_outlined),
),
subtitle: Text(L10n.of(context)!.newSpaceDescription),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: controller.loading ? null : controller.selectPhoto,
child: CircleAvatar(
radius: Avatar.defaultSize / 2,
child: avatar == null
? const Icon(Icons.camera_alt_outlined)
: ClipRRect(
borderRadius: BorderRadius.circular(90),
child: Image.memory(
avatar,
width: Avatar.defaultSize,
height: Avatar.defaultSize,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: controller.nameController,
autocorrect: false,
readOnly: controller.loading,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.spaceName,
errorText: controller.nameError,
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: controller.loading ? null : controller.selectPhoto,
child: CircleAvatar(
radius: Avatar.defaultSize,
child: avatar == null
? const Icon(Icons.add_a_photo_outlined)
: ClipRRect(
borderRadius: BorderRadius.circular(90),
child: Image.memory(
avatar,
width: Avatar.defaultSize,
height: Avatar.defaultSize,
fit: BoxFit.cover,
),
),
),
),
],
),
),
const SizedBox(height: 16),
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
controller: controller.topicController,
minLines: 4,
maxLines: 4,
maxLength: 255,
autofocus: true,
controller: controller.nameController,
autocorrect: false,
readOnly: controller.loading,
decoration: InputDecoration(
hintText: L10n.of(context)!.addChatDescription,
errorText: controller.topicError,
prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.spaceName,
errorText: controller.nameError,
),
),
),
@ -90,15 +62,18 @@ class NewSpaceView extends StatelessWidget {
value: controller.publicGroup,
onChanged: controller.setPublicGroup,
),
ListTile(
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.info_outlined),
),
subtitle: Text(L10n.of(context)!.newSpaceDescription),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
onPressed:
controller.loading ? null : controller.submitAction,
child: controller.loading

View file

@ -209,8 +209,6 @@ class SettingsController extends State<Settings> {
final client = Matrix.of(context).client;
profileFuture ??= client.getProfileFromUserId(
client.userID!,
cache: !profileUpdated,
getFromRooms: !profileUpdated,
);
return SettingsView(this);
}

View file

@ -28,13 +28,6 @@ class SettingsView extends StatelessWidget {
),
),
title: Text(L10n.of(context)!.settings),
actions: [
TextButton.icon(
onPressed: controller.logoutAction,
label: Text(L10n.of(context)!.logout),
icon: const Icon(Icons.logout_outlined),
),
],
),
body: ListTileTheme(
iconColor: Theme.of(context).colorScheme.onSurface,
@ -55,32 +48,17 @@ class SettingsView extends StatelessWidget {
padding: const EdgeInsets.all(32.0),
child: Stack(
children: [
Material(
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
shadowColor:
Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
),
),
child: Avatar(
mxContent: profile?.avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
Avatar(
mxContent: profile?.avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
if (profile != null)
Positioned(
bottom: 0,
right: 0,
child: FloatingActionButton.small(
elevation: 2,
onPressed: controller.setAvatarAction,
heroTag: null,
child: const Icon(Icons.camera_alt_outlined),
@ -108,7 +86,9 @@ class SettingsView extends StatelessWidget {
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
style: const TextStyle(
fontSize: 18,
),
),
),
TextButton.icon(
@ -135,10 +115,7 @@ class SettingsView extends StatelessWidget {
);
},
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
if (showChatBackupBanner == null)
ListTile(
leading: const Icon(Icons.backup_outlined),
@ -154,60 +131,54 @@ class SettingsView extends StatelessWidget {
onChanged: controller.firstRunBootstrapAction,
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
ListTile(
leading: const Icon(Icons.format_paint_outlined),
title: Text(L10n.of(context)!.changeTheme),
onTap: () => context.go('/rooms/settings/style'),
trailing: const Icon(Icons.chevron_right_outlined),
),
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: Text(L10n.of(context)!.notifications),
onTap: () => context.go('/rooms/settings/notifications'),
trailing: const Icon(Icons.chevron_right_outlined),
),
ListTile(
leading: const Icon(Icons.devices_outlined),
title: Text(L10n.of(context)!.devices),
onTap: () => context.go('/rooms/settings/devices'),
trailing: const Icon(Icons.chevron_right_outlined),
),
ListTile(
leading: const Icon(Icons.forum_outlined),
title: Text(L10n.of(context)!.chat),
onTap: () => context.go('/rooms/settings/chat'),
trailing: const Icon(Icons.chevron_right_outlined),
),
ListTile(
leading: const Icon(Icons.shield_outlined),
title: Text(L10n.of(context)!.security),
onTap: () => context.go('/rooms/settings/security'),
trailing: const Icon(Icons.chevron_right_outlined),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
leading: const Icon(Icons.help_outline_outlined),
title: Text(L10n.of(context)!.help),
onTap: () => launchUrlString(AppConfig.supportUrl),
trailing: const Icon(Icons.open_in_new_outlined),
),
ListTile(
leading: const Icon(Icons.shield_sharp),
title: Text(L10n.of(context)!.privacy),
onTap: () => launchUrlString(AppConfig.privacyUrl),
trailing: const Icon(Icons.open_in_new_outlined),
),
ListTile(
leading: const Icon(Icons.info_outline_rounded),
title: Text(L10n.of(context)!.about),
onTap: () => PlatformInfos.showDialog(context),
trailing: const Icon(Icons.chevron_right_outlined),
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
leading: const Icon(Icons.logout_outlined),
title: Text(L10n.of(context)!.logout),
onTap: controller.logoutAction,
),
],
),

View file

@ -69,7 +69,7 @@ class Settings3PidView extends StatelessWidget {
.withTheseAddressesRecoveryDescription,
),
),
const Divider(height: 1),
const Divider(),
Expanded(
child: ListView.builder(
itemCount: identifier.length,

View file

@ -71,10 +71,7 @@ class SettingsChatView extends StatelessWidget {
storeKey: SettingKeys.swipeRightToLeftToReply,
defaultValue: AppConfig.swipeRightToLeftToReply,
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
L10n.of(context)!.customEmojisAndStickers,
@ -93,10 +90,7 @@ class SettingsChatView extends StatelessWidget {
child: Icon(Icons.chevron_right_outlined),
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
Divider(color: Theme.of(context).dividerColor),
ListTile(
title: Text(
L10n.of(context)!.calls,

View file

@ -117,7 +117,7 @@ class EmotesSettingsView extends StatelessWidget {
onChanged: controller.setIsGloballyActive,
),
if (!controller.readonly || controller.room != null)
const Divider(thickness: 1),
const Divider(),
imageKeys.isEmpty
? Center(
child: Padding(

View file

@ -22,8 +22,9 @@ class SettingsIgnoreListController extends State<SettingsIgnoreList> {
@override
void initState() {
super.initState();
if (widget.initialUserId != null) {
controller.text = widget.initialUserId!.replaceAll('@', '');
final initialUserId = widget.initialUserId;
if (initialUserId != null) {
controller.text = initialUserId;
}
}

View file

@ -86,7 +86,6 @@ class SettingsSecurityView extends StatelessWidget {
),
},
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
ListTile(

View file

@ -136,7 +136,6 @@ class SettingsStyleView extends StatelessWidget {
),
const SizedBox(height: 8),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
ListTile(
@ -167,7 +166,6 @@ class SettingsStyleView extends StatelessWidget {
onChanged: controller.switchTheme,
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
ListTile(
@ -192,7 +190,6 @@ class SettingsStyleView extends StatelessWidget {
defaultValue: AppConfig.separateChatTypes,
),
Divider(
height: 1,
color: Theme.of(context).dividerColor,
),
ListTile(

View file

@ -226,6 +226,45 @@ 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);
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(

View file

@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
@ -29,6 +30,7 @@ class UserBottomSheetView extends StatelessWidget {
final client = Matrix.of(controller.widget.outerContext).client;
final profileSearchError = controller.widget.profileSearchError;
final dmRoomId = client.getDirectChatFromUserId(userId);
return SafeArea(
child: Scaffold(
appBar: AppBar(
@ -36,73 +38,20 @@ class UserBottomSheetView extends StatelessWidget {
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
centerTitle: false,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(displayname),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
if (presence == null ||
(presence.presence == PresenceType.offline &&
presence.lastActiveTimestamp == null)) {
return const SizedBox.shrink();
}
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
final lastActiveTimestamp = presence.lastActiveTimestamp;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
),
),
if (presence.currentlyActive == true)
Text(
L10n.of(context)!.currentlyActive,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
)
else if (lastActiveTimestamp != null)
Text(
L10n.of(context)!.lastActiveAgo(
lastActiveTimestamp.localizedTimeShort(context),
),
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
},
),
],
),
actions: [
if (userId != client.userID &&
!client.ignoredUsers.contains(userId))
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: const Icon(Icons.block_outlined),
tooltip: L10n.of(context)!.block,
onPressed: () => controller
.participantAction(UserBottomSheetAction.ignore),
),
),
],
title: Text(displayname),
actions: dmRoomId == null
? null
: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: FloatingActionButton.small(
elevation: 0,
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
child: const Icon(Icons.chat_outlined),
),
),
],
),
body: StreamBuilder<Object>(
stream: user?.room.client.onSync.stream.where(
@ -169,25 +118,12 @@ class UserBottomSheetView extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Material(
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
),
),
child: Avatar(
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
child: Avatar(
client:
Matrix.of(controller.widget.outerContext).client,
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
),
Expanded(
@ -195,26 +131,6 @@ class UserBottomSheetView extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
'https://matrix.to/#/$userId',
context,
),
icon: Icon(
Icons.adaptive.share_outlined,
size: 16,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
),
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
@ -227,37 +143,72 @@ class UserBottomSheetView extends StatelessWidget {
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
Theme.of(context).colorScheme.onSurface,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
if (presence == null ||
(presence.presence == PresenceType.offline &&
presence.lastActiveTimestamp == null)) {
return const SizedBox.shrink();
}
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
final lastActiveTimestamp =
presence.lastActiveTimestamp;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 16),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(width: 12),
if (presence.currentlyActive == true)
Text(
L10n.of(context)!.currentlyActive,
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.bodySmall,
)
else if (lastActiveTimestamp != null)
Text(
L10n.of(context)!.lastActiveAgo(
lastActiveTimestamp
.localizedTimeShort(context),
),
overflow: TextOverflow.ellipsis,
style:
Theme.of(context).textTheme.bodySmall,
),
],
);
},
),
],
),
),
],
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: ElevatedButton.icon(
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
icon: const Icon(Icons.forum_outlined),
label: Text(
controller.widget.user == null
? L10n.of(context)!.startConversation
: L10n.of(context)!.sendAMessage,
),
),
),
PresenceBuilder(
userId: userId,
client: client,
@ -281,6 +232,49 @@ class UserBottomSheetView extends StatelessWidget {
);
},
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: dmRoomId == null
? ElevatedButton.icon(
onPressed: () => controller.participantAction(
UserBottomSheetAction.message,
),
icon: const Icon(Icons.chat_outlined),
label: Text(L10n.of(context)!.startConversation),
)
: TextField(
controller: controller.sendController,
readOnly: controller.isSending,
onSubmitted: controller.sendAction,
minLines: 1,
maxLines: 1,
textInputAction: TextInputAction.send,
decoration: InputDecoration(
errorText: controller.sendError
?.toLocalizedString(context),
hintText: L10n.of(context)!.sendMessages,
suffix: controller.isSending
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
)
: null,
suffixIcon: controller.isSending
? null
: IconButton(
icon: const Icon(Icons.send_outlined),
onPressed: controller.sendAction,
),
),
),
),
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
@ -334,8 +328,8 @@ class UserBottomSheetView extends StatelessWidget {
),
),
),
Divider(color: Theme.of(context).dividerColor),
],
Divider(color: Theme.of(context).dividerColor),
if (user != null && user.canKick)
ListTile(
textColor: Theme.of(context).colorScheme.error,
@ -370,7 +364,7 @@ class UserBottomSheetView extends StatelessWidget {
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.reportUser),
leading: const Icon(Icons.report_outlined),
leading: const Icon(Icons.gavel_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.report),
),
@ -385,6 +379,16 @@ class UserBottomSheetView extends StatelessWidget {
style: const TextStyle(color: Colors.orange),
),
),
if (userId != client.userID &&
!client.ignoredUsers.contains(userId))
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
leading: const Icon(Icons.block_outlined),
title: Text(L10n.of(context)!.block),
onTap: () => controller
.participantAction(UserBottomSheetAction.ignore),
),
],
);
},

View file

@ -8,18 +8,19 @@ Future<T?> showAdaptiveBottomSheet<T>({
required Widget Function(BuildContext) builder,
bool isDismissible = true,
bool isScrollControlled = true,
double maxHeight = 480.0,
double maxHeight = 512,
bool useRootNavigator = true,
}) =>
showModalBottomSheet(
context: context,
builder: builder,
// this sadly is ugly on desktops but otherwise breaks `.of(context)` calls
useRootNavigator: false,
useRootNavigator: useRootNavigator,
isDismissible: isDismissible,
isScrollControlled: isScrollControlled,
constraints: BoxConstraints(
maxHeight: maxHeight,
maxWidth: FluffyThemes.columnWidth * 1.5,
maxWidth: FluffyThemes.columnWidth * 1.25,
),
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(

View file

@ -344,4 +344,7 @@ class MatrixLocals extends MatrixLocalizations {
@override
String startedKeyVerification(String senderName) =>
l10n.startedKeyVerification(senderName);
@override
String invitedBy(String senderName) => l10n.invitedBy(senderName);
}

View file

@ -29,7 +29,7 @@ abstract class PlatformInfos {
static bool get usesTouchscreen => !isMobile;
static bool get platformCanRecord => (isMobile || isMacOS);
static bool get platformCanRecord => (isMobile || isMacOS || isWeb);
static String get clientName =>
'${AppConfig.applicationName} ${isWeb ? 'web' : Platform.operatingSystem}${kReleaseMode ? '' : 'Debug'}';

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/platform_infos.dart';
abstract class UpdateNotifier {
static const String versionStoreKey = 'last_known_version';
static void showUpdateSnackBar(BuildContext context) async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final currentVersion = await PlatformInfos.getVersion();
final store = await SharedPreferences.getInstance();
final storedVersion = store.getString(versionStoreKey);
if (currentVersion != storedVersion) {
if (storedVersion != null) {
ScaffoldFeatureController? controller;
controller = scaffoldMessenger.showSnackBar(
SnackBar(
duration: const Duration(seconds: 30),
content: Row(
children: [
IconButton(
icon: Icon(
Icons.close_outlined,
size: 20,
color: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () => controller?.close(),
),
Expanded(
child: Text(
L10n.of(context)!.updateInstalled(currentVersion),
),
),
],
),
action: SnackBarAction(
label: L10n.of(context)!.changelog,
onPressed: () => launchUrlString(AppConfig.changelogUrl),
),
),
);
}
await store.setString(versionStoreKey, currentVersion);
}
}
}

View file

@ -15,6 +15,9 @@ class Avatar extends StatelessWidget {
final Client? client;
final String? presenceUserId;
final Color? presenceBackgroundColor;
final BorderRadius? borderRadius;
final IconData? icon;
final BorderSide? border;
const Avatar({
this.mxContent,
@ -24,6 +27,9 @@ class Avatar extends StatelessWidget {
this.client,
this.presenceUserId,
this.presenceBackgroundColor,
this.borderRadius,
this.border,
this.icon,
super.key,
});
@ -41,82 +47,98 @@ class Avatar extends StatelessWidget {
final noPic = mxContent == null ||
mxContent.toString().isEmpty ||
mxContent.toString() == 'null';
final textWidget = Center(
final textColor = name?.lightColorAvatar;
final textWidget = Container(
color: textColor,
alignment: Alignment.center,
child: Text(
fallbackLetters,
style: TextStyle(
color: noPic ? Colors.white : null,
fontSize: (size / 2.5).roundToDouble(),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: (size / 3).roundToDouble(),
),
),
);
final borderRadius = BorderRadius.circular(size / 2);
final borderRadius = this.borderRadius ?? BorderRadius.circular(size / 2);
final presenceUserId = this.presenceUserId;
final color =
noPic ? name?.lightColorAvatar : Theme.of(context).secondaryHeaderColor;
final container = Stack(
children: [
ClipRRect(
borderRadius: borderRadius,
child: Container(
width: size,
height: size,
color: color,
SizedBox(
width: size,
height: size,
child: Material(
color: Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
side: border ?? BorderSide.none,
),
clipBehavior: Clip.hardEdge,
child: noPic
? textWidget
: MxcImage(
key: Key(mxContent.toString()),
client: client,
key: ValueKey(mxContent.toString()),
cacheKey: '${mxContent}_$size',
uri: mxContent,
fit: BoxFit.cover,
width: size,
height: size,
placeholder: (_) => textWidget,
cacheKey: mxContent.toString(),
placeholder: (_) => Center(
child: Icon(
Icons.person_2,
color: Theme.of(context).colorScheme.tertiary,
size: size / 1.5,
),
),
),
),
),
PresenceBuilder(
client: client,
userId: presenceUserId,
builder: (context, presence) {
if (presence == null ||
(presence.presence == PresenceType.offline &&
presence.lastActiveTimestamp == null)) {
return const SizedBox.shrink();
}
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
return Positioned(
bottom: -3,
right: -3,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: presenceBackgroundColor ??
Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(32),
),
alignment: Alignment.center,
if (presenceUserId != null)
PresenceBuilder(
client: client,
userId: presenceUserId,
builder: (context, presence) {
if (presence == null ||
(presence.presence == PresenceType.offline &&
presence.lastActiveTimestamp == null)) {
return const SizedBox.shrink();
}
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
return Positioned(
bottom: -3,
right: -3,
child: Container(
width: 10,
height: 10,
width: 16,
height: 16,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.surface,
color: presenceBackgroundColor ??
Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(32),
),
alignment: Alignment.center,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 1,
color: Theme.of(context).colorScheme.surface,
),
),
),
),
),
);
},
),
);
},
),
],
);
if (onTap == null) return container;

View file

@ -1,20 +1,17 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
class MaxWidthBody extends StatelessWidget {
final Widget? child;
final Widget child;
final double maxWidth;
final bool withFrame;
final bool withScrolling;
final EdgeInsets? innerPadding;
const MaxWidthBody({
this.child,
required this.child,
this.maxWidth = 600,
this.withFrame = true,
this.withScrolling = true,
this.innerPadding,
super.key,
@ -24,36 +21,35 @@ class MaxWidthBody extends StatelessWidget {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final paddingVal = max(0, (constraints.maxWidth - maxWidth) / 2);
final hasPadding = paddingVal > 0;
final padding = EdgeInsets.symmetric(
vertical: hasPadding ? 32 : 0,
horizontal: max(0, (constraints.maxWidth - maxWidth) / 2),
);
final childWithPadding = Padding(
padding: padding,
child: withFrame && hasPadding
? Material(
elevation:
Theme.of(context).appBarTheme.scrolledUnderElevation ??
4,
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
shadowColor: Theme.of(context).appBarTheme.shadowColor,
child: child,
)
: child,
);
if (!withScrolling) {
return Padding(
padding: innerPadding ?? EdgeInsets.zero,
child: childWithPadding,
);
}
const desiredWidth = FluffyThemes.columnWidth * 1.5;
final body = constraints.maxWidth <= desiredWidth
? child
: Container(
alignment: Alignment.topCenter,
padding: const EdgeInsets.all(32),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Material(
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
clipBehavior: Clip.hardEdge,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
shadowColor: Theme.of(context).appBarTheme.shadowColor,
child: child,
),
),
);
if (!withScrolling) return body;
return SingleChildScrollView(
padding: innerPadding,
physics: const ScrollPhysics(),
child: childWithPadding,
child: body,
);
},
),

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
class TwoColumnLayout extends StatelessWidget {
final Widget mainView;
final Widget sideView;
@ -20,7 +22,8 @@ class TwoColumnLayout extends StatelessWidget {
Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(),
width: 360.0 + (displayNavigationRail ? 64 : 0),
width: FluffyThemes.columnWidth +
(displayNavigationRail ? FluffyThemes.navRailWidth : 0),
child: mainView,
),
Container(

View file

@ -23,6 +23,7 @@ class MxcImage extends StatefulWidget {
final ThumbnailMethod thumbnailMethod;
final Widget Function(BuildContext context)? placeholder;
final String? cacheKey;
final Client? client;
const MxcImage({
this.uri,
@ -38,6 +39,7 @@ class MxcImage extends StatefulWidget {
this.animationCurve = FluffyThemes.animationCurve,
this.thumbnailMethod = ThumbnailMethod.scale,
this.cacheKey,
this.client,
super.key,
});
@ -48,10 +50,10 @@ class MxcImage extends StatefulWidget {
class _MxcImageState extends State<MxcImage> {
static final Map<String, Uint8List> _imageDataCache = {};
Uint8List? _imageDataNoCache;
Uint8List? get _imageData {
final cacheKey = widget.cacheKey;
return cacheKey == null ? _imageDataNoCache : _imageDataCache[cacheKey];
}
Uint8List? get _imageData => widget.cacheKey == null
? _imageDataNoCache
: _imageDataCache[widget.cacheKey];
set _imageData(Uint8List? data) {
if (data == null) return;
@ -64,7 +66,7 @@ class _MxcImageState extends State<MxcImage> {
bool? _isCached;
Future<void> _load() async {
final client = Matrix.of(context).client;
final client = widget.client ?? Matrix.of(context).client;
final uri = widget.uri;
final event = widget.event;

View file

@ -10,19 +10,18 @@ import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../utils/localized_exception_extension.dart';
class PublicRoomBottomSheet extends StatelessWidget {
final String? roomAlias;
final BuildContext outerContext;
final PublicRoomsChunk? chunk;
final VoidCallback? onRoomJoined;
final List<String>? via;
PublicRoomBottomSheet({
this.roomAlias,
required this.outerContext,
this.chunk,
this.onRoomJoined,
this.via,
super.key,
}) {
assert(roomAlias != null || chunk != null);
@ -39,8 +38,11 @@ class PublicRoomBottomSheet extends StatelessWidget {
return chunk.roomId;
}
final roomId = chunk != null && knock
? await client.knockRoom(chunk.roomId)
: await client.joinRoom(roomAlias ?? chunk!.roomId);
? await client.knockRoom(chunk.roomId, serverName: via)
: await client.joinRoom(
roomAlias ?? chunk!.roomId,
serverName: via,
);
if (!knock && client.getRoomById(roomId) == null) {
await client.waitForRoomInSync(roomId);
@ -52,7 +54,7 @@ class PublicRoomBottomSheet extends StatelessWidget {
return;
}
if (result.error == null) {
Navigator.of(context).pop();
Navigator.of(context).pop<bool>(true);
// don't open the room if the joined room is a space
if (chunk?.roomType != 'm.space' &&
!client.getRoomById(result.result!)!.isSpace) {
@ -64,17 +66,17 @@ class PublicRoomBottomSheet extends StatelessWidget {
bool _testRoom(PublicRoomsChunk r) => r.canonicalAlias == roomAlias;
Future<PublicRoomsChunk> _search(BuildContext context) async {
Future<PublicRoomsChunk> _search() async {
final chunk = this.chunk;
if (chunk != null) return chunk;
final query = await Matrix.of(context).client.queryPublicRooms(
final query = await Matrix.of(outerContext).client.queryPublicRooms(
server: roomAlias!.domain,
filter: PublicRoomQueryFilter(
genericSearchTerm: roomAlias,
),
);
if (!query.chunk.any(_testRoom)) {
throw (L10n.of(context)!.noRoomsFound);
throw (L10n.of(outerContext)!.noRoomsFound);
}
return query.chunk.firstWhere(_testRoom);
}
@ -82,6 +84,7 @@ class PublicRoomBottomSheet extends StatelessWidget {
@override
Widget build(BuildContext context) {
final roomAlias = this.roomAlias ?? chunk?.canonicalAlias;
final roomLink = roomAlias ?? chunk?.roomId;
return SafeArea(
child: Scaffold(
appBar: AppBar(
@ -108,41 +111,84 @@ class PublicRoomBottomSheet extends StatelessWidget {
],
),
body: FutureBuilder<PublicRoomsChunk>(
future: _search(context),
future: _search(),
builder: (context, snapshot) {
final profile = snapshot.data;
return ListView(
padding: EdgeInsets.zero,
children: [
if (profile == null)
Container(
height: 156,
alignment: Alignment.center,
color: Theme.of(context).secondaryHeaderColor,
child: snapshot.hasError
? Text(snapshot.error!.toLocalizedString(context))
: const CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
)
else
Center(
child: Padding(
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Avatar(
mxContent: profile.avatarUrl,
name: profile.name ?? roomAlias,
size: Avatar.defaultSize * 3,
child: profile == null
? const Center(
child: CircularProgressIndicator.adaptive(),
)
: Avatar(
client: Matrix.of(outerContext).client,
mxContent: profile.avatarUrl,
name: profile.name ?? roomAlias,
size: Avatar.defaultSize * 3,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: roomLink != null
? () => FluffyShare.share(
roomLink,
context,
copyOnly: true,
)
: null,
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface,
),
label: Text(
roomLink ?? '...',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
TextButton.icon(
onPressed: () {},
icon: const Icon(
Icons.groups_3_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface,
),
label: Text(
L10n.of(context)!.countParticipants(
profile?.numJoinedMembers ?? 0,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ElevatedButton.icon(
onPressed: () => _joinRoom(context),
label: Text(
chunk?.joinRule == 'knock' &&
Matrix.of(context)
Matrix.of(outerContext)
.client
.getRoomById(chunk!.roomId) ==
null
@ -151,36 +197,10 @@ class PublicRoomBottomSheet extends StatelessWidget {
? L10n.of(context)!.joinSpace
: L10n.of(context)!.joinRoom,
),
icon: const Icon(Icons.login_outlined),
icon: const Icon(Icons.navigate_next),
),
),
const SizedBox(height: 16),
ListTile(
title: Text(
profile?.name ??
roomAlias?.localpart ??
chunk?.roomId.localpart ??
L10n.of(context)!.chat,
),
subtitle: Text(
'${L10n.of(context)!.participant}: ${profile?.numJoinedMembers ?? 0}',
),
trailing: const Icon(Icons.account_box_outlined),
),
if (roomAlias != null)
ListTile(
title: Text(L10n.of(context)!.publicLink),
subtitle: SelectableText(roomAlias),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16.0),
trailing: IconButton(
icon: const Icon(Icons.copy_outlined),
onPressed: () => FluffyShare.share(
roomAlias,
context,
),
),
),
if (profile?.topic?.isNotEmpty ?? false)
ListTile(
subtitle: SelectableLinkify(

View file

@ -19,41 +19,32 @@ class UnreadRoomsBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Matrix.of(context)
.client
.onSync
.stream
.where((syncUpdate) => syncUpdate.hasRoomUpdate),
builder: (context, _) {
final unreadCount = Matrix.of(context)
.client
.rooms
.where(filter)
.where((r) => (r.isUnread || r.membership == Membership.invite))
.length;
return b.Badge(
badgeStyle: b.BadgeStyle(
badgeColor: Theme.of(context).colorScheme.primary,
elevation: 4,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
badgeContent: Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
),
),
showBadge: unreadCount != 0,
badgeAnimation: const b.BadgeAnimation.scale(),
position: badgePosition ?? b.BadgePosition.bottomEnd(),
child: child,
);
},
final unreadCount = Matrix.of(context)
.client
.rooms
.where(filter)
.where((r) => (r.isUnread || r.membership == Membership.invite))
.length;
return b.Badge(
badgeStyle: b.BadgeStyle(
badgeColor: Theme.of(context).colorScheme.primary,
elevation: 4,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.surface,
width: 2,
),
),
badgeContent: Text(
unreadCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
),
),
showBadge: unreadCount != 0,
badgeAnimation: const b.BadgeAnimation.scale(),
position: badgePosition ?? b.BadgePosition.bottomEnd(),
child: child,
);
}
}

View file

@ -12,6 +12,7 @@ permittedLicenses:
- BSD-2-Clause
- BSD-3-Clause
- EUPL-1.2
- LGPL-3.0
- MIT
- MPL-2.0
- Zlib

View file

@ -1210,10 +1210,10 @@ packages:
dependency: "direct main"
description:
name: matrix
sha256: a27c2f73d28ea292e0f67f3d36396fb8acd7cfc97a07901dc7b22f46e082c3d6
sha256: d1955846aaf5a5c6d353a90ce4133b9c99581cd64f4fe9e389e5f8b95157ca3b
url: "https://pub.dev"
source: hosted
version: "0.30.0"
version: "0.31.0"
meta:
dependency: transitive
description:
@ -1278,6 +1278,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
opus_caf_converter_dart:
dependency: "direct main"
description:
name: opus_caf_converter_dart
sha256: e08156066916f790a54df305e103d6dec4d853ec23147e6a02eda3c06f67ba1a
url: "https://pub.dev"
source: hosted
version: "1.0.1"
package_config:
dependency: transitive
description:
@ -2364,13 +2372,13 @@ packages:
source: hosted
version: "1.2.0"
win32:
dependency: transitive
dependency: "direct overridden"
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.1"
version: "5.5.0"
win32_registry:
dependency: transitive
description:

View file

@ -64,8 +64,9 @@ dependencies:
keyboard_shortcuts: ^0.1.4
latlong2: ^0.9.1
linkify: ^5.0.0
matrix: ^0.30.0
matrix: ^0.31.0
native_imaging: ^0.1.1
opus_caf_converter_dart: ^1.0.1
package_info_plus: ^6.0.0
pasteboard: ^0.2.0
path: ^1.9.0
@ -160,3 +161,4 @@ dependency_overrides:
git:
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
ref: null-safety
win32: 5.5.0

View file

@ -1,32 +1,24 @@
diff --git a/android/app/build.gradle b/android/app/build.gradle
index bf972f30..46cebdc6 100644
index 7520ff2a..ae376d9d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -70,6 +70,10 @@
}
release {
signingConfig signingConfigs.release
+ minifyEnabled false
+ shrinkResources false
+
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// https://stackoverflow.com/a/77494454/8222484
@@ -78,8 +82,11 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- //implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
+ implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'androidx.multidex:multidex:2.0.1'
@@ -2,7 +2,7 @@ plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
- //id "com.google.gms.google-services"
+ id "com.google.gms.google-services"
}
-//apply plugin: 'com.google.gms.google-services'
+apply plugin: 'com.google.gms.google-services'
def localProperties = new Properties()
@@ -83,6 +83,6 @@ flutter {
}
dependencies {
- //implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
+ implementation 'com.google.firebase:firebase-messaging:19.0.1' // Workaround for https://github.com/microg/android_packages_apps_GmsCore/issues/313#issuecomment-617651698
implementation 'androidx.multidex:multidex:2.0.1'
}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index d0e0fbc9..0a546da0 100644
--- a/android/app/proguard-rules.pro
@ -93,42 +85,25 @@ index d9930f55..510e9845 100644
}
-*/
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt b/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt
index 1afc4606..894d1571 100644
--- a/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt
+++ b/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt
@@ -7,13 +7,11 @@ import android.content.Context
import androidx.multidex.MultiDex
class MainActivity : FlutterActivity() {
-
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
-
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return provideEngine(this)
}
diff --git a/android/build.gradle b/android/build.gradle
index bd394967..2e9d54de 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -8,7 +8,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- //classpath 'com.google.gms:google-services:4.3.8'
+ classpath 'com.google.gms:google-services:4.3.8'
}
diff --git a/android/settings.gradle b/android/settings.gradle
index b2fd960a..fdb01a4d 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -20,7 +20,7 @@ plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.1.2" apply false
id "org.jetbrains.kotlin.android" version "1.8.0" apply false
- // id "com.google.gms.google-services" version "4.3.8" apply false
+ id "com.google.gms.google-services" version "4.3.8" apply false
}
include ":app"
\ No newline at end of file
diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart
index 8e67ae92..da4da5c3 100644
index 039dde89..1cefdd71 100644
--- a/lib/utils/background_push.dart
+++ b/lib/utils/background_push.dart
@@ -39,7 +39,7 @@ import '../config/setting_keys.dart';
@@ -38,7 +38,7 @@ import '../config/setting_keys.dart';
import '../widgets/matrix.dart';
import 'platform_infos.dart';
@ -137,7 +112,7 @@ index 8e67ae92..da4da5c3 100644
class NoTokenException implements Exception {
String get cause => 'Cannot get firebase token';
@@ -64,7 +64,7 @@ class BackgroundPush {
@@ -63,7 +63,7 @@ class BackgroundPush {
final pendingTests = <String, Completer<void>>{};
@ -147,10 +122,10 @@ index 8e67ae92..da4da5c3 100644
DateTime? lastReceivedPush;
diff --git a/pubspec.yaml b/pubspec.yaml
index 193e6ed6..f70e48d4 100644
index 69c80d6e..efd32d89 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -26,7 +26,7 @@ dependencies:
@@ -25,7 +25,7 @@ dependencies:
emoji_picker_flutter: ^2.1.1
emoji_proposal: ^0.0.1
emojis: ^0.9.9