diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index d13e40248f..16cc35cebe 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -24,6 +24,8 @@ pbkdf pids pkcs + previewable + previewables riotx signin signout @@ -31,6 +33,7 @@ ssss sygnal threepid + unpublish unwedging diff --git a/CHANGES.md b/CHANGES.md index b031e626eb..83c825c21e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,44 @@ +Changes in Element 1.0.12 (2020-12-15) +=================================================== + +Features ✨: + - Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) + - Room setting: update join rules and guest access (#2442) + - Url preview (#481) + - Store encrypted file in cache and cleanup decrypted file at each app start (#2512) + - Emoji Keyboard (#2520) + - Social login (#2452) + - Support for chat effects in timeline (confetti, snow) (#2535) + +Improvements 🙌: + - Add Setting Item to Change PIN (#2462) + - Improve room history visibility setting UX (#1579) + - Matrix.to deeplink custom scheme support + - Homeserver history (#1933) + +Bugfix 🐛: + - Fix cancellation of sending event (#2438) + - Double bottomsheet effect after verify with passphrase + - EditText cursor jumps to the start while typing fast (#2469) + - UTD for events before invitation if member state events are hidden (#2486) + - No known servers error is given when joining rooms on new Gitter bridge (#2516) + - Show preview when sending attachment from the keyboard (#2440) + - Do not compress GIFs (#1616, #1254) + +SDK API changes ⚠️: + - StateService now exposes suspendable function instead of using MatrixCallback. + - RawCacheStrategy has been moved and renamed to CacheStrategy + - FileService: remove useless FileService.DownloadMode + +Build 🧱: + - Upgrade some dependencies and Kotlin version + - Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable) + - Upgrade Realm dependency to 10.1.2 + +Other changes: + - Remove "Status.im" theme #2424 + - Log HTTP requests and responses in production (level BASIC, i.e. without any private data) + Changes in Element 1.0.11 (2020-11-27) =================================================== diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 91ddd519df..59ba6c4500 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -66,7 +66,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation 'com.google.android.material:material:1.2.1' diff --git a/build.gradle b/build.gradle index 0c4b35b060..7531dee61e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ buildscript { // Ref: https://kotlinlang.org/releases.html - ext.kotlin_version = '1.4.10' - ext.kotlin_coroutines_version = "1.3.9" + ext.kotlin_version = '1.4.20' + ext.kotlin_coroutines_version = "1.4.1" repositories { google() jcenter() @@ -12,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.google.gms:google-services:4.3.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' @@ -43,6 +43,10 @@ allprojects { includeGroupByRegex 'com\\.github\\.chrisbanes' // PFLockScreen-Android includeGroupByRegex 'com\\.github\\.vector-im' + + //Chat effects + includeGroupByRegex 'com\\.github\\.jetradarmobile' + includeGroupByRegex 'nl\\.dionsegijn' } } maven { diff --git a/fastlane/metadata/android/en-US/changelogs/40100120.txt b/fastlane/metadata/android/en-US/changelogs/40100120.txt new file mode 100644 index 0000000000..39715c2910 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100120.txt @@ -0,0 +1,2 @@ +Main changes in this version: URL Preview, new Emoji keyboard, new room settings capabilities, and snow for Christmas! +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.12 \ No newline at end of file diff --git a/fastlane/metadata/android/fi/changelogs/40100100.txt b/fastlane/metadata/android/fi/changelogs/40100100.txt new file mode 100644 index 0000000000..0717ff27d7 --- /dev/null +++ b/fastlane/metadata/android/fi/changelogs/40100100.txt @@ -0,0 +1,2 @@ +Tämä versio sisältää virheenkorjauksia ja muita parannuksia. Viestien lähettäminen on nyt paljon nopeampaa. +Täysi muutosloki: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/fi/full_description.txt b/fastlane/metadata/android/fi/full_description.txt new file mode 100644 index 0000000000..70def518da --- /dev/null +++ b/fastlane/metadata/android/fi/full_description.txt @@ -0,0 +1,30 @@ +Element on uudenlainen viestinsovellus, joka: + +1. Antaa sinun päättää yksityisyydestäsi. +2. Antaa sinun kommunikoida kenen tahansa kanssa Matrix-verkossa ja jopa sen ulkopuolella siltaamalla sovelluksiin, kuten Slack +3. Suojaa sinua mainonnalta, tietojen keräämiseltä ja suljetuilta alustoilta +4. Suojaa sinut päästä päähän -salauksella sekä ristiin varmentamisella muiden todentamiseksi + +Element eroaa täysin muista viestintäsovelluksista, koska se on hajautettu ja avointa lähdekoodia. + +Element antaa sinun isännöidä itse - valita isännän - jotta sinulla on yksityisyys ja voit hallita tietojasi sekä keskustelujasi. Se antaa sinulle pääsyn avoimeen verkkoon; joten et ole jumissa Elementin käyttäjissä. + +Element pystyy tekemään kaiken tämän, koska se toimii Matrixilla - avoimella, hajautetun viestinnän standardilla. + +Element antaa sinulle hallinnan antamalla sinun valita, kuka isännöi keskustelujasi. Element-sovelluksessa voit valita isännän eri tavoin: + +1. Hanki ilmainen tili Matrix-kehittäjien ylläpitämällä matrix.org-palvelimella tai valitse tuhansista vapaaehtoisten ylläpitämistä julkisista palvelimista. +2. Isännöi tiliäsi itse suorittamalla palvelinta omalla laitteellasi +3. Luo tili mukautetulla palvelimella yksinkertaisesti tilaamalla Element Matrix Services -palvelu + +Miksi valita Element? + +OMAT TIEDOT: Sinä päätät, missä tietosi ja viestisi säilytetään. Hallitset sitä itse, eikä jokin MEGAYHTIÖ, joka tutkii tietojasi tai antaa niitä kolmansille osapuolille. + +AVOIN KOMMUNIKOINYI JA YHTEISTYÖ: Voit keskustella kaikkien muiden Matrix-verkon käyttäjien kanssa, riippumatta siitä käyttävätkö he Elementiä tai muuta Matrix-sovellusta, ja vaikka he käyttäisivät eri viestijärjestelmiä, kuten Slack, IRC tai XMPP. + +ERITTÄIN TURVALLINEN: Vahva päästä päähän -salaus (vain keskustelussa olevat voivat purkaa viestien salauksen), ja ristiin varmentaminen keskustelun osallistujien laitteiden tarkistamiseksi. + +TÄYDELLISTÄ VIESTINTÄÄ: Viestit, ääni- ja videopuhelut, tiedostojen jakaminen, näytön jakaminen ja koko joukko integraatioita, botteja ja widgettejä. Rakenna huoneita, yhteisöjä, pidä yhteyttä ja tee asioita. + +MISSÄ TAHANSA OLETKIN: Pidä yhteyttä missä tahansa, täysin synkronoidun viestihistorian kautta kaikilla laitteillasi ja verkossa osoitteessa https://app.element.io. diff --git a/fastlane/metadata/android/fi/short_description.txt b/fastlane/metadata/android/fi/short_description.txt index 64f35a7dff..5573ac1cb9 100644 --- a/fastlane/metadata/android/fi/short_description.txt +++ b/fastlane/metadata/android/fi/short_description.txt @@ -1 +1 @@ -Turvallista, hajautettua keskustelua ja VoIP-puheluita. Pidä tietosi turvassa. +Turvallista, hajautettua, keskusteluja ja VoIP-puheluita. Pidä tietosi turvassa. diff --git a/fastlane/metadata/android/it/changelogs/40100100.txt b/fastlane/metadata/android/it/changelogs/40100100.txt index 0c7cc8cc6c..5ca2f86e45 100644 --- a/fastlane/metadata/android/it/changelogs/40100100.txt +++ b/fastlane/metadata/android/it/changelogs/40100100.txt @@ -1 +1,2 @@ -// DA FARE +Questa nuova versione contiene soprattutto correzioni di errori e miglioramenti. L'invio di messaggi ora è molto più veloce. +Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100100.txt b/fastlane/metadata/android/pt_BR/changelogs/40100100.txt index 02cfd45a87..4884d7f62a 100644 --- a/fastlane/metadata/android/pt_BR/changelogs/40100100.txt +++ b/fastlane/metadata/android/pt_BR/changelogs/40100100.txt @@ -1 +1,2 @@ -// A FAZER +Esta nova versão contém principalmente correções de erros e melhorias. Enviar mensagens agora é muito mais rápido. +Registro de todas as alterações: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/sv/changelogs/40100100.txt b/fastlane/metadata/android/sv/changelogs/40100100.txt index 6da756aca9..afdef3583b 100644 --- a/fastlane/metadata/android/sv/changelogs/40100100.txt +++ b/fastlane/metadata/android/sv/changelogs/40100100.txt @@ -1 +1,2 @@ -// ATT GÖRA +Den här nya versionen innehåller mest buggfixar och förbättringar. Det går nu mycket snabbare att skicka meddelanden. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/uk/changelogs/40100100.txt b/fastlane/metadata/android/uk/changelogs/40100100.txt new file mode 100644 index 0000000000..e5333ae561 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40100100.txt @@ -0,0 +1,2 @@ +Ця версія містить переважно виправлення помилок та деякі покращення. Відправлення повідомлень стало тепер ще швидшим. +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 64247581d2..026ae4162a 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -7,7 +7,7 @@ Element — це застосунок для спілкування та спі Element ґрунтовно відрізняється від інших застосунків для спілкування та співпраці тому що він є децентралізованим та відкритоджерельним. -Element дозволяє вам розміщувати сервер в себе або обирати будь-якого з надавачів послуг, таким чином забезпечуючи вам конфіденційність і можливість володіти власними даними й бесідами та контролювати їх. Він надає вам доступ до відкритої мережі, тож ви не є обмеженими спілкуванням виключно з користувачами Element. І він є дуже надійним та безпечним. +Element дозволяє вам розміщувати сервер в себе або обирати будь-якого з надавачів послуг, таким чином забезпечуючи вам конфіденційність і можливість володіти власними даними й бесідами та контролювати їх. Він надає вам доступ до відкритої мережі, тож ви не є обмеженими спілкуванням виключно з користувачами Element. І він є дуже надійним та безпечним. Element здатен забезпечити усе це завдяки тому, що він заснований на протоколі Matrix — стандарті для відкритого та децентралізованого спілкування. diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt index 3c21bcbeb6..0ea092ba9a 100644 --- a/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt @@ -1 +1,2 @@ -// 待辦事項 +這個新版本主要包含錯誤修復與改善。傳送訊息更快了。 +完整的變更紀錄請見:https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/gradle.properties b/gradle.properties index b3f11e08a3..200866be25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m org.gradle.vfs.watch=true vector.debugPrivateData=false -vector.httpLogLevel=NONE +vector.httpLogLevel=BASIC # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above #vector.debugPrivateData=true diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 3d62758065..a99b5856ba 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -36,9 +36,10 @@ android { dependencies { implementation project(":matrix-sdk-android") implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version" + // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.2" diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt index f6dbe3d160..ec30a31f6d 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxCallbackBuilders.kt @@ -21,34 +21,36 @@ import org.matrix.android.sdk.api.util.Cancelable import io.reactivex.Completable import io.reactivex.Single -fun singleBuilder(builder: (callback: MatrixCallback) -> Cancelable): Single = Single.create { - val callback: MatrixCallback = object : MatrixCallback { +fun singleBuilder(builder: (MatrixCallback) -> Cancelable): Single = Single.create { emitter -> + val callback = object : MatrixCallback { override fun onSuccess(data: T) { - it.onSuccess(data) + // Add `!!` to fix the warning: + // "Type mismatch: type parameter with nullable bounds is used T is used where T was expected. This warning will become an error soon" + emitter.onSuccess(data!!) } override fun onFailure(failure: Throwable) { - it.tryOnError(failure) + emitter.tryOnError(failure) } } val cancelable = builder(callback) - it.setCancellable { + emitter.setCancellable { cancelable.cancel() } } -fun completableBuilder(builder: (callback: MatrixCallback) -> Cancelable): Completable = Completable.create { - val callback: MatrixCallback = object : MatrixCallback { +fun completableBuilder(builder: (MatrixCallback) -> Cancelable): Completable = Completable.create { emitter -> + val callback = object : MatrixCallback { override fun onSuccess(data: T) { - it.onComplete() + emitter.onComplete() } override fun onFailure(failure: Throwable) { - it.tryOnError(failure) + emitter.tryOnError(failure) } } val cancelable = builder(callback) - it.setCancellable { + emitter.setCancellable { cancelable.cancel() } } diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 86f2d26808..b938f60e39 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -17,14 +17,20 @@ package org.matrix.android.sdk.rx import android.net.Uri +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import kotlinx.coroutines.rx2.rxCompletable import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState @@ -32,9 +38,6 @@ import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.Single class RxRoom(private val room: Room) { @@ -119,32 +122,28 @@ class RxRoom(private val room: Room) { room.invite3pid(threePid, it) } - fun updateTopic(topic: String): Completable = completableBuilder { - room.updateTopic(topic, it) + fun updateTopic(topic: String): Completable = rxCompletable { + room.updateTopic(topic) } - fun updateName(name: String): Completable = completableBuilder { - room.updateName(name, it) + fun updateName(name: String): Completable = rxCompletable { + room.updateName(name) } - fun addRoomAlias(alias: String): Completable = completableBuilder { - room.addRoomAlias(alias, it) + fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable { + room.updateHistoryReadability(readability) } - fun updateCanonicalAlias(alias: String): Completable = completableBuilder { - room.updateCanonicalAlias(alias, it) + fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable { + room.updateJoinRule(joinRules, guestAccess) } - fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder { - room.updateHistoryReadability(readability, it) + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable { + room.updateAvatar(avatarUri, fileName) } - fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { - room.updateAvatar(avatarUri, fileName, it) - } - - fun deleteAvatar(): Completable = completableBuilder { - room.deleteAvatar(it) + fun deleteAvatar(): Completable = rxCompletable { + room.deleteAvatar() } } diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 0e5b88adb2..a7b269fcc6 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription class RxSession(private val session: Session) { @@ -139,7 +140,7 @@ class RxSession(private val session: Session) { } fun getRoomIdByAlias(roomAlias: String, - searchOnServer: Boolean): Single> = singleBuilder { + searchOnServer: Boolean): Single> = singleBuilder { session.getRoomIdByAlias(roomAlias, searchOnServer, it) } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 29c709844a..519b8439c9 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.0.0" + classpath "io.realm:realm-gradle-plugin:10.1.2" } } @@ -63,7 +63,7 @@ android { release { buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" - buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC" } } @@ -125,7 +125,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.appcompat:appcompat:1.2.0" - implementation "androidx.fragment:fragment:1.3.0-beta01" implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" @@ -146,7 +145,7 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" // Image - implementation 'androidx.exifinterface:exifinterface:1.3.0' + implementation 'androidx.exifinterface:exifinterface:1.3.1' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.7.1' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index 0d71af864b..03943cea14 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -25,6 +25,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.common.DaggerTestMatrixComponent @@ -49,6 +50,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager + @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService private val uiHandler = Handler(Looper.getMainLooper()) @@ -71,6 +73,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun rawService() = rawService + fun homeServerHistoryService() = homeServerHistoryService + fun legacySessionImporter(): LegacySessionImporter { return legacySessionImporter } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index e42059c639..da5e90abdd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.crypto.encryption import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.Room @@ -57,13 +57,14 @@ class EncryptionTest : InstrumentedTest { @Test fun test_EncryptionStateEvent() { performTest(roomShouldBeEncrypted = true) { room -> - // Send an encryption Event as a State Event - room.sendStateEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = null, - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent(), - callback = NoOpMatrixCallback() - ) + runBlocking { + // Send an encryption Event as a State Event + room.sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = null, + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) + } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 606f57b467..eb8b8b9730 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest { assertNotNull(decryption) // - Check decryptKeyBackupData() returns stg val sessionData = keysBackup - .decryptKeyBackupData(keyBackupData!!, + .decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 944d1036d3..b6e5ae7364 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -111,7 +111,7 @@ class KeysBackupTestHelper( Assert.assertTrue(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version) } /** diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt new file mode 100644 index 0000000000..9ee84fdfc6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType + +@RunWith(AndroidJUnit4::class) +internal class UrlsExtractorTest : InstrumentedTest { + + private val urlsExtractor = UrlsExtractor() + + @Test + fun wrongEventTypeTest() { + createEvent(body = "https://matrix.org") + .copy(type = EventType.STATE_ROOM_GUEST_ACCESS) + .let { urlsExtractor.extract(it) } + .size shouldBeEqualTo 0 + } + + @Test + fun oneUrlTest() { + createEvent(body = "https://matrix.org") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org" + } + } + + @Test + fun withoutProtocolTest() { + createEvent(body = "www.matrix.org") + .let { urlsExtractor.extract(it) } + .size shouldBeEqualTo 0 + } + + @Test + fun oneUrlWithParamTest() { + createEvent(body = "https://matrix.org?foo=bar") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org?foo=bar" + } + } + + @Test + fun oneUrlWithParamsTest() { + createEvent(body = "https://matrix.org?foo=bar&bar=foo") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org?foo=bar&bar=foo" + } + } + + @Test + fun oneUrlInlinedTest() { + createEvent(body = "Hello https://matrix.org, how are you?") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 1 + result[0] shouldBeEqualTo "https://matrix.org" + } + } + + @Test + fun twoUrlsTest() { + createEvent(body = "https://matrix.org https://example.org") + .let { urlsExtractor.extract(it) } + .let { result -> + result.size shouldBeEqualTo 2 + result[0] shouldBeEqualTo "https://matrix.org" + result[1] shouldBeEqualTo "https://example.org" + } + } + + private fun createEvent(body: String): Event = Event( + type = EventType.MESSAGE, + content = MessageTextContent( + msgType = MessageType.MSGTYPE_TEXT, + body = body + ).toContent() + ) +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 630f6f1e29..34ed28d467 100644 --- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.network.interceptors import androidx.annotation.NonNull -import org.matrix.android.sdk.BuildConfig import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONArray import org.json.JSONException @@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { */ @Synchronized override fun log(@NonNull message: String) { - // In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG - if (BuildConfig.DEBUG) { - Timber.v(message) + Timber.v(message) - if (message.startsWith("{")) { - // JSON Detected - try { - val o = JSONObject(message) - logJson(o.toString(INDENT_SPACE)) - } catch (e: JSONException) { - // Finally this is not a JSON string... - Timber.e(e) - } - } else if (message.startsWith("[")) { - // JSON Array detected - try { - val o = JSONArray(message) - logJson(o.toString(INDENT_SPACE)) - } catch (e: JSONException) { - // Finally not JSON... - Timber.e(e) - } + if (message.startsWith("{")) { + // JSON Detected + try { + val o = JSONObject(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally this is not a JSON string... + Timber.e(e) + } + } else if (message.startsWith("[")) { + // JSON Array detected + try { + val o = JSONArray(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally not JSON... + Timber.e(e) } - // Else not a json string to log } + // Else not a json string to log } private fun logJson(formattedJson: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index cf6f37cec8..a5d457222f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.SessionManager @@ -47,6 +48,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager + @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService init { Monarchy.init(context) @@ -65,6 +67,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun rawService() = rawService + fun homeServerHistoryService() = homeServerHistoryService + fun legacySessionImporter(): LegacySessionImporter { return legacySessionImporter } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt index 6cacf55a38..871c2559f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.auth /** * Path to use when the client does not supported any or all login flows * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback - * */ + */ const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt new file mode 100644 index 0000000000..77e33b8934 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth + +/** + * A simple service to remember homeservers you already connected to. + */ +interface HomeServerHistoryService { + + fun getKnownServersUrls(): List + + fun addHomeServerToHistory(url: String) + + fun clearHistory() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 64d3ddcca5..f1f9ba3916 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data sealed class LoginFlowResult { data class Success( val supportedLoginTypes: List, + val ssoIdentityProviders: List?, val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt new file mode 100644 index 0000000000..d89607843f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.auth.data + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class SsoIdentityProvider( + /** + * The id field would be opaque with the accepted characters matching unreserved URI characters as defined in RFC3986 + * - this was chosen to avoid having to encode special characters in the URL. Max length 128. + */ + @Json(name = "id") val id: String, + /** + * The name field should be the human readable string intended for printing by the client. + */ + @Json(name = "name") val name: String?, + /** + * The icon field is the only optional field and should point to an icon representing the IdP. + * If present then it must be an HTTPS URL to an image resource. + * This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily. + */ + @Json(name = "icon") val iconUrl: String? +) : Parcelable { + + companion object { + // Not really defined by the spec, but we may define some ids here + const val ID_GOOGLE = "google" + const val ID_GITHUB = "github" + const val ID_APPLE = "apple" + const val ID_FACEBOOK = "facebook" + const val ID_TWITTER = "twitter" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt similarity index 83% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt index f4eada559e..2880d851d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.raw +package org.matrix.android.sdk.api.cache -sealed class RawCacheStrategy { +sealed class CacheStrategy { // Data is always fetched from the server - object NoCache : RawCacheStrategy() + object NoCache : CacheStrategy() // Once data is retrieved, it is stored for the provided amount of time. // In case of error, and if strict is set to false, the cache can be returned if available - data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : RawCacheStrategy() + data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy() // Once retrieved, the data is stored in cache and will be always get from the cache - object InfiniteCache : RawCacheStrategy() + object InfiniteCache : CacheStrategy() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt index 19549a338e..f1722b2189 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.raw +import org.matrix.android.sdk.api.cache.CacheStrategy + /** * Useful methods to fetch raw data from the server. The access token will not be used to fetched the data */ @@ -23,7 +25,7 @@ interface RawService { /** * Get a URL, either from cache or from the remote server, depending on the cache strategy */ - suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String + suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String /** * Specific case for the well-known file. Cache validity is 8 hours diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 56609610f1..8a95baf3cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -181,6 +182,11 @@ interface Session : */ fun widgetService(): WidgetService + /** + * Returns the media service associated with the session + */ + fun mediaService(): MediaService + /** * Returns the integration manager service associated with the session */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt index e55546e12d..757a09fb3f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -36,7 +36,7 @@ sealed class CallState { * Connected. Incoming/Outgoing call, ice layer connecting or connected * Notice that the PeerState failed is not always final, if you switch network, new ice candidtates * could be exchanged, and the connection could go back to connected - * */ + */ data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState() /** Terminated. Incoming/Outgoing call, the call is terminated */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt index 4677c2be32..4164b84ecd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt @@ -21,6 +21,7 @@ import android.os.Parcelable import androidx.exifinterface.media.ExifInterface import com.squareup.moshi.JsonClass import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType @Parcelize @JsonClass(generateAdapter = true) @@ -45,5 +46,5 @@ data class ContentAttachmentData( VIDEO } - fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType + fun getSafeMimeType() = mimeType?.normalizeMimeType() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 0a7f3ff09f..68874a1fb1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -49,6 +49,12 @@ object EventType { const val STATE_ROOM_JOIN_RULES = "m.room.join_rules" const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" + + /** + * Note that this Event has been deprecated, see + * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events + * - https://github.com/matrix-org/matrix-doc/pull/2432 + */ const val STATE_ROOM_ALIASES = "m.room.aliases" const val STATE_ROOM_TOMBSTONE = "m.room.tombstone" const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index 31f016be14..bcdb5ea257 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file import android.net.Uri import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent +import org.matrix.android.sdk.api.session.room.model.message.getFileName +import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import java.io.File /** @@ -27,23 +31,6 @@ import java.io.File */ interface FileService { - enum class DownloadMode { - /** - * Download file in external storage - */ - TO_EXPORT, - - /** - * Download file in cache - */ - FOR_INTERNAL_USE, - - /** - * Download file in file provider path - */ - FOR_EXTERNAL_SHARE - } - enum class FileState { IN_CACHE, DOWNLOADING, @@ -54,34 +41,79 @@ interface FileService { * Download a file. * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. */ - fun downloadFile( - downloadMode: DownloadMode, - id: String, - fileName: String, - mimeType: String?, - url: String?, - elementToDecrypt: ElementToDecrypt?, - callback: MatrixCallback): Cancelable + fun downloadFile(fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback): Cancelable - fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean + fun downloadFile(messageContent: MessageWithAttachmentContent, + callback: MatrixCallback): Cancelable = + downloadFile( + fileName = messageContent.getFileName(), + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = callback + ) + + fun isFileInCache(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt? + ): Boolean + + fun isFileInCache(messageContent: MessageWithAttachmentContent) = + isFileInCache( + mxcUrl = messageContent.getFileUrl(), + fileName = messageContent.getFileName(), + mimeType = messageContent.mimeType, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()) /** * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION * (if not other app won't be able to access it) */ - fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? + fun getTemporarySharableURI(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): Uri? + + fun getTemporarySharableURI(messageContent: MessageWithAttachmentContent): Uri? = + getTemporarySharableURI( + mxcUrl = messageContent.getFileUrl(), + fileName = messageContent.getFileName(), + mimeType = messageContent.mimeType, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() + ) /** * Get information on the given file. * Mimetype should be the same one as passed to downloadFile (limitation for now) */ - fun fileState(mxcUrl: String, mimeType: String?): FileState + fun fileState(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): FileState + + fun fileState(messageContent: MessageWithAttachmentContent): FileState = + fileState( + mxcUrl = messageContent.getFileUrl(), + fileName = messageContent.getFileName(), + mimeType = messageContent.mimeType, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt() + ) /** - * Clears all the files downloaded by the service + * Clears all the files downloaded by the service, including decrypted files */ fun clearCache() + /** + * Clears all the decrypted files by the service + */ + fun clearDecryptedCache() + /** * Get size of cached files */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt index e27d81edb7..60af93888e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/integrationmanager/IntegrationManagerService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.integrationmanager -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This is the entry point to manage integration. You can grab an instance of this service through an active session. */ @@ -80,19 +77,17 @@ interface IntegrationManagerService { /** * Offers to enable or disable the integration. * @param enable the param to change - * @param callback the matrix callback to listen for result. * @return Cancelable */ - fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable + suspend fun setIntegrationEnabled(enable: Boolean) /** * Offers to allow or disallow a widget. * @param stateEventId the eventId of the state event defining the widget. * @param allowed the param to change - * @param callback the matrix callback to listen for result. * @return Cancelable */ - fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable + suspend fun setWidgetAllowed(stateEventId: String, allowed: Boolean) /** * Returns true if the widget is allowed, false otherwise. @@ -105,7 +100,7 @@ interface IntegrationManagerService { * @param widgetType the widget type to check for * @param domain the domain to check for */ - fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable + suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) /** * Returns true if the widget domain is allowed, false otherwise. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt new file mode 100644 index 0000000000..9040ec7d5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.media + +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.util.JsonDict + +interface MediaService { + /** + * Extract URLs from an Event. + * @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data + */ + fun extractUrls(event: Event): List + + /** + * Get Raw Url Preview data from the homeserver. There is no cache management for this request + * @param url The url to get the preview data from + * @param timestamp The optional timestamp + */ + suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict + + /** + * Get Url Preview data from the homeserver, or from cache, depending on the cache strategy + * @param url The url to get the preview data from + * @param timestamp The optional timestamp. Note that this parameter is not taken into account + * if the data is already in cache and the cache strategy allow to use it + * @param cacheStrategy the cache strategy, see the type for more details + */ + suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData + + /** + * Clear the cache of all retrieved UrlPreview data + */ + suspend fun clearCache() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt new file mode 100644 index 0000000000..33fc8b052b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.media + +/** + * Facility data class to get the common field of a PreviewUrl response form the server + * + * Example of return data for the url `https://matrix.org`: + *
+ * {
+ *     "matrix:image:size": 112805,
+ *     "og:description": "Matrix is an open standard for interoperable, decentralised, real-time communication",
+ *     "og:image": "mxc://matrix.org/2020-12-03_uFqjagCCTJbaaJxb",
+ *     "og:image:alt": "Matrix is an open standard for interoperable, decentralised, real-time communication",
+ *     "og:image:height": 467,
+ *     "og:image:type": "image/jpeg",
+ *     "og:image:width": 911,
+ *     "og:locale": "en_US",
+ *     "og:site_name": "Matrix.org",
+ *     "og:title": "Matrix.org",
+ *     "og:type": "website",
+ *     "og:url": "https://matrix.org"
+ * }
+ * 
+ */ +data class PreviewUrlData( + // Value of field "og:url". If not provided, this is the value passed in parameter + val url: String, + // Value of field "og:site_name" + val siteName: String?, + // Value of field "og:title" + val title: String?, + // Value of field "og:description" + val description: String?, + // Value of field "og:image" + val mxcUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index ac1d726d03..aefc086b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -25,6 +25,7 @@ interface PermalinkService { companion object { const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" + const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 837bda031b..cb6690b5c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService import org.matrix.android.sdk.api.session.room.members.MembershipService @@ -46,6 +47,7 @@ interface Room : DraftService, ReadService, TypingService, + AliasService, TagsService, MembershipService, StateService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt index dc5b3d55f5..61970ce848 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol @@ -39,4 +40,14 @@ interface RoomDirectoryService { * Includes both the available protocols and all fields required for queries against each protocol. */ fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable + + /** + * Get the visibility of a room in the directory + */ + suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility + + /** + * Set the visibility of a room in the directory + */ + suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index f30037e5c2..5f02b77a1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription /** * This interface defines methods to get rooms. It's implemented at the session level. @@ -120,7 +123,12 @@ interface RoomService { */ fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, - callback: MatrixCallback>): Cancelable + callback: MatrixCallback>): Cancelable + + /** + * Delete a room alias + */ + suspend fun deleteRoomAlias(roomAlias: String) /** * Return a live data of all local changes membership that happened since the session has been opened. @@ -158,4 +166,16 @@ interface RoomService { * @return a LiveData of the optional found room member */ fun getRoomMemberLive(userId: String, roomId: String): LiveData> + + /** + * Get some state events about a room + */ + fun getRoomState(roomId: String, callback: MatrixCallback>) + + /** + * Use this if you want to get information from a room that you are not yet in (or invited) + * It might be possible to get some information on this room if it is public or if guest access is allowed + * This call will try to gather some information on this room, but it could fail and get nothing more + */ + fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/AliasService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/AliasService.kt new file mode 100644 index 0000000000..5fe7e99425 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/AliasService.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.alias + +interface AliasService { + /** + * Get list of local alias of the room + * @return the list of the aliases (full aliases, not only the local part) + */ + suspend fun getRoomAliases(): List + + /** + * Add local alias to the room + * @param aliasLocalPart the local part of the alias. + * Ex: for the alias "#my_alias:example.org", the local part is "my_alias" + */ + suspend fun addAlias(aliasLocalPart: String) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt new file mode 100644 index 0000000000..d2cb7c58a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.alias + +sealed class RoomAliasError : Throwable() { + object AliasEmpty : RoomAliasError() + object AliasNotAvailable : RoomAliasError() + object AliasInvalid : RoomAliasError() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt index b4e2dc645c..208cdd4556 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -18,13 +18,10 @@ package org.matrix.android.sdk.api.session.room.failure import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError sealed class CreateRoomFailure : Failure.FeatureFailure() { object CreatedWithTimeout : CreateRoomFailure() data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure() - sealed class RoomAliasError : CreateRoomFailure() { - object AliasEmpty : RoomAliasError() - object AliasNotAvailable : RoomAliasError() - object AliasInvalid : RoomAliasError() - } + data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt index f70e013786..59989f3045 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomAliasesContent.kt @@ -21,6 +21,9 @@ import com.squareup.moshi.JsonClass /** * Class representing the EventType.STATE_ROOM_ALIASES state event content + * Note that this Event has been deprecated, see + * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events + * - https://github.com/matrix-org/matrix-doc/pull/2432 */ @JsonClass(generateAdapter = true) data class RoomAliasesContent( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt index 5487b2ff82..4e8bd2e71b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomCanonicalAliasContent.kt @@ -24,5 +24,14 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomCanonicalAliasContent( - @Json(name = "alias") val canonicalAlias: String? = null + /** + * The canonical alias for the room. If not present, null, or empty the room should be considered to have no canonical alias. + */ + @Json(name = "alias") val canonicalAlias: String? = null, + + /** + * Alternative aliases the room advertises. + * This list can have aliases despite the alias field being null, empty, or otherwise not present. + */ + @Json(name = "alt_aliases") val alternativeAliases: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt index 859f7fd104..73e27b64e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) @@ -54,5 +55,5 @@ data class MessageImageContent( @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null ) : MessageImageInfoContent { override val mimeType: String? - get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*" + get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: MimeTypes.Images } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index 0f133323b0..a2b4e135d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -33,4 +33,7 @@ object MessageType { // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" + + const val MSGTYPE_CONFETTI = "nic.custom.confetti" + const val MSGTYPE_SNOW = "nic.custom.snow" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt new file mode 100644 index 0000000000..db70dadef3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.peeking + +sealed class PeekResult { + data class Success( + val roomId: String, + val alias: String?, + val name: String?, + val topic: String?, + val avatarUrl: String?, + val numJoinedMembers: Int?, + val viaServers: List + ) : PeekResult() + + data class PeekingNotAllowed( + val roomId: String, + val alias: String?, + val viaServers: List + ) : PeekResult() + + object UnknownAlias : PeekResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index e4baa58c30..444366e912 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -18,11 +18,11 @@ package org.matrix.android.sdk.api.session.room.state import android.net.Uri import androidx.lifecycle.LiveData -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -31,39 +31,41 @@ interface StateService { /** * Update the topic of the room */ - fun updateTopic(topic: String, callback: MatrixCallback): Cancelable + suspend fun updateTopic(topic: String) /** * Update the name of the room */ - fun updateName(name: String, callback: MatrixCallback): Cancelable - - /** - * Add new alias to the room. - */ - fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable + suspend fun updateName(name: String) /** * Update the canonical alias of the room + * @param alias the canonical alias, or null to reset the canonical alias of this room + * @param altAliases the alternative aliases for this room. It should include the canonical alias if any. */ - fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable + suspend fun updateCanonicalAlias(alias: String?, altAliases: List) /** * Update the history readability of the room */ - fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable + suspend fun updateHistoryReadability(readability: RoomHistoryVisibility) + + /** + * Update the join rule and/or the guest access + */ + suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) /** * Update the avatar of the room */ - fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable + suspend fun updateAvatar(avatarUri: Uri, fileName: String) /** * Delete the avatar of the room */ - fun deleteAvatar(callback: MatrixCallback): Cancelable + suspend fun deleteAvatar() - fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable + suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict) fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/EventTypeFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/EventTypeFilter.kt new file mode 100644 index 0000000000..18faa6a452 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/EventTypeFilter.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.timeline + +data class EventTypeFilter( + /** + * Allowed event type. + */ + val eventType: String, + /** + * Allowed state key. Set null if you want to allow all events, + * otherwise allowed events will be filtered according to the given stateKey. + */ + val stateKey: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt index c751632286..4415c8e4b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt @@ -36,5 +36,5 @@ data class TimelineEventFilters( /** * If [filterTypes] is true, the list of types allowed by the list. */ - val allowedTypes: List = emptyList() + val allowedTypes: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt index c3cc1eb9ee..e2462d007d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/uploads/UploadsService.kt @@ -16,9 +16,6 @@ package org.matrix.android.sdk.api.session.room.uploads -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - /** * This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level. */ @@ -29,7 +26,5 @@ interface UploadsService { * @param numberOfEvents the expected number of events to retrieve. The result can contain less events. * @param since token to get next page, or null to get the first page */ - fun getUploads(numberOfEvents: Int, - since: String?, - callback: MatrixCallback): Cancelable + suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt index 2d88125662..10ce0829d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt @@ -16,22 +16,16 @@ package org.matrix.android.sdk.api.session.terms -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - interface TermsService { enum class ServiceType { IntegrationManager, IdentityService } - fun getTerms(serviceType: ServiceType, - baseUrl: String, - callback: MatrixCallback): Cancelable + suspend fun getTerms(serviceType: ServiceType, baseUrl: String): GetTermsResponse - fun agreeToTerms(serviceType: ServiceType, - baseUrl: String, - agreedUrls: List, - token: String?, - callback: MatrixCallback): Cancelable + suspend fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt new file mode 100644 index 0000000000..c74999b4ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +import org.matrix.android.sdk.api.extensions.orFalse + +// The Android SDK does not provide constant for mime type, add some of them here +object MimeTypes { + const val Any: String = "*/*" + const val OctetStream = "application/octet-stream" + + const val Images = "image/*" + + const val Png = "image/png" + const val BadJpg = "image/jpg" + const val Jpeg = "image/jpeg" + const val Gif = "image/gif" + + fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this + + fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse() + fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse() + fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt index c6d610188e..2ec8900f7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.di.AuthDatabase import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter import org.matrix.android.sdk.internal.wellknown.WellknownModule import io.realm.RealmConfiguration +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import java.io.File @Module(includes = [WellknownModule::class]) @@ -80,4 +81,7 @@ internal abstract class AuthModule { @Binds abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask + + @Binds + abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 3d5a0efcd4..55f053de8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.wellknown.WellknownResult @@ -278,6 +279,7 @@ internal class DefaultAuthenticationService @Inject constructor( } return LoginFlowResult.Success( loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, + loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl, !versions.isSupportedBySdk() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt new file mode 100644 index 0000000000..7415938ebc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth + +import com.zhuinden.monarchy.Monarchy +import io.realm.kotlin.where +import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity +import org.matrix.android.sdk.internal.di.GlobalDatabase +import javax.inject.Inject + +class DefaultHomeServerHistoryService @Inject constructor( + @GlobalDatabase private val monarchy: Monarchy +) : HomeServerHistoryService { + + override fun getKnownServersUrls(): List { + return monarchy.fetchAllMappedSync( + { realm -> + realm.where() + }, + { it.url } + ) + } + + override fun addHomeServerToHistory(url: String) { + monarchy.writeAsync { realm -> + KnownServerUrlEntity(url).let { + realm.insertOrUpdate(it) + } + } + } + + override fun clearHistory() { + monarchy.runTransactionSync { it.where().findAll().deleteAllFromRealm() } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 8acdee3608..c333b3524e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider @JsonClass(generateAdapter = true) internal data class LoginFlowResponse( @@ -34,5 +35,13 @@ internal data class LoginFlow( * The login type. This is supplied as the type when logging in. */ @Json(name = "type") - val type: String? + val type: String?, + + /** + * Augments m.login.sso flow discovery definition to include metadata on the supported IDPs + * the client can show a button for each of the supported providers + * See MSC #2858 + */ + @Json(name = "identity_providers") + val ssoIdentityProvider: List? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt index 5b105c4d40..3461a4d738 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt @@ -51,6 +51,18 @@ data class RegistrationFlowResponse( * The information that the client will need to know in order to use a given type of authentication. * For each login stage type presented, that type may be present as a key in this dictionary. * For example, the public key of reCAPTCHA stage could be given here. + * other example + * "params": { + * "m.login.sso": { + * "identity_providers": [ + * { + * "id": "google", + * "name": "Google", + * "icon": "https://..." + * } + * ] + * } + * } */ @Json(name = "params") val params: JsonDict? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 22a190c68e..c7885ce449 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.extensions.toUnsignedInt import org.matrix.olm.OlmSAS import org.matrix.olm.OlmUtility import timber.log.Timber +import java.util.Locale /** * Represents an ongoing short code interactive key verification between two devices. @@ -344,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction( } protected fun hashUsingAgreedHashMethod(toHash: String): String? { - if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { + if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) { val olmUtil = OlmUtility() val hashBytes = olmUtil.sha256(toHash) olmUtil.releaseUtility() @@ -354,12 +355,11 @@ internal abstract class SASDefaultVerificationTransaction( } private fun macUsingAgreedMethod(message: String, info: String): String? { - if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { - return getSAS().calculateMacLongKdf(message, info) - } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { - return getSAS().calculateMac(message, info) + return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) { + SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info) + SAS_MAC_SHA256 -> getSAS().calculateMac(message, info) + else -> null } - return null } override fun getDecimalCodeRepresentation(): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 973388da49..b970ec60e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -20,6 +20,7 @@ import io.realm.DynamicRealm import io.realm.RealmMigration import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -27,7 +28,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 5L + const val SESSION_STORE_SCHEMA_VERSION = 6L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -38,6 +39,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.removeField("adminE2EByDefault") ?.removeField("preferredJitsiDomain") } + + private fun migrateTo6(realm: DynamicRealm) { + Timber.d("Step 5 -> 6") + realm.schema.create("PreviewUrlCacheEntity") + .addField(PreviewUrlCacheEntityFields.URL, String::class.java) + .setRequired(PreviewUrlCacheEntityFields.URL, true) + .addPrimaryKey(PreviewUrlCacheEntityFields.URL) + .addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java) + .addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java) + .addField(PreviewUrlCacheEntityFields.TITLE, String::class.java) + .addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java) + .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java) + .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt new file mode 100644 index 0000000000..1ebdc22ab4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class KnownServerUrlEntity( + @PrimaryKey + var url: String = "" +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt new file mode 100644 index 0000000000..b1e0b64405 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class PreviewUrlCacheEntity( + @PrimaryKey + var url: String = "", + + var urlFromServer: String? = null, + var siteName: String? = null, + var title: String? = null, + var description: String? = null, + var mxcUrl: String? = null, + + var lastUpdatedTimestamp: Long = 0L +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index f62312f8fc..bca2c42c9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule PushRulesEntity::class, PushRuleEntity::class, PushConditionEntity::class, + PreviewUrlCacheEntity::class, PusherEntity::class, PusherDataEntity::class, ReadReceiptsSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt new file mode 100644 index 0000000000..a139c17439 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields + +/** + * Get the current PreviewUrlCacheEntity, return null if it does not exist + */ +internal fun PreviewUrlCacheEntity.Companion.get(realm: Realm, url: String): PreviewUrlCacheEntity? { + return realm.where() + .equalTo(PreviewUrlCacheEntityFields.URL, url) + .findFirst() +} + +/** + * Get the current PreviewUrlCacheEntity, create one if it does not exist + */ +internal fun PreviewUrlCacheEntity.Companion.getOrCreate(realm: Realm, url: String): PreviewUrlCacheEntity { + return get(realm, url) ?: realm.createObject(url) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index 41a13c785d..148232cf94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -71,8 +71,23 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, } internal fun RealmQuery.filterEvents(filters: TimelineEventFilters): RealmQuery { - if (filters.filterTypes) { - `in`(TimelineEventEntityFields.ROOT.TYPE, filters.allowedTypes.toTypedArray()) + if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) { + beginGroup() + filters.allowedTypes.forEachIndexed { index, filter -> + if (filter.stateKey == null) { + equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) + } else { + beginGroup() + equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType) + and() + equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey) + endGroup() + } + if (index != filters.allowedTypes.size - 1) { + or() + } + } + endGroup() } if (filters.filterUseless) { not() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index d3f08fde36..9d6fa29bb2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -25,6 +25,7 @@ import okhttp3.OkHttpClient import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.AuthModule @@ -62,6 +63,8 @@ internal interface MatrixComponent { fun rawService(): RawService + fun homeServerHistoryService(): HomeServerHistoryService + fun context(): Context fun matrixConfiguration(): MatrixConfiguration @@ -71,9 +74,6 @@ internal interface MatrixComponent { @CacheDirectory fun cacheDir(): File - @ExternalFilesDirectory - fun externalFilesDir(): File? - fun olmManager(): OlmManager fun taskExecutor(): TaskExecutor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt index 71cbd8f1a1..b58fb3e683 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt @@ -57,13 +57,6 @@ internal object MatrixModule { return context.cacheDir } - @JvmStatic - @Provides - @ExternalFilesDirectory - fun providesExternalFilesDir(context: Context): File? { - return context.getExternalFilesDir(null) - } - @JvmStatic @Provides @MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index e6cec7f7ac..2535a5347a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -16,14 +16,15 @@ package org.matrix.android.sdk.internal.network -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.internal.network.ssl.CertUtil import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.shouldBeRetried +import org.matrix.android.sdk.internal.network.ssl.CertUtil import retrofit2.Call import retrofit2.awaitResponse +import timber.log.Timber import java.io.IOException internal suspend inline fun executeRequest(eventBus: EventBus?, @@ -49,6 +50,9 @@ internal class Request(private val eventBus: EventBus?) { throw response.toFailure(eventBus) } } catch (exception: Throwable) { + // Log some details about the request which has failed + Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}") + // Check if this is a certificateException CertUtil.getCertificateException(exception) // TODO Support certificate error once logged diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt index 3b0d7546e5..42b826de16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.internal.raw -import org.matrix.android.sdk.api.raw.RawCacheStrategy +import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.raw.RawService import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor( private val getUrlTask: GetUrlTask, private val cleanRawCacheTask: CleanRawCacheTask ) : RawService { - override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String { - return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy)) + override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String { + return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy)) } override suspend fun getWellknown(userId: String): String { val homeServerDomain = userId.substringAfter(":") return getUrl( "https://$homeServerDomain/.well-known/matrix/client", - RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) + CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt index 1f4ca6d627..16633d90ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw import com.zhuinden.monarchy.Monarchy import okhttp3.ResponseBody -import org.matrix.android.sdk.api.raw.RawCacheStrategy +import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.internal.database.model.RawCacheEntity import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -32,7 +32,7 @@ import javax.inject.Inject internal interface GetUrlTask : Task { data class Params( val url: String, - val rawCacheStrategy: RawCacheStrategy + val cacheStrategy: CacheStrategy ) } @@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor( ) : GetUrlTask { override suspend fun execute(params: GetUrlTask.Params): String { - return when (params.rawCacheStrategy) { - RawCacheStrategy.NoCache -> doRequest(params.url) - is RawCacheStrategy.TtlCache -> doRequestWithCache( + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url) + is CacheStrategy.TtlCache -> doRequestWithCache( params.url, - params.rawCacheStrategy.validityDurationInMillis, - params.rawCacheStrategy.strict + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict ) - RawCacheStrategy.InfiniteCache -> doRequestWithCache( + CacheStrategy.InfiniteCache -> doRequestWithCache( params.url, Long.MAX_VALUE, true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt new file mode 100644 index 0000000000..49bcc72181 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.raw + +import io.realm.DynamicRealm +import io.realm.RealmMigration +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields +import timber.log.Timber + +internal object GlobalRealmMigration : RealmMigration { + + // Current schema version + const val SCHEMA_VERSION = 1L + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + realm.schema.create("KnownServerUrlEntity") + .addField(KnownServerUrlEntityFields.URL, String::class.java) + .addPrimaryKey(KnownServerUrlEntityFields.URL) + .setRequired(KnownServerUrlEntityFields.URL, true) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt index e4e4160193..770a49c904 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.raw import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity import org.matrix.android.sdk.internal.database.model.RawCacheEntity /** @@ -24,6 +25,7 @@ import org.matrix.android.sdk.internal.database.model.RawCacheEntity */ @RealmModule(library = true, classes = [ - RawCacheEntity::class + RawCacheEntity::class, + KnownServerUrlEntity::class ]) internal class GlobalRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt index aee2a52818..50721b809a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt @@ -57,6 +57,9 @@ internal abstract class RawModule { realmKeysUtils.configureEncryption(this, DB_ALIAS) } .name("matrix-sdk-global.realm") + .schemaVersion(GlobalRealmMigration.SCHEMA_VERSION) + .migration(GlobalRealmMigration) + .allowWritesOnUiThread(true) .modules(GlobalRealmModule()) .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 861ae7c7ee..07cde3da60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -21,6 +21,10 @@ import android.net.Uri import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import arrow.core.Try +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver @@ -29,35 +33,21 @@ import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments -import org.matrix.android.sdk.internal.di.CacheDirectory -import org.matrix.android.sdk.internal.di.ExternalFilesDirectory import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import org.matrix.android.sdk.internal.util.md5 import org.matrix.android.sdk.internal.util.toCancelable import org.matrix.android.sdk.internal.util.writeToFile -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import okio.buffer -import okio.sink -import okio.source import timber.log.Timber import java.io.File import java.io.IOException -import java.io.InputStream -import java.net.URLEncoder import javax.inject.Inject internal class DefaultFileService @Inject constructor( private val context: Context, - @CacheDirectory - private val cacheDirectory: File, - @ExternalFilesDirectory - private val externalFilesDirectory: File?, @SessionDownloadsDirectory private val sessionCacheDirectory: File, private val contentUrlResolver: ContentUrlResolver, @@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor( private val taskExecutor: TaskExecutor ) : FileService { - private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) + // Legacy folder, will be deleted + private val legacyFolder = File(sessionCacheDirectory, "MF") + // Folder to store downloaded files (not decrypted) + private val downloadFolder = File(sessionCacheDirectory, "F") + // Folder to store decrypted files + private val decryptedFolder = File(downloadFolder, "D") - private val downloadFolder = File(sessionCacheDirectory, "MF") + init { + // Clear the legacy downloaded files + legacyFolder.deleteRecursively() + } /** * Retain ongoing downloads to avoid re-downloading and already downloading file @@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor( * Download file in the cache folder, and eventually decrypt it * TODO looks like files are copied 3 times */ - override fun downloadFile(downloadMode: FileService.DownloadMode, - id: String, - fileName: String, + override fun downloadFile(fileName: String, mimeType: String?, url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable { - val unwrappedUrl = url ?: return NoOpCancellable.also { + url ?: return NoOpCancellable.also { callback.onFailure(IllegalArgumentException("url is null")) } - Timber.v("## FileService downloadFile $unwrappedUrl") + Timber.v("## FileService downloadFile $url") synchronized(ongoing) { - val existing = ongoing[unwrappedUrl] + val existing = ongoing[url] if (existing != null) { Timber.v("## FileService downloadFile is already downloading.. ") existing.add(callback) return NoOpCancellable } else { // mark as tracked - ongoing[unwrappedUrl] = ArrayList() + ongoing[url] = ArrayList() // and proceed to download } } @@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor( return taskExecutor.executorScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { - if (!downloadFolder.exists()) { - downloadFolder.mkdirs() + if (!decryptedFolder.exists()) { + decryptedFolder.mkdirs() } // ensure we use unique file name by using URL (mapped to suitable file name) // Also we need to add extension for the FileProvider, if not it lot's of app that it's // shared with will not function well (even if mime type is passed in the intent) - File(downloadFolder, fileForUrl(unwrappedUrl, mimeType)) - }.flatMap { destFile -> - if (!destFile.exists()) { + getFiles(url, fileName, mimeType, elementToDecrypt != null) + }.flatMap { cachedFiles -> + if (!cachedFiles.file.exists()) { val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) val request = Request.Builder() @@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor( Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") - if (elementToDecrypt != null) { - Timber.v("## FileService: decrypt file") - val decryptSuccess = destFile.outputStream().buffered().use { - MXEncryptedAttachments.decryptAttachment( - source.inputStream(), - elementToDecrypt, - it - ) - } - response.close() - if (!decryptSuccess) { - return@flatMap Try.Failure(IllegalStateException("Decryption error")) - } - } else { - writeToFile(source.inputStream(), destFile) - response.close() - } + // Write the file to cache (encrypted version if the file is encrypted) + writeToFile(source.inputStream(), cachedFiles.file) + response.close() } else { Timber.v("## FileService: cache hit for $url") } - Try.just(copyFile(destFile, downloadMode)) + Try.just(cachedFiles) } - }.fold({ - callback.onFailure(it) - // notify concurrent requests - val toNotify = synchronized(ongoing) { - ongoing[unwrappedUrl]?.also { - ongoing.remove(unwrappedUrl) + }.flatMap { cachedFiles -> + // Decrypt if necessary + if (cachedFiles.decryptedFile != null) { + if (!cachedFiles.decryptedFile.exists()) { + Timber.v("## FileService: decrypt file") + // Ensure the parent folder exists + cachedFiles.decryptedFile.parentFile?.mkdirs() + val decryptSuccess = cachedFiles.file.inputStream().use { inputStream -> + cachedFiles.decryptedFile.outputStream().buffered().use { outputStream -> + MXEncryptedAttachments.decryptAttachment( + inputStream, + elementToDecrypt, + outputStream + ) + } + } + if (!decryptSuccess) { + return@flatMap Try.Failure(IllegalStateException("Decryption error")) + } + } else { + Timber.v("## FileService: cache hit for decrypted file") } + Try.just(cachedFiles.decryptedFile) + } else { + // Clear file + Try.just(cachedFiles.file) } - toNotify?.forEach { otherCallbacks -> - tryOrNull { otherCallbacks.onFailure(it) } - } - }, { file -> - callback.onSuccess(file) - // notify concurrent requests - val toNotify = synchronized(ongoing) { - ongoing[unwrappedUrl]?.also { - ongoing.remove(unwrappedUrl) + }.fold( + { throwable -> + callback.onFailure(throwable) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[url]?.also { + ongoing.remove(url) + } + } + toNotify?.forEach { otherCallbacks -> + tryOrNull { otherCallbacks.onFailure(throwable) } + } + }, + { file -> + callback.onSuccess(file) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[url]?.also { + ongoing.remove(url) + } + } + Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") + toNotify?.forEach { otherCallbacks -> + tryOrNull { otherCallbacks.onSuccess(file) } + } } - } - Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") - toNotify?.forEach { otherCallbacks -> - tryOrNull { otherCallbacks.onSuccess(file) } - } - }) + ) }.toCancelable() } - fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { - val file = File(downloadFolder, fileForUrl(url, mimeType)) - val source = inputStream.source().buffer() - file.sink().buffer().let { sink -> - source.use { input -> - sink.use { output -> - output.writeAll(input) + fun storeDataFor(mxcUrl: String, + filename: String?, + mimeType: String?, + originalFile: File, + encryptedFile: File?) { + val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null) + if (encryptedFile != null) { + // We switch the two files here, original file it the decrypted file + files.decryptedFile?.let { originalFile.copyTo(it) } + encryptedFile.copyTo(files.file) + } else { + // Just copy the original file + originalFile.copyTo(files.file) + } + } + + private fun safeFileName(fileName: String?, mimeType: String?): String { + return buildString { + // filename has to be safe for the Android System + val result = fileName + ?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_") + ?.takeIf { it.isNotEmpty() } + ?: DEFAULT_FILENAME + append(result) + // Check that the extension is correct regarding the mimeType + val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } + if (extensionFromMime != null) { + // Compare + val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "") + if (fileExtension.isEmpty() || fileExtension != extensionFromMime) { + // Missing extension, or diff in extension, add the one provided by the mimetype + append(".") + append(extensionFromMime) } } } } - private fun fileForUrl(url: String, mimeType: String?): String { - val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } - return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() + override fun isFileInCache(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): Boolean { + return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE } - override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { - return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() + internal data class CachedFiles( + // This is the downloaded file. Can be clear or encrypted + val file: File, + // This is the decrypted file. Null if the original file is not encrypted + val decryptedFile: File? + ) { + fun getClearFile(): File = decryptedFile ?: file } - override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState { - if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE + private fun getFiles(mxcUrl: String, + fileName: String?, + mimeType: String?, + isEncrypted: Boolean): CachedFiles { + val hashFolder = mxcUrl.md5() + val safeFileName = safeFileName(fileName, mimeType) + return if (isEncrypted) { + // Encrypted file + CachedFiles( + File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"), + File(decryptedFolder, "$hashFolder/$safeFileName") + ) + } else { + // Clear file + CachedFiles( + File(downloadFolder, "$hashFolder/$safeFileName"), + null + ) + } + } + + override fun fileState(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): FileService.FileState { + mxcUrl ?: return FileService.FileState.UNKNOWN + if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE val isDownloading = synchronized(ongoing) { ongoing[mxcUrl] != null } @@ -224,26 +294,18 @@ internal class DefaultFileService @Inject constructor( * Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION * (if not other app won't be able to access it) */ - override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? { + override fun getTemporarySharableURI(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): Uri? { + mxcUrl ?: return null // this string could be extracted no? val authority = "${context.packageName}.mx-sdk.fileprovider" - val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType)) + val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile() if (!targetFile.exists()) return null return FileProvider.getUriForFile(context, authority, targetFile) } - private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File { - // TODO some of this seems outdated, will need to be re-worked - return when (downloadMode) { - FileService.DownloadMode.TO_EXPORT -> - file.copyTo(File(externalFilesDirectory, file.name), true) - FileService.DownloadMode.FOR_EXTERNAL_SHARE -> - file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true) - FileService.DownloadMode.FOR_INTERNAL_USE -> - file - } - } - override fun getCacheSize(): Int { return downloadFolder.walkTopDown() .onEnter { @@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor( override fun clearCache() { downloadFolder.deleteRecursively() } + + override fun clearDecryptedCache() { + decryptedFolder.deleteRecursively() + } + + companion object { + private const val ENCRYPTED_FILENAME = "encrypted.bin" + // The extension would be added from the mimetype + private const val DEFAULT_FILENAME = "file" + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 25345e953c..c5f3f65a34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService +import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor( private val permalinkService: Lazy, private val secureStorageService: Lazy, private val profileService: Lazy, + private val mediaService: Lazy, private val widgetService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, @@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor( override fun widgetService(): WidgetService = widgetService.get() + override fun mediaService(): MediaService = mediaService.get() + override fun integrationManagerService() = integrationManagerService override fun callSignalingService(): CallSignalingService = callSignalingService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index e6fd5a7a0c..659fcc8f5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.group.GroupModule import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule import org.matrix.android.sdk.internal.session.identity.IdentityModule import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule +import org.matrix.android.sdk.internal.session.media.MediaModule import org.matrix.android.sdk.internal.session.openid.OpenIdModule import org.matrix.android.sdk.internal.session.profile.ProfileModule import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker @@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers GroupModule::class, ContentModule::class, CacheModule::class, + MediaModule::class, CryptoModule::class, PushersModule::class, OpenIdModule::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 32949d60c4..96b44917bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.di.Authenticated +import org.matrix.android.sdk.internal.di.CacheDirectory import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory @@ -169,9 +170,9 @@ internal abstract class SessionModule { @JvmStatic @Provides @SessionDownloadsDirectory - fun providesCacheDir(@SessionId sessionId: String, - context: Context): File { - return File(context.cacheDir, "downloads/$sessionId") + fun providesDownloadsCacheDir(@SessionId sessionId: String, + @CacheDirectory cacheFile: File): File { + return File(cacheFile, "downloads/$sessionId") } @JvmStatic diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt index b5de26b39d..1ebe5b2eb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt @@ -20,6 +20,9 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class ContentUploadResponse( +internal data class ContentUploadResponse( + /** + * Required. The MXC URI to the uploaded content. + */ @Json(name = "content_uri") val contentUri: String ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index 8c3aad6a1f..4b31db59b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes import timber.log.Timber import java.io.ByteArrayOutputStream @@ -58,7 +59,7 @@ internal object ThumbnailExtractor { height = thumbnailHeight, size = thumbnailSize.toLong(), bytes = outputStream.toByteArray(), - mimeType = "image/jpeg" + mimeType = MimeTypes.Jpeg ) thumbnail.recycle() outputStream.reset() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 4a30d6c1e6..672d407d25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -151,7 +152,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter params.attachment.size ) - if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { + if (attachment.type == ContentAttachmentData.Type.IMAGE + // Do not compress gif + && attachment.mimeType != MimeTypes.Gif + && params.compressBeforeSending) { fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) .also { compressedFile -> // Get new Bitmap size @@ -174,14 +178,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } + val encryptedFile: File? val contentUploadResponse = if (params.isEncrypted) { Timber.v("## FileService: Encrypt file") - val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) .also { filesToDelete.add(it) } uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -190,18 +195,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.v("## FileService: Uploading file") fileUploader - .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) + .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener) } else { Timber.v("## FileService: Clear file") + encryptedFile = null fileUploader .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) } Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") try { - context.contentResolver.openInputStream(attachment.queryUri)?.let { - fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) - } + fileService.storeDataFor( + mxcUrl = contentUploadResponse.contentUri, + filename = params.attachment.name, + mimeType = params.attachment.getSafeMimeType(), + originalFile = workingFile, + encryptedFile = encryptedFile + ) Timber.v("## FileService: cache storage updated") } catch (failure: Throwable) { Timber.e(failure, "## FileService: Failed to update file cache") @@ -252,7 +262,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${params.attachment.name}", - "application/octet-stream", + MimeTypes.OctetStream, thumbnailProgressListener) UploadThumbnailResult( contentUploadResponse.contentUri, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt new file mode 100644 index 0000000000..6a50f3ee37 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.directory + +import org.matrix.android.sdk.internal.network.NetworkConstants +import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasBody +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path + +internal interface DirectoryAPI { + /** + * Get the room ID associated to the room alias. + * + * @param roomAlias the room alias. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call + + /** + * Get the room directory visibility. + * + * @param roomId the room id. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}") + fun getRoomDirectoryVisibility(@Path("roomId") roomId: String): Call + + /** + * Set the room directory visibility. + * + * @param roomId the room id. + * @param body the body containing the new directory visibility + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/list/room/{roomId}") + fun setRoomDirectoryVisibility(@Path("roomId") roomId: String, + @Body body: RoomDirectoryVisibilityJson): Call + + /** + * Add alias to the room. + * @param roomAlias the room alias. + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun addRoomAlias(@Path("roomAlias") roomAlias: String, + @Body body: AddRoomAliasBody): Call + + /** + * Delete a room alias + * @param roomAlias the room alias. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun deleteRoomAlias(@Path("roomAlias") roomAlias: String): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/RoomDirectoryVisibilityJson.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/RoomDirectoryVisibilityJson.kt new file mode 100644 index 0000000000..ddf927a3dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/RoomDirectoryVisibilityJson.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.directory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility + +@JsonClass(generateAdapter = true) +internal data class RoomDirectoryVisibilityJson( + /** + * The visibility of the room in the directory. One of: ["private", "public"] + */ + @Json(name = "visibility") val visibility: RoomDirectoryVisibility +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt index 39b6608de3..8242edac84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt @@ -22,19 +22,12 @@ import retrofit2.Call import retrofit2.http.GET internal interface CapabilitiesAPI { - /** * Request the homeserver capabilities */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") fun getCapabilities(): Call - /** - * Request the upload capabilities - */ - @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") - fun getUploadCapabilities(): Call - /** * Request the versions */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt similarity index 89% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 8d289dfda5..f3686b02d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor +import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult +import org.matrix.android.sdk.internal.session.media.MediaAPI import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.wellknown.GetWellknownTask @@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val capabilitiesAPI: CapabilitiesAPI, + private val mediaAPI: MediaAPI, @SessionDatabase private val monarchy: Monarchy, private val eventBus: EventBus, private val getWellknownTask: GetWellknownTask, @@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } }.getOrNull() - val uploadCapabilities = runCatching { - executeRequest(eventBus) { - apiCall = capabilitiesAPI.getUploadCapabilities() + val mediaConfig = runCatching { + executeRequest(eventBus) { + apiCall = mediaAPI.getMediaConfig() } }.getOrNull() @@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) }.getOrNull() - insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) + insertInDb(capabilities, mediaConfig, versions, wellknownResult) } private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, - getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getMediaConfigResult: GetMediaConfigResult?, getVersionResult: Versions?, getWellknownResult: WellknownResult?) { monarchy.awaitTransaction { realm -> @@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() } - if (getUploadCapabilitiesResult != null) { - homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + if (getMediaConfigResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt index 753e865b4a..8bf6437009 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/DefaultIntegrationManagerService.kt @@ -16,10 +16,8 @@ package org.matrix.android.sdk.internal.session.integrationmanager -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService -import org.matrix.android.sdk.api.util.Cancelable import javax.inject.Inject internal class DefaultIntegrationManagerService @Inject constructor(private val integrationManager: IntegrationManager) : IntegrationManagerService { @@ -44,20 +42,20 @@ internal class DefaultIntegrationManagerService @Inject constructor(private val return integrationManager.isIntegrationEnabled() } - override fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { - return integrationManager.setIntegrationEnabled(enable, callback) + override suspend fun setIntegrationEnabled(enable: Boolean) { + integrationManager.setIntegrationEnabled(enable) } - override fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { - return integrationManager.setWidgetAllowed(stateEventId, allowed, callback) + override suspend fun setWidgetAllowed(stateEventId: String, allowed: Boolean) { + integrationManager.setWidgetAllowed(stateEventId, allowed) } override fun isWidgetAllowed(stateEventId: String): Boolean { return integrationManager.isWidgetAllowed(stateEventId) } - override fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { - return integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed, callback) + override suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) { + integrationManager.setNativeWidgetDomainAllowed(widgetType, domain, allowed) } override fun isNativeWidgetDomainAllowed(widgetType: String, domain: String): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt index df4e407415..ebd57ce657 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -20,15 +20,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.extensions.observeNotNull @@ -41,7 +38,6 @@ import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccoun import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith import timber.log.Timber import javax.inject.Inject @@ -137,22 +133,17 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri return integrationProvisioningContent?.enabled ?: false } - fun setIntegrationEnabled(enable: Boolean, callback: MatrixCallback): Cancelable { + suspend fun setIntegrationEnabled(enable: Boolean) { val isIntegrationEnabled = isIntegrationEnabled() if (enable == isIntegrationEnabled) { - callback.onSuccess(Unit) - return NoOpCancellable + return } val integrationProvisioningContent = IntegrationProvisioningContent(enabled = enable) val params = UpdateUserAccountDataTask.IntegrationProvisioning(integrationProvisioningContent = integrationProvisioningContent) - return updateUserAccountDataTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return updateUserAccountDataTask.execute(params) } - fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + suspend fun setWidgetAllowed(stateEventId: String, allowed: Boolean) { val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() val newContent = if (currentContent == null) { @@ -165,11 +156,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri currentContent.copy(widgets = allowedWidgets) } val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) - return updateUserAccountDataTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return updateUserAccountDataTask.execute(params) } fun isWidgetAllowed(stateEventId: String): Boolean { @@ -178,7 +165,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri return currentContent?.widgets?.get(stateEventId) ?: false } - fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback): Cancelable { + suspend fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean) { val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountDataTypes.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel() val newContent = if (currentContent == null) { @@ -195,11 +182,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri currentContent.copy(native = nativeAllowedWidgets) } val params = UpdateUserAccountDataTask.AllowedWidgets(allowedWidgetsContent = newContent) - return updateUserAccountDataTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + return updateUserAccountDataTask.execute(params) } fun isNativeWidgetDomainAllowed(widgetType: String, domain: String?): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt new file mode 100644 index 0000000000..004b622c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface ClearPreviewUrlCacheTask : Task + +internal class DefaultClearPreviewUrlCacheTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : ClearPreviewUrlCacheTask { + + override suspend fun execute(params: Unit) { + monarchy.awaitTransaction { realm -> + realm.where() + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt new file mode 100644 index 0000000000..1a400ccfcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import androidx.collection.LruCache +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.util.getOrPut +import javax.inject.Inject + +internal class DefaultMediaService @Inject constructor( + private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask, + private val getPreviewUrlTask: GetPreviewUrlTask, + private val getRawPreviewUrlTask: GetRawPreviewUrlTask, + private val urlsExtractor: UrlsExtractor +) : MediaService { + // Cache of extracted URLs + private val extractedUrlsCache = LruCache>(1_000) + + override fun extractUrls(event: Event): List { + return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) } + } + + private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" + + override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { + return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) + } + + override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData { + return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy)) + } + + override suspend fun clearCache() { + extractedUrlsCache.evictAll() + clearPreviewUrlCacheTask.execute(Unit) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt index 92903bf96e..fece6c06c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.homeserver +package org.matrix.android.sdk.internal.session.media import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class GetUploadCapabilitiesResult( +internal data class GetMediaConfigResult( /** * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. * If not listed or null, the size limit should be treated as unknown. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt new file mode 100644 index 0000000000..69cdfa8faa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.getOrCreate +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import java.util.Date +import javax.inject.Inject + +internal interface GetPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long?, + val cacheStrategy: CacheStrategy + ) +} + +internal class DefaultGetPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus, + @SessionDatabase private val monarchy: Monarchy +) : GetPreviewUrlTask { + + override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData { + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url, params.timestamp) + is CacheStrategy.TtlCache -> doRequestWithCache( + params.url, + params.timestamp, + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict + ) + CacheStrategy.InfiniteCache -> doRequestWithCache( + params.url, + params.timestamp, + Long.MAX_VALUE, + true + ) + } + } + + private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(url, timestamp) + } + .toPreviewUrlData(url) + } + + private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData { + return PreviewUrlData( + url = (get("og:url") as? String) ?: url, + siteName = get("og:site_name") as? String, + title = get("og:title") as? String, + description = get("og:description") as? String, + mxcUrl = get("og:image") as? String + ) + } + + private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData { + // Get data from cache + var dataFromCache: PreviewUrlData? = null + var isCacheValid = false + monarchy.doWithRealm { realm -> + val entity = PreviewUrlCacheEntity.get(realm, url) + dataFromCache = entity?.toDomain() + isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis + } + + val finalDataFromCache = dataFromCache + if (finalDataFromCache != null && isCacheValid) { + return finalDataFromCache + } + + // No cache or outdated cache + val data = try { + doRequest(url, timestamp) + } catch (throwable: Throwable) { + // In case of error, we can return value from cache even if outdated + return finalDataFromCache + ?.takeIf { !strict } + ?: throw throwable + } + + // Store cache + monarchy.awaitTransaction { realm -> + val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url) + previewUrlCacheEntity.urlFromServer = data.url + previewUrlCacheEntity.siteName = data.siteName + previewUrlCacheEntity.title = data.title + previewUrlCacheEntity.description = data.description + previewUrlCacheEntity.mxcUrl = data.mxcUrl + + previewUrlCacheEntity.lastUpdatedTimestamp = Date().time + } + + return data + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt new file mode 100644 index 0000000000..6c5dad2422 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetRawPreviewUrlTask : Task { + data class Params( + val url: String, + val timestamp: Long? + ) +} + +internal class DefaultGetRawPreviewUrlTask @Inject constructor( + private val mediaAPI: MediaAPI, + private val eventBus: EventBus +) : GetRawPreviewUrlTask { + + override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict { + return executeRequest(eventBus) { + apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt new file mode 100644 index 0000000000..bbb4f1e06a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +internal interface MediaAPI { + /** + * Retrieve the configuration of the content repository + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") + fun getMediaConfig(): Call + + /** + * Get information about a URL for the client. Typically this is called when a client + * sees a URL in a message and wants to render a preview for the user. + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-url + * @param url Required. The URL to get a preview of. + * @param ts The preferred point in time to return a preview for. The server may return a newer version + * if it does not have the requested version available. + */ + @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url") + fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt new file mode 100644 index 0000000000..bc58b3f444 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.media.MediaService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class MediaModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesMediaAPI(retrofit: Retrofit): MediaAPI { + return retrofit.create(MediaAPI::class.java) + } + } + + @Binds + abstract fun bindMediaService(service: DefaultMediaService): MediaService + + @Binds + abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask + + @Binds + abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask + + @Binds + abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt new file mode 100644 index 0000000000..dd1a9ead26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import org.matrix.android.sdk.api.session.media.PreviewUrlData +import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity + +/** + * PreviewUrlCacheEntity -> PreviewUrlData + */ +internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData( + url = urlFromServer ?: url, + siteName = siteName, + title = title, + description = description, + mxcUrl = mxcUrl +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt new file mode 100644 index 0000000000..9d374c3428 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.media + +import android.util.Patterns +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import javax.inject.Inject + +internal class UrlsExtractor @Inject constructor() { + // Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later + private val urlRegex = Patterns.WEB_URL.toRegex() + + fun extract(event: Event): List { + return event.takeIf { it.getClearType() == EventType.MESSAGE } + ?.getClearContent() + ?.toModel() + ?.takeIf { it.msgType == MessageType.MSGTYPE_TEXT || it.msgType == MessageType.MSGTYPE_EMOTE } + ?.body + ?.let { urlRegex.findAll(it) } + ?.map { it.value } + ?.filter { it.startsWith("https://") || it.startsWith("http://") } + ?.distinct() + ?.toList() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 5265e4f17d..500d43408e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.UserThreePidEntity @@ -80,7 +81,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback): Cancelable { return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) { - val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg") + val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg) setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) userStore.updateAvatar(userId, response.contentUri) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index c7bb640f7c..7a819250cf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -58,6 +59,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val roomCallService: RoomCallService, private val readService: ReadService, private val typingService: TypingService, + private val aliasService: AliasService, private val tagsService: TagsService, private val cryptoService: CryptoService, private val relationService: RelationService, @@ -76,6 +78,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, RoomCallService by roomCallService, ReadService by readService, TypingService by typingService, + AliasService by aliasService, TagsService by tagsService, RelationService by relationService, MembershipService by roomMembersService, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt index a091b5f85e..0d41c6f35e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -18,19 +18,25 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import javax.inject.Inject -internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask, - private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, - private val taskExecutor: TaskExecutor) : RoomDirectoryService { +internal class DefaultRoomDirectoryService @Inject constructor( + private val getPublicRoomTask: GetPublicRoomTask, + private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, + private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, + private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask, + private val taskExecutor: TaskExecutor) : RoomDirectoryService { override fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, @@ -49,4 +55,12 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu } .executeBy(taskExecutor) } + + override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility { + return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId)) + } + + override suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) { + setRoomDirectoryVisibilityTask.execute(SetRoomDirectoryVisibilityTask.Params(roomId, roomDirectoryVisibility)) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 28656463c1..383dd876d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams @@ -27,17 +28,22 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask @@ -53,6 +59,9 @@ internal class DefaultRoomService @Inject constructor( private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val roomIdByAliasTask: GetRoomIdByAliasTask, + private val deleteRoomAliasTask: DeleteRoomAliasTask, + private val resolveRoomStateTask: ResolveRoomStateTask, + private val peekRoomTask: PeekRoomTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @@ -117,7 +126,7 @@ internal class DefaultRoomService @Inject constructor( .executeBy(taskExecutor) } - override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { + override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { return roomIdByAliasTask .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { this.callback = callback @@ -125,6 +134,10 @@ internal class DefaultRoomService @Inject constructor( .executeBy(taskExecutor) } + override suspend fun deleteRoomAlias(roomAlias: String) { + deleteRoomAliasTask.execute(DeleteRoomAliasTask.Params(roomAlias)) + } + override fun getChangeMembershipsLive(): LiveData> { return roomChangeMembershipStateDataSource.getLiveStates() } @@ -148,4 +161,20 @@ internal class DefaultRoomService @Inject constructor( results.firstOrNull().toOptional() } } + + override fun getRoomState(roomId: String, callback: MatrixCallback>) { + resolveRoomStateTask + .configureWith(ResolveRoomStateTask.Params(roomId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) { + peekRoomTask + .configureWith(PeekRoomTask.Params(roomIdOrAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index fc80842f73..aa92c1cb3b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -23,8 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsRe import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants -import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasBody -import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse import org.matrix.android.sdk.internal.session.room.create.CreateRoomBody import org.matrix.android.sdk.internal.session.room.create.CreateRoomResponse import org.matrix.android.sdk.internal.session.room.create.JoinRoomResponse @@ -184,7 +183,7 @@ internal interface RoomAPI { @Body body: ThreePidInviteBody): Call /** - * Send a generic state events + * Send a generic state event * * @param roomId the room id. * @param stateEventType the state event type @@ -196,7 +195,7 @@ internal interface RoomAPI { @Body params: JsonDict): Call /** - * Send a generic state events + * Send a generic state event * * @param roomId the room id. * @param stateEventType the state event type @@ -209,6 +208,13 @@ internal interface RoomAPI { @Path("state_key") stateKey: String, @Body params: JsonDict): Call + /** + * Get state events of a room + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") + fun getRoomState(@Path("roomId") roomId: String) : Call> + /** * Send a relation event to a room. * @@ -321,20 +327,11 @@ internal interface RoomAPI { @Body body: ReportContentBody): Call /** - * Get the room ID associated to the room alias. - * - * @param roomAlias the room alias. + * Get a list of aliases maintained by the local server for the given room. + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-aliases */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") - fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call - - /** - * Add alias to the room. - * @param roomAlias the room alias. - */ - @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") - fun addRoomAlias(@Path("roomAlias") roomAlias: String, - @Body body: AddRoomAliasBody): Call + @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases") + fun getAliases(@Path("roomId") roomId: String): Call /** * Inform that the user is starting to type or has stopped typing diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index d4fa040d06..63370a1ad8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService @@ -54,6 +55,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: private val roomCallServiceFactory: DefaultRoomCallService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val typingServiceFactory: DefaultTypingService.Factory, + private val aliasServiceFactory: DefaultAliasService.Factory, private val tagsServiceFactory: DefaultTagsService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory, @@ -76,6 +78,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService: roomCallService = roomCallServiceFactory.create(roomId), readService = readServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId), + aliasService = aliasServiceFactory.create(roomId), tagsService = tagsServiceFactory.create(roomId), cryptoService = cryptoService, relationService = relationServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 6381796ee0..92f4ea2aea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -26,16 +26,25 @@ import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.internal.session.DefaultFileService import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DefaultAddRoomAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultDeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAliasesTask +import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.GetRoomLocalAliasesTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask @@ -48,6 +57,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLe import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask @@ -90,6 +103,13 @@ internal abstract class RoomModule { return retrofit.create(RoomAPI::class.java) } + @Provides + @JvmStatic + @SessionScope + fun providesDirectoryAPI(retrofit: Retrofit): DirectoryAPI { + return retrofit.create(DirectoryAPI::class.java) + } + @Provides @JvmStatic fun providesParser(): Parser { @@ -127,6 +147,12 @@ internal abstract class RoomModule { @Binds abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask + @Binds + abstract fun bindGetRoomDirectoryVisibilityTask(task: DefaultGetRoomDirectoryVisibilityTask): GetRoomDirectoryVisibilityTask + + @Binds + abstract fun bindSetRoomDirectoryVisibilityTask(task: DefaultSetRoomDirectoryVisibilityTask): SetRoomDirectoryVisibilityTask + @Binds abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask @@ -181,9 +207,15 @@ internal abstract class RoomModule { @Binds abstract fun bindGetRoomIdByAliasTask(task: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask + @Binds + abstract fun bindGetRoomLocalAliasesTask(task: DefaultGetRoomLocalAliasesTask): GetRoomLocalAliasesTask + @Binds abstract fun bindAddRoomAliasTask(task: DefaultAddRoomAliasTask): AddRoomAliasTask + @Binds + abstract fun bindDeleteRoomAliasTask(task: DefaultDeleteRoomAliasTask): DeleteRoomAliasTask + @Binds abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask @@ -195,4 +227,10 @@ internal abstract class RoomModule { @Binds abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask + + @Binds + abstract fun bindResolveRoomStateTask(task: DefaultResolveRoomStateTask): ResolveRoomStateTask + + @Binds + abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt index 695be3f633..9793750fa0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/AddRoomAliasTask.kt @@ -16,28 +16,38 @@ package org.matrix.android.sdk.internal.session.room.alias -import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.task.Task import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasAvailabilityChecker.Companion.toFullLocalAlias +import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface AddRoomAliasTask : Task { data class Params( val roomId: String, - val roomAlias: String + /** + * the local part of the alias. + * Ex: for the alias "#my_alias:example.org", the local part is "my_alias" + */ + val aliasLocalPart: String ) } internal class DefaultAddRoomAliasTask @Inject constructor( - private val roomAPI: RoomAPI, + @UserId private val userId: String, + private val directoryAPI: DirectoryAPI, + private val aliasAvailabilityChecker: RoomAliasAvailabilityChecker, private val eventBus: EventBus ) : AddRoomAliasTask { override suspend fun execute(params: AddRoomAliasTask.Params) { + aliasAvailabilityChecker.check(params.aliasLocalPart) + executeRequest(eventBus) { - apiCall = roomAPI.addRoomAlias( - roomAlias = params.roomAlias, + apiCall = directoryAPI.addRoomAlias( + roomAlias = params.aliasLocalPart.toFullLocalAlias(userId), body = AddRoomAliasBody( roomId = params.roomId ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt new file mode 100644 index 0000000000..b6c69224e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import org.matrix.android.sdk.api.session.room.alias.AliasService + +internal class DefaultAliasService @AssistedInject constructor( + @Assisted private val roomId: String, + private val getRoomLocalAliasesTask: GetRoomLocalAliasesTask, + private val addRoomAliasTask: AddRoomAliasTask +) : AliasService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): AliasService + } + + override suspend fun getRoomAliases(): List { + return getRoomLocalAliasesTask.execute(GetRoomLocalAliasesTask.Params(roomId)) + } + + override suspend fun addAlias(aliasLocalPart: String) { + addRoomAliasTask.execute(AddRoomAliasTask.Params(roomId, aliasLocalPart)) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt new file mode 100644 index 0000000000..3400fd994c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DeleteRoomAliasTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface DeleteRoomAliasTask : Task { + data class Params( + val roomAlias: String + ) +} + +internal class DefaultDeleteRoomAliasTask @Inject constructor( + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) : DeleteRoomAliasTask { + + override suspend fun execute(params: DeleteRoomAliasTask.Params) { + executeRequest(eventBus) { + apiCall = directoryAPI.deleteRoomAlias( + roomAlias = params.roomAlias + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetAliasesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetAliasesResponse.kt new file mode 100644 index 0000000000..5965924085 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetAliasesResponse.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class GetAliasesResponse( + /** + * Required. The server's local aliases on the room. Can be empty. + */ + @Json(name = "aliases") val aliases: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt index 58a119cc77..543d605707 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -25,11 +25,11 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.findByAlias import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface GetRoomIdByAliasTask : Task> { +internal interface GetRoomIdByAliasTask : Task> { data class Params( val roomAlias: String, val searchOnServer: Boolean @@ -38,25 +38,25 @@ internal interface GetRoomIdByAliasTask : Task { - var roomId = Realm.getInstance(monarchy.realmConfiguration).use { + override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { + val roomId = Realm.getInstance(monarchy.realmConfiguration).use { RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId } return if (roomId != null) { - Optional.from(roomId) + Optional.from(RoomAliasDescription(roomId)) } else if (!params.searchOnServer) { - Optional.from(null) + Optional.from(null) } else { - roomId = tryOrNull("## Failed to get roomId from alias") { + val description = tryOrNull("## Failed to get roomId from alias") { executeRequest(eventBus) { - apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) + apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias) } - }?.roomId - Optional.from(roomId) + } + Optional.from(description) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt new file mode 100644 index 0000000000..7cfce4ecdc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetRoomLocalAliasesTask : Task> { + data class Params( + val roomId: String + ) +} + +internal class DefaultGetRoomLocalAliasesTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : GetRoomLocalAliasesTask { + + override suspend fun execute(params: GetRoomLocalAliasesTask.Params): List { + // We do not check for "org.matrix.msc2432", so the API may be missing + val response = executeRequest(eventBus) { + apiCall = roomAPI.getAliases(roomId = params.roomId) + } + + return response.aliases + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt new file mode 100644 index 0000000000..25ba493891 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.alias + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import javax.inject.Inject + +internal class RoomAliasAvailabilityChecker @Inject constructor( + @UserId private val userId: String, + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) { + /** + * @param aliasLocalPart the local part of the alias. + * Ex: for the alias "#my_alias:example.org", the local part is "my_alias" + */ + @Throws(RoomAliasError::class) + suspend fun check(aliasLocalPart: String?) { + if (aliasLocalPart.isNullOrEmpty()) { + throw RoomAliasError.AliasEmpty + } + // Check alias availability + val fullAlias = aliasLocalPart.toFullLocalAlias(userId) + try { + executeRequest(eventBus) { + apiCall = directoryAPI.getRoomIdByAlias(fullAlias) + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == 404) { + // This is a 404, so the alias is available: nominal case + null + } else { + // Other error, propagate it + throw throwable + } + } + ?.let { + // Alias already exists: error case + throw RoomAliasError.AliasNotAvailable + } + } + + companion object { + internal fun String.toFullLocalAlias(userId: String) = "#" + this + ":" + userId.substringAfter(":") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt index ada3839fa0..d1f93c50be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class RoomAliasDescription( +data class RoomAliasDescription( /** * The room ID for this alias. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 79ff9db087..fb840b4eb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.toMedium import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.di.AuthenticatedIdentity @@ -96,7 +97,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( fileUploader.uploadFromUri( uri = avatarUri, filename = UUID.randomUUID().toString(), - mimeType = "image/jpeg") + mimeType = MimeTypes.Jpeg) } ?.let { response -> Event( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 0fe9b0ba68..ef792ab98e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.TimeoutCancellationException import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset @@ -31,10 +32,9 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI -import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasAvailabilityChecker import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask @@ -47,8 +47,8 @@ internal interface CreateRoomTask : Task internal class DefaultCreateRoomTask @Inject constructor( private val roomAPI: RoomAPI, - @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, + private val aliasAvailabilityChecker: RoomAliasAvailabilityChecker, private val directChatsHelper: DirectChatsHelper, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val readMarkersTask: SetReadMarkersTask, @@ -65,28 +65,11 @@ internal class DefaultCreateRoomTask @Inject constructor( } else null if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) { - if (params.roomAliasName.isNullOrEmpty()) { - throw CreateRoomFailure.RoomAliasError.AliasEmpty - } - // Check alias availability - val fullAlias = "#" + params.roomAliasName + ":" + userId.substringAfter(":") try { - executeRequest(eventBus) { - apiCall = roomAPI.getRoomIdByAlias(fullAlias) - } - } catch (throwable: Throwable) { - if (throwable is Failure.ServerError && throwable.httpCode == 404) { - // This is a 404, so the alias is available: nominal case - null - } else { - // Other error, propagate it - throw throwable - } + aliasAvailabilityChecker.check(params.roomAliasName) + } catch (aliasError: RoomAliasError) { + throw CreateRoomFailure.AliasError(aliasError) } - ?.let { - // Alias already exists: error case - throw CreateRoomFailure.RoomAliasError.AliasNotAvailable - } } val createRoomBody = createRoomBodyBuilder.build(params) @@ -104,7 +87,7 @@ internal class DefaultCreateRoomTask @Inject constructor( } else if (throwable.httpCode == 400 && throwable.error.code == MatrixError.M_UNKNOWN && throwable.error.message == "Invalid characters in room alias") { - throw CreateRoomFailure.RoomAliasError.AliasInvalid + throw CreateRoomFailure.AliasError(RoomAliasError.AliasInvalid) } } throw throwable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt new file mode 100644 index 0000000000..fbdd6a03eb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetRoomDirectoryVisibilityTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.directory + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.session.directory.RoomDirectoryVisibilityJson +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetRoomDirectoryVisibilityTask : Task { + data class Params( + val roomId: String + ) +} + +internal class DefaultGetRoomDirectoryVisibilityTask @Inject constructor( + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) : GetRoomDirectoryVisibilityTask { + + override suspend fun execute(params: GetRoomDirectoryVisibilityTask.Params): RoomDirectoryVisibility { + return executeRequest(eventBus) { + apiCall = directoryAPI.getRoomDirectoryVisibility(params.roomId) + } + .visibility + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt new file mode 100644 index 0000000000..33b12aa1ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/SetRoomDirectoryVisibilityTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.directory + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.directory.DirectoryAPI +import org.matrix.android.sdk.internal.session.directory.RoomDirectoryVisibilityJson +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface SetRoomDirectoryVisibilityTask : Task { + data class Params( + val roomId: String, + val roomDirectoryVisibility: RoomDirectoryVisibility + ) +} + +internal class DefaultSetRoomDirectoryVisibilityTask @Inject constructor( + private val directoryAPI: DirectoryAPI, + private val eventBus: EventBus +) : SetRoomDirectoryVisibilityTask { + + override suspend fun execute(params: SetRoomDirectoryVisibilityTask.Params) { + executeRequest(eventBus) { + apiCall = directoryAPI.setRoomDirectoryVisibility( + params.roomId, + RoomDirectoryVisibilityJson(visibility = params.roomDirectoryVisibility) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index a7dfcfc96f..784b610af7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -21,7 +21,6 @@ import org.matrix.android.sdk.R import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -71,12 +70,6 @@ internal class RoomDisplayNameResolver @Inject constructor( return name } - val aliases = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root - name = ContentMapper.map(aliases?.content).toModel()?.aliases?.firstOrNull() - if (!name.isNullOrEmpty()) { - return name - } - val roomMembers = RoomMemberHelper(realm, roomId) val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt new file mode 100644 index 0000000000..5a82d74537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.peeking + +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomTopicContent +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface PeekRoomTask : Task { + data class Params( + val roomIdOrAlias: String + ) +} + +internal class DefaultPeekRoomTask @Inject constructor( + private val getRoomIdByAliasTask: GetRoomIdByAliasTask, + private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, + private val getPublicRoomTask: GetPublicRoomTask, + private val resolveRoomStateTask: ResolveRoomStateTask +) : PeekRoomTask { + + override suspend fun execute(params: PeekRoomTask.Params): PeekResult { + val roomId: String + val serverList: List + val isAlias = MatrixPatterns.isRoomAlias(params.roomIdOrAlias) + if (isAlias) { + // get alias description + val aliasDescription = getRoomIdByAliasTask + .execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true)) + .getOrNull() + ?: return PeekResult.UnknownAlias + + roomId = aliasDescription.roomId + serverList = aliasDescription.servers + } else { + roomId = params.roomIdOrAlias + serverList = emptyList() + } + + // Is it a public room? + val publicRepoResult = when (getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))) { + RoomDirectoryVisibility.PRIVATE -> { + // We cannot resolve this room :/ + null + } + RoomDirectoryVisibility.PUBLIC -> { + // Try to find it in directory + val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1)) + else null + + getPublicRoomTask.execute(GetPublicRoomTask.Params( + server = serverList.firstOrNull(), + publicRoomsParams = PublicRoomsParams( + filter = filter, + limit = 20.takeIf { filter != null } ?: 100 + ) + )).chunk?.firstOrNull { it.roomId == roomId } + } + } + + if (publicRepoResult != null) { + return PeekResult.Success( + roomId = roomId, + alias = publicRepoResult.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias }, + avatarUrl = publicRepoResult.avatarUrl, + name = publicRepoResult.name, + topic = publicRepoResult.topic, + numJoinedMembers = publicRepoResult.numJoinedMembers, + viaServers = serverList + ) + } + + // mm... try to peek state ? maybe the room is not public but yet allow guest to get events? + // this could be slow + try { + val stateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId)) + val name = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_NAME && it.stateKey == "" } + ?.let { it.content?.toModel()?.name } + + val topic = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_TOPIC && it.stateKey == "" } + ?.let { it.content?.toModel()?.topic } + + val avatarUrl = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_AVATAR } + ?.let { it.content?.toModel()?.avatarUrl } + + val alias = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS } + ?.let { it.content?.toModel()?.canonicalAlias } + + // not sure if it's the right way to do that :/ + val memberCount = stateEvents + .filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true } + .distinctBy { it.stateKey } + .count() + + return PeekResult.Success( + roomId = roomId, + alias = alias, + avatarUrl = avatarUrl, + name = name, + topic = topic, + numJoinedMembers = memberCount, + viaServers = serverList + ) + } catch (failure: Throwable) { + // Would be M_FORBIDDEN if cannot peek :/ + // User XXX not in room !XXX, and room previews are disabled + return PeekResult.PeekingNotAllowed( + roomId = roomId, + alias = params.roomIdOrAlias.takeIf { isAlias }, + viaServers = serverList + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt new file mode 100644 index 0000000000..03ea2408f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.peeking + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ResolveRoomStateTask : Task> { + data class Params( + val roomId: String + ) +} + +internal class DefaultResolveRoomStateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : ResolveRoomStateTask { + + override suspend fun execute(params: ResolveRoomStateTask.Params): List { + return executeRequest(eventBus) { + apiCall = roomAPI.getRoomState(params.roomId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index b13ce15da6..8828f3dfed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor( val attachmentData = ContentAttachmentData( size = messageContent.info!!.size, mimeType = messageContent.info.mimeType!!, - name = messageContent.body, + name = messageContent.getFileName(), queryUri = Uri.parse(messageContent.url), type = ContentAttachmentData.Type.FILE ) @@ -210,6 +210,8 @@ internal class DefaultSendService @AssistedInject constructor( override fun cancelSend(eventId: String) { cancelSendTracker.markLocalEchoForCancel(eventId, roomId) + // This is maybe the current task, so cancel it too + eventSenderProcessor.cancel(eventId, roomId) taskExecutor.executorScope.launch { localEchoRepository.deleteFailedEcho(roomId, eventId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index 62e225c624..5014d94558 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.send.queue +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.auth.data.SessionParams @@ -106,17 +107,21 @@ internal class EventSenderProcessor @Inject constructor( // non blocking add to queue sendingQueue.add(task) markAsManaged(task) - return object : Cancelable { - override fun cancel() { - task.cancel() - } - } + return task + } + + fun cancel(eventId: String, roomId: String) { + (currentTask as? SendEventQueuedTask) + ?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId } + ?.cancel() } companion object { private const val RETRY_WAIT_TIME_MS = 10_000L } + private var currentTask: QueuedTask? = null + private var sendingQueue = LinkedBlockingQueue() private var networkAvailableLock = Object() @@ -129,6 +134,7 @@ internal class EventSenderProcessor @Inject constructor( while (!isInterrupted) { Timber.v("## SendThread wait for task to process") val task = sendingQueue.take() + .also { currentTask = it } Timber.v("## SendThread Found task to process $task") if (task.isCancelled()) { @@ -183,6 +189,10 @@ internal class EventSenderProcessor @Inject constructor( task.onTaskFailed() throw InterruptedException() } + exception is CancellationException -> { + Timber.v("## SendThread task has been cancelled") + break@retryLoop + } else -> { Timber.v("## SendThread retryLoop Un-Retryable error, try next task") // this task is in error, check next one? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index e69c65ec4c..dfbac347d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room.send.queue import android.content.Context -import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.send.SendState diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt index bccbc97ff4..9a7fcd8d91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt @@ -16,14 +16,26 @@ package org.matrix.android.sdk.internal.session.room.send.queue -abstract class QueuedTask { +import org.matrix.android.sdk.api.util.Cancelable + +abstract class QueuedTask : Cancelable { var retryCount = 0 - abstract suspend fun execute() + private var hasBeenCancelled: Boolean = false + + suspend fun execute() { + if (!isCancelled()) { + doExecute() + } + } + + abstract suspend fun doExecute() abstract fun onTaskFailed() - abstract fun isCancelled() : Boolean + open fun isCancelled() = hasBeenCancelled - abstract fun cancel() + final override fun cancel() { + hasBeenCancelled = true + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index a3c19a1f7c..8e7ba2f155 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -22,20 +22,18 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository internal class RedactQueuedTask( - val toRedactEventId: String, + private val toRedactEventId: String, val redactionLocalEchoId: String, - val roomId: String, - val reason: String?, - val redactEventTask: RedactEventTask, - val localEchoRepository: LocalEchoRepository, - val cancelSendTracker: CancelSendTracker + private val roomId: String, + private val reason: String?, + private val redactEventTask: RedactEventTask, + private val localEchoRepository: LocalEchoRepository, + private val cancelSendTracker: CancelSendTracker ) : QueuedTask() { - private var _isCancelled: Boolean = false + override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]" - override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]" - - override suspend fun execute() { + override suspend fun doExecute() { redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason)) } @@ -44,10 +42,6 @@ internal class RedactQueuedTask( } override fun isCancelled(): Boolean { - return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) - } - - override fun cancel() { - _isCancelled = true + return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt index 21a4145a9d..ea097082c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt @@ -33,11 +33,9 @@ internal class SendEventQueuedTask( val cancelSendTracker: CancelSendTracker ) : QueuedTask() { - private var _isCancelled: Boolean = false + override fun toString() = "[SendEventQueuedTask ${event.eventId}]" - override fun toString() = "[SendEventRunnableTask ${event.eventId}]" - - override suspend fun execute() { + override suspend fun doExecute() { sendEventTask.execute(SendEventTask.Params(event, encrypt)) } @@ -56,10 +54,6 @@ internal class SendEventQueuedTask( } override fun isCancelled(): Boolean { - return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) - } - - override fun cancel() { - _isCancelled = true + return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 3463b26c8a..b546584450 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -20,28 +20,26 @@ import android.net.Uri import androidx.lifecycle.LiveData import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.state.StateService -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.task.launchToCallback -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.awaitCallback internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, - private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, private val fileUploader: FileUploader, private val addRoomAliasTask: AddRoomAliasTask ) : StateService { @@ -67,88 +65,91 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) } - override fun sendStateEvent( + override suspend fun sendStateEvent( eventType: String, stateKey: String?, - body: JsonDict, - callback: MatrixCallback - ): Cancelable { + body: JsonDict + ) { val params = SendStateTask.Params( roomId = roomId, stateKey = stateKey, eventType = eventType, body = body ) - return sendStateTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + sendStateTask.execute(params) } - override fun updateTopic(topic: String, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateTopic(topic: String) { + sendStateEvent( eventType = EventType.STATE_ROOM_TOPIC, body = mapOf("topic" to topic), - callback = callback, stateKey = null ) } - override fun updateName(name: String, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateName(name: String) { + sendStateEvent( eventType = EventType.STATE_ROOM_NAME, body = mapOf("name" to name), - callback = callback, stateKey = null ) } - override fun addRoomAlias(roomAlias: String, callback: MatrixCallback): Cancelable { - return addRoomAliasTask - .configureWith(AddRoomAliasTask.Params(roomId, roomAlias)) { - this.callback = callback - } - .executeBy(taskExecutor) - } - - override fun updateCanonicalAlias(alias: String, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateCanonicalAlias(alias: String?, altAliases: List) { + sendStateEvent( eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, - body = mapOf("alias" to alias), - callback = callback, + body = RoomCanonicalAliasContent( + canonicalAlias = alias, + alternativeAliases = altAliases + // Ensure there is no duplicate + .distinct() + // Ensure the canonical alias is not also included in the alt alias + .minus(listOfNotNull(alias)) + // Sort for the cleanup + .sorted() + ).toContent(), stateKey = null ) } - override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateHistoryReadability(readability: RoomHistoryVisibility) { + sendStateEvent( eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, body = mapOf("history_visibility" to readability), - callback = callback, stateKey = null ) } - override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") - awaitCallback { - sendStateEvent( - eventType = EventType.STATE_ROOM_AVATAR, - body = mapOf("url" to response.contentUri), - callback = it, - stateKey = null - ) - } + override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) { + if (joinRules != null) { + sendStateEvent( + eventType = EventType.STATE_ROOM_JOIN_RULES, + body = RoomJoinRulesContent(joinRules).toContent(), + stateKey = null + ) + } + if (guestAccess != null) { + sendStateEvent( + eventType = EventType.STATE_ROOM_GUEST_ACCESS, + body = RoomGuestAccessContent(guestAccess).toContent(), + stateKey = null + ) } } - override fun deleteAvatar(callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateAvatar(avatarUri: Uri, fileName: String) { + val response = fileUploader.uploadFromUri(avatarUri, fileName, MimeTypes.Jpeg) + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + stateKey = null + ) + } + + override suspend fun deleteAvatar() { + sendStateEvent( eventType = EventType.STATE_ROOM_AVATAR, body = emptyMap(), - callback = callback, stateKey = null ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt index a3862b001b..7437a686da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants +import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.latestEvent @@ -26,7 +27,7 @@ internal object RoomSummaryEventsHelper { private val previewFilters = TimelineEventFilters( filterTypes = true, - allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES, + allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES.map { EventTypeFilter(eventType = it, stateKey = null) }, filterUseless = true, filterRedacted = false, filterEdits = true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 995a21aa23..86b0497bd0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -784,19 +784,20 @@ internal class DefaultTimeline( } private fun List.filterEventsWithSettings(): List { - return filter { - val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type) + return filter { event -> + val filterType = !settings.filters.filterTypes + || settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) } if (!filterType) return@filter false - val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) { - val messageContent = it.root.getClearContent().toModel() + val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) { + val messageContent = event.root.getClearContent().toModel() messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE } else { true } if (!filterEdits) return@filter false - val filterRedacted = settings.filters.filterRedacted && it.root.isRedacted() + val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted() !filterRedacted } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 3dcc5e21b1..fa517bebf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -151,8 +151,25 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu beginGroup() var needOr = false if (settings.filters.filterTypes) { - val allowedTypes = settings.filters.allowedTypes.toTypedArray() - not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", allowedTypes) + beginGroup() + // Events: A, B, C, D, (E and S1), F, G, (H and S1), I + // Allowed: A, B, C, (E and S1), G, (H and S2) + // Result: D, F, H, I + settings.filters.allowedTypes.forEachIndexed { index, filter -> + if (filter.stateKey == null) { + notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", filter.eventType) + } else { + beginGroup() + notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", filter.eventType) + or() + notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.STATE_KEY}", filter.stateKey) + endGroup() + } + if (index != settings.filters.allowedTypes.size - 1) { + and() + } + } + endGroup() needOr = true } if (settings.filters.filterUseless) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt index 824bd23c01..895f1cf50d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt @@ -18,17 +18,12 @@ package org.matrix.android.sdk.internal.session.room.uploads import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult import org.matrix.android.sdk.api.session.room.uploads.UploadsService -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith internal class DefaultUploadsService @AssistedInject constructor( @Assisted private val roomId: String, - private val taskExecutor: TaskExecutor, private val getUploadsTask: GetUploadsTask, private val cryptoService: CryptoService ) : UploadsService { @@ -38,11 +33,7 @@ internal class DefaultUploadsService @AssistedInject constructor( fun create(roomId: String): UploadsService } - override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable { - return getUploadsTask - .configureWith(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult { + return getUploadsTask.execute(GetUploadsTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), numberOfEvents, since)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index 74cba5e796..424c24663c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.session.sync.SyncTask -import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.createUIHandler @@ -50,14 +49,13 @@ private const val RETRY_WAIT_TIME_MS = 10_000L private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L internal class SyncThread @Inject constructor(private val syncTask: SyncTask, - private val typingUsersTracker: DefaultTypingUsersTracker, private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, private val activeCallHandler: ActiveCallHandler ) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle - private var liveState = MutableLiveData(state) + private var liveState = MutableLiveData(state) private val lock = Object() private val syncScope = CoroutineScope(SupervisorJob()) private val debouncer = Debouncer(createUIHandler()) @@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, return } state = newState - debouncer.debounce("post_state", Runnable { + debouncer.debounce("post_state", { liveState.value = newState }, 150) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index 5eb97cee3a..41914cc799 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -17,11 +17,10 @@ package org.matrix.android.sdk.internal.session.terms import dagger.Lazy -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.terms.GetTermsResponse import org.matrix.android.sdk.api.session.terms.TermsService -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.RetrofitFactory @@ -33,8 +32,6 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTe import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.ensureTrailingSlash import okhttp3.OkHttpClient @@ -49,13 +46,11 @@ internal class DefaultTermsService @Inject constructor( private val getOpenIdTokenTask: GetOpenIdTokenTask, private val identityRegisterTask: IdentityRegisterTask, private val updateUserAccountDataTask: UpdateUserAccountDataTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val taskExecutor: TaskExecutor + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : TermsService { - override fun getTerms(serviceType: TermsService.ServiceType, - baseUrl: String, - callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + override suspend fun getTerms(serviceType: TermsService.ServiceType, + baseUrl: String): GetTermsResponse { + return withContext(coroutineDispatchers.main) { val url = buildUrl(baseUrl, serviceType) val termsResponse = executeRequest(null) { apiCall = termsAPI.getTerms("${url}terms") @@ -64,12 +59,11 @@ internal class DefaultTermsService @Inject constructor( } } - override fun agreeToTerms(serviceType: TermsService.ServiceType, - baseUrl: String, - agreedUrls: List, - token: String?, - callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + override suspend fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List, + token: String?) { + withContext(coroutineDispatchers.main) { val url = buildUrl(baseUrl, serviceType) val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt index 4dc54d3b19..fb5e3a5774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt @@ -25,6 +25,9 @@ import java.io.InputStream */ @WorkerThread fun writeToFile(inputStream: InputStream, outputFile: File) { + // Ensure the parent folder exists, else it will crash + outputFile.parentFile?.mkdirs() + outputFile.outputStream().use { inputStream.copyTo(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt index 3d80ad01d5..e19b1bcca7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.util import java.security.MessageDigest +import java.util.Locale /** * Compute a Hash of a String, using md5 algorithm @@ -26,7 +27,7 @@ fun String.md5() = try { digest.update(toByteArray()) digest.digest() .joinToString("") { String.format("%02X", it) } - .toLowerCase() + .toLowerCase(Locale.ROOT) } catch (exc: Exception) { // Should not happen, but just in case hashCode().toString() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt new file mode 100644 index 0000000000..0998601db6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import androidx.collection.LruCache + +@Suppress("NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER") +internal inline fun LruCache.getOrPut(key: K, defaultValue: () -> V): V { + return get(key) ?: defaultValue().also { put(key, it) } +} diff --git a/matrix-sdk-android/src/main/res/values-bg/strings_sas.xml b/matrix-sdk-android/src/main/res/values-bg/strings_sas.xml new file mode 100644 index 0000000000..e2ee9faefc --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bg/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Куче + Котка + Лъв + Кон + Еднорог + Прасе + Слон + Заек + Панда + Петел + Пингвин + Костенурка + Риба + Октопод + Пеперуда + Цвете + Дърво + Кактус + Гъба + Глобус + Луна + Облак + Огън + Банан + Ябълка + Ягода + Царевица + Пица + Торта + Сърце + Усмивка + Робот + Шапка + Очила + Гаечен ключ + Дядо Коледа + Палец нагоре + Чадър + Пясъчен часовник + Часовник + Подарък + Лампа + Книга + Молив + Кламер + Ножици + Катинар + Ключ + Чук + Телефон + Флаг + Влак + Колело + Самолет + Ракета + Трофей + Топка + Китара + Тромпет + Звънец + Котва + Слушалки + Папка + Кабърче + diff --git a/matrix-sdk-android/src/main/res/values-ca/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ca/strings_sas.xml new file mode 100644 index 0000000000..acc0dcbc72 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ca/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Gos + Gat + Lleó + Cavall + Unicorn + Porc + Elefant + Conill + Panda + Gall + Pingüí + Tortuga + Peix + Pop + Papallona + Flor + Arbre + Cactus + Bolet + Globus terraqüi + Lluna + Núvol + Foc + Plàtan + Poma + Maduixa + Blat de moro + Pizza + Pastís + Cor + Somrient + Robot + Barret + Ulleres + Clau anglesa + Pare Noél + Polzes amunt + Paraigües + Rellotge de sorra + Rellotge + Regal + Bombeta + Llibre + Llapis + Clip + Tisores + Cadenat + Clau + Martell + Telèfon + Bandera + Tren + Bicicleta + Avió + Coet + Trofeu + Pilota + Guitarra + Trompeta + Campana + Àncora + Auriculars + Carpeta + Xinxeta + diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml index ebf7590596..50dea12b09 100644 --- a/matrix-sdk-android/src/main/res/values-cs/strings.xml +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -218,4 +218,28 @@ %1$s vstoupili Založili jste diskusi %1$s založil diskusi + Prázdná místnost (byla %s) + + %1$s, %2$s, %3$s a %4$d další + %1$s, %2$s, %3$s a %4$d další + %1$s, %2$s, %3$s a %4$d dalších + + %1$s, %2$s, %3$s a %4$s + %1$s, %2$s a %3$s + 🎉 Účast všech serverů je zakázána! Tuto místnost již nelze použít. + Beze změny. + • Servery shodující se doslovně s IP jsou nyní zakázány. + • Servery shodující se doslovně s IP jsou nyní povoleny. + • Servery shodující se s %s byly odstraněny ze seznamu povolených. + • Servery shodující se s %s jsou nyní povoleny. + • Servery shodující se s %s byly odstraněny ze seznamu zakázaných. + • Servery shodující se s %s jsou nyní zakázány. + Změnili jste ACL serveru pro tuto místnost. + %s změnili ACL serveru pro tuto místnost. + • Server shodující se doslovně s IP je povolen. + • Server shodující se doslovně s IP je zakázán. + • Server shodující se s %s je povolen. + • Server shodující se s %s je zakázán. + Nastavili jste ACL serveru pro tuto místnost. + %s nastavili ACL serveru pro tuto místnost. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index 4c574d578a..bdeeafccb6 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -56,7 +56,7 @@ E-Mail-Adresse Telefonnummer - %1$s sandte einen Sticker. + %1$s hat einen Sticker gesendet. Einladung von %s Raumeinladung diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml index 71ee50f075..957c0b9955 100644 --- a/matrix-sdk-android/src/main/res/values-et/strings.xml +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -213,4 +213,27 @@ %1$s liitus Sina alustasid vestlust %1$s alustas vestlust + Tühi jututuba (oli %s) + + %1$s, %2$s, %3$s ja %4$d muu + %1$s, %2$s, %3$s ja %4$d muud + + %1$s, %2$s, %3$s ja %4$s + %1$s, %2$s ja %3$s + 🎉 Kõikide serverite osalemine on keelatud! Seda jututuba ei saa enam kasutada. + Muudatusi ei ole. + • Nüüd on keelatud serverid, mille ip-aadress vastab mustrile. + • Nüüd on lubatud serverid, mille ip-aadress vastab mustrile. + • Server, mille nimes leidub %s, eemaldati lubatud serverite loendist. + • Nüüd on lubatud serverid, mille nimes leidub %s. + • Server, mille nimes leidub %s eemaldati keeluloendist. + • Keelatud on server, mille nimes leidub %s. + Sina muutsid selle jututoa jaoks serverite pääsuloendit. + %s muutis selle jututoa jaoks serverite pääsuloendit. + Sina kirjeldasid selle jututoa jaoks serverite pääsuloendi. + %s kirjeldas selle jututoa jaoks serverite pääsuloendi. + • Keelatud on serverid, mille ip-aadress vastab mustrile. + • Lubatud on serverid, mille ip-aadress vastab mustrile. + • Lubatud on serverid, mille nimes leidub %s. + • Keelatud on serverid, mille nimes leidub %s. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml index 11a786f5ac..8f8059067e 100644 --- a/matrix-sdk-android/src/main/res/values-fa/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -213,4 +213,9 @@ %1$s پیوست گفت‌وگو را ایجاد کردید %1$s گفت‌وگو را ایجاد کرد + + %1$s، %2$s، %3$s و %4$d نفر دیگر + %1$s، %2$s، %3$s و %4$d نفر دیگر + + %1$s، %2$s و %3$s \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index fccd22d3b6..1e3788476f 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -1,7 +1,6 @@ - + %1$s lähetti kuvan. - Käyttäjän %s kutsu %1$s kutsui käyttäjän %2$s %1$s kutsui sinut @@ -29,11 +28,9 @@ kaikki. tuntematon (%s). %1$s otti käyttöön osapuolten välisen salauksen (%2$s) - %1$s lähetti VoIP-konferenssipyynnön VoIP-konferenssi alkoi VoIP-konferenssi päättyi - (myös kuva vaihdettiin) %1$s poisti huoneen nimen %1$s poisti huoneen aiheen @@ -42,44 +39,31 @@ %1$s hyväksyi kutsun käyttäjän %2$s puolesta ** Salauksen purku epäonnistui: %s ** Lähettäjän laite ei ole lähettänyt avaimia tähän viestiin. - Viestin lähetys epäonnistui - Kuvan lataaminen epäonnistui - Verkkovirhe Matrix-virhe - Tällä hetkellä ei ole mahdollista liittyä uudelleen tyhjään huoneeseen. - Salattu viesti - Sähköpostiosoite Puhelinnumero - - Takaisinveto epäonnistui %1$s: %2$s - - Kutsu käyttäjältä %s + Kutsu käyttäjältä %s + Huonekutsu %1$s ja %2$s Tyhjä huone - - %1$s lähetti tarran. - %1$s ja yksi muu %1$s ja %2$d muuta - Viesti poistettu %1$s poisti viestin Viesti poistettu [syy: %1$s] %1$s poisti viestin [syy: %2$s] - Alkusynkronointi: \nTuodaan tiliä… Alkusynkronointi: @@ -96,12 +80,9 @@ \nTuodaan yhteisöjä Alkusynkronointi: \nTuodaan tilin tietoja - %s päivitti tämän huoneen. - Lähetetään viestiä… Tyhjennä lähetysjono - %1$s veti takaisin käyttäjän %2$s liittymiskutsun huoneeseen Henkilön %1$s kutsu. Syy: %2$s %1$s kutsui henkilön %2$s. Syy: %3$s @@ -116,28 +97,117 @@ %1$s kumosi kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s %1$s hyväksyi kutsun liityäkseen huoneeseen %2$s. Syy: %3$s %1$s veti takaisin käyttäjän %2$s kutsun. Syy: %3$s - %1$s lisäsi tälle huoneelle osoitteen %2$s. %1$s lisäsi tälle huoneelle osoitteet %2$s. - %1$s poisti tältä huoneelta osoitteen %2$s. %1$s poisti tältä huoneelta osoitteet %3$s. - %1$s lisäsi tälle huoneelle osoitteen %2$s ja poisti osoitteen %3$s. - %1$s asetti tämän huoneen pääosoitteeksi %2$s. %1$s poisti tämän huoneen pääosoitteen. - %1$s salli vieraiden liittyä huoneeseen. %1$s esti vieraita liittymästä huoneeseen. - %1$s laittoi päälle osapuolten välisen salauksen. %1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s). - %s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa. - - + Hyväksyit käyttäjän %1$s kutsun. Syy: %2$s + Peruutit kutsun liittyä huoneeseen käyttäjältä %1$s. Syy: %2$s + Lähetit kutsun liittyä huoneeseen käyttäjälle %1$s. Syy: %2$s + Estit käyttäjän %1$s. Syy: %2$s + Peruutit eston %1$s. Syy: %2$s + Poistit käyttäjän %1$s. Syy: %2$s + Hylkäsit kutsun. Syy: %1$s + Lähdit. Syy: %1$s + %1$s lähti. Syy: %2$s + Poistuit huoneesta. Syy: %1$s + Liityit. Syy: %1$s + %1$s liittyi. Syy: %2$s + Liityit ryhmään. Syy: %1$s + Kutsuit %1$s. Syy: %2$s + Kutsusi. Syy: %1$s + Tyhjä huone (oli %s) + %1$s, %2$s, %3$s ja %4$s + %1$s, %2$s ja %3$s + Mukautettu + Mukautettu (%1$d) + Oletus + Valvoja + Ylläpitäjä + %1$s muutti %2$s sovelmaa + Poistit %1$s sovelman + %1$s poisti %2$s sovelman + Lisäsit %1$s sovelman + %1$s lisäsi %2$s sovelman + Muutit %1$s sovelmaa + Hyväksyit kutsun henkilölle %1$s + Peruutit kutsun henkilöltä %1$s + %1$s peruutti kutsun henkilöltä %2$s + Peruutit henkilön %1$s kutsun liittyä ryhmään + Kutsuit %1$s + %1$s kutsui %2$s + Lähetit henkilölle %1$s kutsun liittyä huoneeseen + Päivitit profiilisi %1$s + Poistit huoneen profiilikuvan + %1$s poisti huoneen profiilikuvan + Poistit huoneen aiheen + Poistit huoneen nimen + Pyysit ryhmäpuhelua + 🎉 Kaikki palvelimet on estetty osallistumasta! Tätä huonetta ei voi enää käyttää. + Ei muutosta. + • Palvelimet jotka %s poistettiin estolistalta. + • Palvelimen haku %s on nyt kielletty. + • Palvelimen haku %s on sallittu. + • Palvelimen haku %s on kielletty. + %1$s on estänyt vieraita liittymästä huoneeseen. + Estit vieraita liittymästä huoneeseen. + Annoit vieraille luvan liittyä huoneeseen. + Annoit vieraille luvan liittyä tänne. + %1$s on antanut vieraille luvan liittyä tänne. + Poistit tämän huoneen pääosoitteen. + Otit käyttöön päästä päähän -salauksen. + Olet estänyt vieraiden liittymisen huoneeseen. + Otit päästä päähän -salauksen käyttöön (tuntematon algoritmi %1$s). + Päivitit tässä. + %s päivitti täällä. + Päivitit tämän huoneen. + Otit päästä päähän -salauksen käyttöön (%1$s) + Teit tulevista viesteistä näkyviä käyttäjälle %1$s + %1$s teki tulevista viesteistä näkyviä käyttäjälle %2$s + Teit tulevan huonehistorian näkyväksi %1$s + Lopetit puhelun. + Vastasit puheluun. + Lähetit tietoja puhelun valmistelemiseksi. + %s lähetti tietoja puhelun valmistelemiseksi. + Aloitit äänipuhelun. + Aloitit videopuhelun. + Vaihdoit huoneen nimeksi: %1$s + Vaihdoit huoneen profiilikuvaa + %1$s muutti huoneen profiilikuvaa + Vaihdoit aiheen: %1$s + Poistit nimimerkkisi (se oli %1$s) + Vaihdoit nimimerkkisi %1$s nimeen %2$s + Asetit nimimerkiksesi %1$s + Kutsusi + Vaihdoit profiilikuvaasi + Peruutit %1$sn kutsun + Estit %1$s + Poistit eston %1$s + Poistit %1$s + Hylkäsit kutsun + Poistuit huoneesta + %1$s poistui huoneesta + Poistuit huoneesta + Liityit + %1$s liittyi + Liityit huoneeseen + Kutsuit %1$s + Loit keskustelun + %1$s loi keskustelun + Loit huoneen + %1$s loi huoneen + Lähetit tarran. + Lähetit kuvan. + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index 9c5fa7d3b1..f49c54a8ba 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -213,4 +213,27 @@ Vous avez exclus %1$s Vous avez révoqué l\'exclusion de %1$s Vous avez expulsé %1$s. Raison : %2$s + Salon vide (était %s) + + %1$s, %2$s, %3$s et %4$d autre + %1$s, %2$s, %3$s et %4$d autres + + %1$s, %2$s, %3$s et %4$s + %1$s, %2$s et %3$s + 🎉 Tous les serveurs sont interdits de participer ! Ce salon ne peut plus être utilisé. + Aucun changement. + • Les serveurs correspondant à des IP littérales sont maintenant interdits. + • Les serveurs correspondant à %s sont interdits. + • Les serveurs correspondants à des IP littérales sont interdites. + • Les serveurs correspondants à des IP littérales sont autorisés. + • Les serveurs correspondants à des IP littérales sont maintenant autorisées. + • Les serveurs correspondant à %s sont supprimés de la liste autorisée. + • les serveur correspondant à %s sont maintenant autorisés. + • Les serveurs correspondant à %s étaient supprimés de la liste des interdits. + • Les serveurs correspondant à %s sont maintenant interdits. + Vous avez changé les droits ACL du serveur pour ce salon. + %s a changé les droits ACL du serveur pour ce salon. + • Les serveurs correspondant à %s sont autorisés. + Vous avez paramétré les ACL pour ce salon. + %s paramètre les autorisations étendues (ACL) du serveur pour ce salon. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index 5eab8c57df..c1a5cf85cb 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -214,4 +214,27 @@ %1$s è entrato Hai creato la discussione %1$s ha creato la discussione + Stanza vuota (era %s) + + %1$s, %2$s, %3$s e %4$d altro + %1$s, %2$s, %3$s e altri %4$d + + %1$s, %2$s, %3$s e %4$s + %1$s, %2$s e %3$s + 🎉 Tutti i server sono banditi dalla partecipazione! Questa stanza non può più essere usata. + Nessuna modifica. + • I server che corrispondono a IP letterali ora sono banditi. + • I server che corrispondono a IP letterali ora sono permessi. + • I server che corrispondono a %s sono stati rimossi dalla lista dei consentiti. + • I server che corrispondono a %s ora sono permessi. + • I server che corrispondono a %s sono stati rimossi dalla lista di ban. + • I server che corrispondono a %s ora sono banditi. + Hai cambiato le ACL del server per questa stanza. + %s ha cambiato le ACL del server per questa stanza. + • I server che corrispondono a IP letterali sono banditi. + • I server che corrispondono a IP letterali sono permessi. + • I server che corrispondono a %s sono permessi. + • I server che corrispondono a %s sono banditi. + Hai impostato le ACL del server per questa stanza. + %s ha impostato le ACL del server per questa stanza. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml index ed9f91cdb3..e6c93cb55c 100644 --- a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -221,4 +221,27 @@ %1$s entrou Você criou a sala %1$s criou a sala + Sala vazia (era %s) + + %1$s, %2$s, %3$s e %4$d outro + %1$s, %2$s, %3$s e %4$d outros + + %1$s, %2$s, %3$s e %4$s + %1$s, %2$s e %3$s + 🎉 Todos os servidores estão proibidos de participar! Esta sala não pode mais ser usada. + Nenhuma alteração. + • Servidores correspondentes aos IP literais agora estão banidos. + • Servidores correspondentes aos IP literais agora estão permitidos. + • Servidores correspondentes à %s foram removidos da lista de permitidos. + • Servidores correspondentes à %s agora são permitidos. + • Servidores correspondente à %s foram removidos da lista de banidos. + • Servidores correspondentes à %s foram banidos. + Você alterou a lista de controle de acesso (ACL) do servidor para esta sala. + %s alterou a lista de controle de acesso (ACL) do servidor para esta sala. + • Servidores correspondentes aos IP literais estão banidos. + • Servidores correspondentes aos IP literais estão permitidos. + • Servidores correspondentes à %s estão permitidos. + • Servidores correspondentes à %s estão banidos. + Você definiu a lista de controle de acesso (ACL) do servidor para esta sala. + %s definiu a lista de controle de acesso (ACL) do servidor para esta sala. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml index ef9cda1dc2..5ef5a4f447 100644 --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -225,6 +225,31 @@ %1$s вошел(ла) Вы создали обсуждение %1$s создал(а) обсуждение - Вы обновили. - %s обновлена. + Вы обновили эту комнату. + %s обновил(а) эту комнату. + + %1$s, %2$s, %3$s и %4$d другой + %1$s, %2$s, %3$s и %4$d других + %1$s, %2$s, %3$s и %4$d другие + %1$s, %2$s, %3$s и %4$d другие + + %1$s, %2$s, %3$s и %4$s + %1$s, %2$s и %3$s + 🎉 Всем серверам запрещено участвовать! Эта комната больше не может быть использована. + Без изменений. + Пустая комната (была %s) + • Соответствующий сервер %s заблокирован. + • Сервер, соответствующий буквальным IP-адресам, теперь запрещён. + • Сервер, соответствующий буквальным IP-адресам, теперь разрешён. + • Сервер, соответствующий %s, теперь запрещён. + • Сервер, соответствующий %s, теперь разрешён. + • Сервер, соответствующий %s, был удалён из списка блокировки. + • Сервер, соответствующий буквальным IP-адресам, запрещён. + • Сервер, соответствующий буквальным IP-адресам, разрешён. + • Сервер, соответствующий %s, разрешён. + • Сервер, соответствующий %s, был удалён из разрешённого списка. + Вы изменили права доступа сервера (ACL) для этой комнаты. + %s изменил права доступа сервера (ACL) для этой комнаты. + Вы настроили права доступа сервера (ACL) для этой комнаты. + %s устанавливает права доступа сервера (ACL) для этой комнаты. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index 4055b35025..58ba8877bb 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -213,4 +213,23 @@ %1$s erdhi Krijuat diskutimin %1$s krijoi diskutimin + Dhomë e zbrazët (was %s) + + %1$s, %2$s, %3$s dhe %4$d tjetër + %1$s, %2$s, %3$s dhe %4$d të tjerë + + %1$s, %2$s, %3$s dhe %4$s + %1$s, %2$s dhe %3$s + 🎉 U është penguar pjesëmarrja krejt shërbyesve! Kjo dhomë s’mund të përdoret më. + Pa ndryshim. + Ndryshuat ACL-ra shërbyesi për këtë dhomë. + %s ndryshoi ACL-ra shërbyesi për këtë dhomë. + Ujdisët ACL-ra shërbyesi për këtë dhomë. + %s ujdisi ACL-ra shërbyesi për këtë dhomë. + • Shërbyes që kanë përputhje me %s u hoqën nga lista e të lejuarve. + • Shërbyesit që kanë përputhje me %s tani janë të lejuar. + • Shërbyesit që kanë përputhje me %s u hoqën nga lista e ndalimeve. + • Shërbyesit që kanë përputhje me %s tani janë të ndaluar. + • Shërbyesit që kanë përputhje me %s janë të ndaluar. + • Shërbyesit që kanë përputhje me %s janë të ndaluar. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-uk/strings.xml b/matrix-sdk-android/src/main/res/values-uk/strings.xml index eb5071f190..2477487379 100644 --- a/matrix-sdk-android/src/main/res/values-uk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-uk/strings.xml @@ -1,39 +1,34 @@ - + %1$s: %2$s %1$s надіслав(ла) зображення. - %s запрошення %1$s запросив(ла) %2$s Зашифроване повідомлення - Запрошення від %s Запрошення до кімнати %1$s і %2$s Порожня кімната - - %1$s надіслав(ла) наліпку. - %1$s запросив(ла) Вас - %1$s приєднався(лась) + %1$s приєднується %1$s покинув(ла) %1$s відхилив(ла) запрошення %1$s копнув(ла) %2$s %1$s розблокував(ла) %2$s %1$s заблокував(ла) %2$s %1$s відкликав(ла) запрошення для %2$s - %1$s змінив(ла) свій аватар - %1$s встановив(ла) собі ім’я %2$s - %1$s змінив(ла) своє ім’я з %2$s на %3$s + %1$s змінює свій аватар + %1$s встановлюють собі назву %2$s + %1$s змінює своє ім’я з %2$s на %3$s %1$s прибрав(ла) своє ім’я (%2$s) - %1$s змінив(ла) тему на: %2$s + %1$s змінює тему на: %2$s %1$s змінив(ла) назву кімнати на: %2$s %s розпочав(ла) відеодзвінок. %s розпочав(ла) голосовий дзвінок. %s відповів(ла) на дзвінок. - %s завершив(ла) дзвінок. + %s завершує дзвінок. %1$s зробив(ла) майбутню історію кімнати видимою для %2$s усіх співрозмовників, з моменту їх запрошення. усіх співрозмовників, з моменту їх приєднання. @@ -41,44 +36,33 @@ будь-кого. невідомо (%s). %1$s увімкнув(ла) наскрізне шифрування (%2$s) - %1$s запросив(ла) VoIP конференцію VoIP конференція розпочалась VoIP конференція завершилась - (аватар також змінено) %1$s прибрав(ла) назву кімнати %1$s прибрав(ла) тему кімнати %1$s оновив(ла) свій профіль %2$s %1$s надіслав(ла) запрошення %2$s приєднатися до кімнати %1$s прийняв(ла) запрошення у %2$s - ** Неможливо розшифрувати: %s ** Пристрій відправника не надіслав нам ключ для цього повідомлення. - Неможливо відредагувати Не вдалося надіслати повідомлення - Не вдалося завантажити зображення - Помилка мережі Помилка Matrix - Наразі неможливо переприєднатися до порожньої кімнати. - Адреса електронної пошти Номер телефону - %1$s та 1 інший %1$s та %2$d інші %1$s та %2$d інших - + - %s вдосконалили цю кімнату. - Повідомлення видалено %1$s видалили повідомлення Повідомлення видалено [причина: %1$s] - + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index b3de5910a5..08050b400d 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -208,4 +208,26 @@ %1$s 已加入 您已建立此討論 %1$s 已建立此討論 + 空的聊天室(曾為 %s) + + %1$s, %2$s, %3$s 與 %4$d 個其他 + + %1$s, %2$s, %3$s 與 %4$s + %1$s, %2$s 與 %3$s + 🎉 禁止所有伺服器參與!無法再使用此聊天室。 + 無變更。 + • 禁止伺服器符合 IP 文字。 + • 允許伺服器符合 IP 文字。 + • 伺服器符合 %s 已從允許清單中移除。 + • 允許伺服器符合 %s。 + • 伺服器符合 %s 已從禁止清單中移除。 + • 現在禁止伺服器符合 %s。 + 您為此聊天室變更了伺服器 ACL。 + %s 為此聊天是變更了伺服器 ACL。 + • 禁止伺服器符合 IP 文字。 + • 允許伺服器符合 IP 文字。 + • 已允許伺服器符合 %s。 + • 已禁止伺服器符合 %s。 + 您為此聊天是設定了伺服器 ACL。 + %s 為此聊天是設定了伺服器 ACL。 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index f77cd3203d..7a0fe1d735 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -246,7 +246,7 @@ %1$s removed %2$s as an address for this room. - %1$s removed %3$s as addresses for this room. + %1$s removed %2$s as addresses for this room. @@ -262,6 +262,33 @@ "%1$s removed the main address for this room." "You removed the main address for this room." + + %1$s added the alternative address %2$s for this room. + %1$s added the alternative addresses %2$s for this room. + + + + You added the alternative address %1$s for this room. + You added the alternative addresses %1$s for this room. + + + + %1$s removed the alternative address %2$s for this room. + %1$s removed the alternative addresses %2$s for this room. + + + + You removed the alternative address %1$s for this room. + You removed the alternative addresses %1$s for this room. + + + %1$s changed the alternative addresses for this room. + You changed the alternative addresses for this room. + %1$s changed the main and alternative addresses for this room. + You changed the main and alternative addresses for this room. + %1$s changed the addresses for this room. + You changed the addresses for this room. + "%1$s has allowed guests to join the room." "You have allowed guests to join the room." "%1$s has allowed guests to join here." diff --git a/multipicker/build.gradle b/multipicker/build.gradle index b6e500e493..7c29a5539f 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -43,8 +43,8 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:1.3.0-beta01" - implementation 'androidx.exifinterface:exifinterface:1.3.0' + implementation "androidx.fragment:fragment-ktx:1.3.0-beta01" + implementation 'androidx.exifinterface:exifinterface:1.3.1' // Log implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 63a3fad109..fc510e585c 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===83 +enum class===84 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/build.gradle b/vector/build.gradle index 037b049a76..7bb8ca187c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -17,7 +17,7 @@ androidExtensions { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 0 -ext.versionPatch = 11 +ext.versionPatch = 12 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -315,9 +315,8 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.0-alpha06" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version" - implementation 'androidx.constraintlayout:constraintlayout:2.0.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.0.0" implementation 'androidx.core:core-ktx:1.3.2' @@ -362,11 +361,11 @@ dependencies { implementation "io.arrow-kt:arrow-core:$arrow_version" // Pref - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' // UI implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'com.google.android.material:material:1.3.0-alpha02' + implementation 'com.google.android.material:material:1.3.0-alpha04' implementation 'me.gujun.android:span:1.7' implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:html:$markwon_version" @@ -374,7 +373,7 @@ dependencies { implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" - implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10' + implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // Custom Tab implementation 'androidx.browser:browser:1.2.0' @@ -411,6 +410,9 @@ dependencies { // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' + // Chat effects + implementation 'nl.dionsegijn:konfetti:1.2.5' + implementation 'com.github.jetradarmobile:android-snowfall:1.2.0' // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" @@ -418,7 +420,7 @@ dependencies { kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' // gplay flavor only - gplayImplementation('com.google.firebase:firebase-messaging:20.3.0') { + gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -441,6 +443,10 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' implementation 'me.dm7.barcodescanner:zxing:1.9.13' + // Emoji Keyboard + implementation 'com.vanniktech:emoji-material:0.7.0' + implementation 'com.vanniktech:emoji-google:0.7.0' + // TESTS testImplementation 'junit:junit:4.13' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/lint.xml b/vector/lint.xml index 4ac0f20e51..572f937406 100644 --- a/vector/lint.xml +++ b/vector/lint.xml @@ -41,6 +41,7 @@ + @@ -52,6 +53,9 @@ + + + diff --git a/vector/src/debug/res/layout/activity_test_linkify.xml b/vector/src/debug/res/layout/activity_test_linkify.xml index bbaadb20a2..7e625ad08c 100644 --- a/vector/src/debug/res/layout/activity_test_linkify.xml +++ b/vector/src/debug/res/layout/activity_test_linkify.xml @@ -4,7 +4,7 @@ android:id="@+id/test_linkify_coordinator" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/riot_secondary_text_color_status" + android:background="#7F70808D" tools:context=".features.debug.TestLinkifyActivity"> - - - - - - \ No newline at end of file diff --git a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt index 32888dafd7..1107737888 100644 --- a/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt +++ b/vector/src/gplay/java/im/vector/app/gplay/features/settings/troubleshoot/TestFirebaseToken.kt @@ -18,7 +18,7 @@ package im.vector.app.gplay.features.settings.troubleshoot import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AppCompatActivity -import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.startAddGoogleAccountIntent @@ -36,29 +36,33 @@ class TestFirebaseToken @Inject constructor(private val context: AppCompatActivi override fun perform(activityResultLauncher: ActivityResultLauncher) { status = TestStatus.RUNNING try { - FirebaseInstanceId.getInstance().instanceId + FirebaseMessaging.getInstance().token .addOnCompleteListener(context) { task -> if (!task.isSuccessful) { - val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage // Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated) - if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) - } else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) - } else if ("ACCOUNT_MISSING".equals(errorMsg)) { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) - quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { - override fun doFix() { - startAddGoogleAccountIntent(context, activityResultLauncher) - } + description = when (val errorMsg = task.exception?.localizedMessage ?: "Unknown") { + "SERVICE_NOT_AVAILABLE" -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg) + } + "TOO_MANY_REGISTRATIONS" -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg) + } + "ACCOUNT_MISSING" -> { + quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) { + override fun doFix() { + startAddGoogleAccountIntent(context, activityResultLauncher) + } + } + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg) + } + else -> { + stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) } - } else { - description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg) } status = TestStatus.FAILED } else { - task.result?.token?.let { token -> - val tok = token.substring(0, Math.min(8, token.length)) + "********************" + task.result?.let { token -> + val tok = token.take(8) + "********************" description = stringProvider.getString(R.string.settings_troubleshoot_test_fcm_success, tok) Timber.e("Retrieved FCM token success [$tok].") // Ensure it is well store in our local storage diff --git a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt index 913eab211d..f3bdcafb1c 100755 --- a/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt +++ b/vector/src/gplay/java/im/vector/app/push/fcm/FcmHelper.kt @@ -21,7 +21,7 @@ import android.widget.Toast import androidx.core.content.edit import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.firebase.iid.FirebaseInstanceId +import com.google.firebase.messaging.FirebaseMessaging import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultSharedPreferences @@ -71,14 +71,16 @@ object FcmHelper { // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' if (checkPlayServices(activity)) { try { - FirebaseInstanceId.getInstance().instanceId - .addOnSuccessListener(activity) { instanceIdResult -> - storeFcmToken(activity, instanceIdResult.token) + FirebaseMessaging.getInstance().token + .addOnSuccessListener { token -> + storeFcmToken(activity, token) if (registerPusher) { - pushersManager.registerPusherWithFcmKey(instanceIdResult.token) + pushersManager.registerPusherWithFcmKey(token) } } - .addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } + .addOnFailureListener { e -> + Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") + } } catch (e: Throwable) { Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e9bd03cb4b..bf839b807c 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -81,8 +81,9 @@ android:resource="@xml/shortcuts" /> - + - + + + + + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index ac80b0d691..bf341e38b7 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -347,11 +347,6 @@ SOFTWARE.
Copyright 2017 Gabriel Ittner. -
  • - Android-multipicker-library -
    - Copyright 2018 Kumar Bibek -
  • htmlcompressor
    @@ -390,6 +385,16 @@ SOFTWARE.
    Copyright 2018, Aleksandr Nikiforov
  • +
  • + Emoji +
    + Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors +
  • +
  • + JetradarMobile / android-snowfall +
    + Copyright 2016 JetRadar +
  •  Apache License
    @@ -576,5 +581,14 @@ Apache License
         
     
    +
    +    ISC License
    +    
  • + DanielMartinus / Konfetti +
    + Copyright (c) 2017 Dion Segijn +
  • +
    + diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 5be313d719..921e8c0780 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -36,6 +36,8 @@ import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen +import com.vanniktech.emoji.EmojiManager +import com.vanniktech.emoji.google.GoogleEmojiProvider import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DaggerVectorComponent import im.vector.app.core.di.HasVectorInjector @@ -184,6 +186,8 @@ class VectorApplication : addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_ON) }) + + EmojiManager.install(GoogleEmojiProvider()) } private fun enableStrictModeIfNeeded() { diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 32c98922fb..87ab875746 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -36,6 +36,7 @@ import im.vector.app.features.crypto.recover.BootstrapMigrateBackupFragment import im.vector.app.features.crypto.recover.BootstrapSaveRecoveryKeyFragment import im.vector.app.features.crypto.recover.BootstrapSetupRecoveryKeyFragment import im.vector.app.features.crypto.recover.BootstrapWaitingFragment +import im.vector.app.features.crypto.verification.QuadSLoadingFragment import im.vector.app.features.crypto.verification.cancel.VerificationCancelFragment import im.vector.app.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.app.features.crypto.verification.choose.VerificationChooseMethodFragment @@ -63,7 +64,6 @@ import im.vector.app.features.login.LoginResetPasswordSuccessFragment import im.vector.app.features.login.LoginServerSelectionFragment import im.vector.app.features.login.LoginServerUrlFormFragment import im.vector.app.features.login.LoginSignUpSignInSelectionFragment -import im.vector.app.features.login.LoginSignUpSignInSsoFragment import im.vector.app.features.login.LoginSplashFragment import im.vector.app.features.login.LoginWaitForEmailFragment import im.vector.app.features.login.LoginWebFragment @@ -83,6 +83,7 @@ import im.vector.app.features.roomprofile.RoomProfileFragment import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment +import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment @@ -228,11 +229,6 @@ interface FragmentModule { @FragmentKey(LoginSignUpSignInSelectionFragment::class) fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment - @Binds - @IntoMap - @FragmentKey(LoginSignUpSignInSsoFragment::class) - fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment - @Binds @IntoMap @FragmentKey(LoginSplashFragment::class) @@ -363,6 +359,11 @@ interface FragmentModule { @FragmentKey(RoomSettingsFragment::class) fun bindRoomSettingsFragment(fragment: RoomSettingsFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomAliasFragment::class) + fun bindRoomAliasFragment(fragment: RoomAliasFragment): Fragment + @Binds @IntoMap @FragmentKey(RoomMemberProfileFragment::class) @@ -418,6 +419,11 @@ interface FragmentModule { @FragmentKey(VerificationCancelFragment::class) fun bindVerificationCancelFragment(fragment: VerificationCancelFragment): Fragment + @Binds + @IntoMap + @FragmentKey(QuadSLoadingFragment::class) + fun bindQuadSLoadingFragment(fragment: QuadSLoadingFragment): Fragment + @Binds @IntoMap @FragmentKey(VerificationNotMeFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 818a32fca3..f56a6a3d70 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -67,6 +67,9 @@ import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.roommemberprofile.RoomMemberProfileActivity import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet import im.vector.app.features.roomprofile.RoomProfileActivity +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity @@ -153,6 +156,9 @@ interface ScreenComponent { fun inject(bottomSheet: ViewEditHistoryBottomSheet) fun inject(bottomSheet: DisplayReadReceiptsBottomSheet) fun inject(bottomSheet: RoomListQuickActionsBottomSheet) + fun inject(bottomSheet: RoomAliasBottomSheet) + fun inject(bottomSheet: RoomHistoryVisibilityBottomSheet) + fun inject(bottomSheet: RoomJoinRuleBottomSheet) fun inject(bottomSheet: VerificationBottomSheet) fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) fun inject(bottomSheet: DeviceListBottomSheet) diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 28f3a52efa..273a142ff1 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -59,6 +59,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import javax.inject.Singleton @@ -127,6 +128,8 @@ interface VectorComponent { fun rawService(): RawService + fun homeServerHistoryService(): HomeServerHistoryService + fun bugReporter(): BugReporter fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt index 1d7cd33241..77cad0ae73 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt @@ -33,6 +33,7 @@ import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session @@ -85,6 +86,12 @@ abstract class VectorModule { fun providesRawService(matrix: Matrix): RawService { return matrix.rawService() } + + @Provides + @JvmStatic + fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { + return matrix.homeServerHistoryService() + } } @Binds diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index 7ae8bc9c2e..bed2e0b850 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -35,6 +35,9 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.reactions.EmojiChooserViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel import im.vector.app.features.userdirectory.UserListSharedActionViewModel @Module @@ -105,6 +108,21 @@ interface ViewModelModule { @ViewModelKey(RoomListQuickActionsSharedActionViewModel::class) fun bindRoomListQuickActionsSharedActionViewModel(viewModel: RoomListQuickActionsSharedActionViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(RoomAliasBottomSheetSharedActionViewModel::class) + fun bindRoomAliasBottomSheetSharedActionViewModel(viewModel: RoomAliasBottomSheetSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomHistoryVisibilitySharedActionViewModel::class) + fun bindRoomHistoryVisibilitySharedActionViewModel(viewModel: RoomHistoryVisibilitySharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomJoinRuleSharedActionViewModel::class) + fun bindRoomJoinRuleSharedActionViewModel(viewModel: RoomJoinRuleSharedActionViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(RoomDirectorySharedActionViewModel::class) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt index e28bec6874..80792648f6 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetActionItem.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isInvisible @@ -43,6 +44,13 @@ abstract class BottomSheetActionItem : VectorEpoxyModel() var destructive: Boolean = false @EpoxyAttribute - var listener: View.OnClickListener? = null + var listener: ClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - holder.view.setOnClickListener(listener) + holder.view.onClick(listener) if (listener == null) { holder.view.isClickable = false } diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt index fdbe9f7f94..99acd6cb36 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/ProfileItemExtensions.kt @@ -59,9 +59,7 @@ fun EpoxyController.buildProfileAction( accessoryRes(accessory) accessoryMatrixItem(accessoryMatrixItem) avatarRenderer(avatarRenderer) - listener { _ -> - action?.invoke() - } + listener(action) } if (divider) { diff --git a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt index 05b70def3d..33e7199334 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt @@ -57,3 +57,15 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_searc return@OnTouchListener false }) } + +/** + * Update the edit text value, only if necessary and move the cursor to the end of the text + */ +fun EditText.setTextSafe(value: String?) { + if (value != null && text.toString() != value) { + setText(value) + // To fix jumping cursor to the start https://github.com/airbnb/epoxy/issues/426 + // Note: there is still a known bug if deleting char in the middle of the text, by long pressing on the backspace button. + setSelection(value.length) + } +} diff --git a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt index 71bd3ccc05..9a7cf1eb76 100644 --- a/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt +++ b/vector/src/main/java/im/vector/app/core/glide/VectorGlideModelLoader.kt @@ -28,7 +28,6 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.media.ImageContentRenderer import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.file.FileService import timber.log.Timber import java.io.File import java.io.IOException @@ -110,11 +109,9 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde } // Use the file vector service, will avoid flickering and redownload after upload fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - mimeType = data.mimeType, - id = data.eventId, - url = data.url, fileName = data.filename, + mimeType = data.mimeType, + url = data.url, elementToDecrypt = data.elementToDecrypt, callback = object : MatrixCallback { override fun onSuccess(data: File) { diff --git a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt index c8a2bf65d5..1299f4086b 100644 --- a/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt +++ b/vector/src/main/java/im/vector/app/core/intent/VectorMimeType.kt @@ -21,6 +21,7 @@ import android.net.Uri import android.webkit.MimeTypeMap import im.vector.app.core.utils.getFileExtension import timber.log.Timber +import java.util.Locale /** * Returns the mimetype from a uri. @@ -44,7 +45,7 @@ fun getMimeTypeFromUri(context: Context, uri: Uri): String? { if (null != mimeType) { // the mimetype is sometimes in uppercase. - mimeType = mimeType.toLowerCase() + mimeType = mimeType.toLowerCase(Locale.ROOT) } } catch (e: Exception) { Timber.e(e, "Failed to open resource input stream") diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt index 002dfcf068..d6f43beaf7 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorViewModel.kt @@ -43,7 +43,7 @@ abstract class VectorViewModel Single.toAsync(stateReducer: S.(Async) -> S): Single> { setState { stateReducer(Loading()) } return map { Success(it) as Async } @@ -56,7 +56,7 @@ abstract class VectorViewModel Observable.toAsync(stateReducer: S.(Async) -> S): Observable> { setState { stateReducer(Loading()) } return map { Success(it) as Async } diff --git a/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt b/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt index 7ab2271c57..f14c9b834d 100644 --- a/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt +++ b/vector/src/main/java/im/vector/app/core/resources/ResourceUtils.kt @@ -20,17 +20,11 @@ import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap import im.vector.app.core.utils.getFileExtension +import org.matrix.android.sdk.api.util.MimeTypes +import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType import timber.log.Timber import java.io.InputStream -/** - * Mime types - */ -const val MIME_TYPE_JPEG = "image/jpeg" -const val MIME_TYPE_JPG = "image/jpg" -const val MIME_TYPE_IMAGE_ALL = "image/*" -const val MIME_TYPE_ALL_CONTENT = "*/*" - data class Resource( var mContentStream: InputStream? = null, var mMimeType: String? = null @@ -55,7 +49,7 @@ data class Resource( * @return true if the opened resource is a jpeg one. */ fun isJpegResource(): Boolean { - return MIME_TYPE_JPEG == mMimeType || MIME_TYPE_JPG == mMimeType + return mMimeType.normalizeMimeType() == MimeTypes.Jpeg } } diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt new file mode 100644 index 0000000000..da136fb072 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGeneric.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.bottomsheet + +import android.os.Bundle +import android.view.View +import androidx.annotation.CallSuper +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import javax.inject.Inject + +/** + * Generic Bottom sheet with actions + */ +abstract class BottomSheetGeneric : + VectorBaseBottomSheetDialogFragment(), + BottomSheetGenericController.Listener { + + @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView + + final override val showExpanded = true + + final override fun getLayoutResId() = R.layout.bottom_sheet_generic_list + + abstract fun getController(): BottomSheetGenericController + + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recyclerView.configureWith(getController(), viewPool = sharedViewPool, hasFixedSize = false, disableItemAnimation = true) + getController().listener = this + } + + @CallSuper + override fun onDestroyView() { + recyclerView.cleanup() + getController().listener = null + super.onDestroyView() + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericAction.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericAction.kt new file mode 100644 index 0000000000..da48accf35 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericAction.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.bottomsheet + +import androidx.annotation.DrawableRes +import im.vector.app.core.epoxy.bottomsheet.BottomSheetActionItem_ +import im.vector.app.core.platform.VectorSharedAction + +/** + * Parent class for a bottom sheet action + */ +open class BottomSheetGenericAction( + open val title: String, + @DrawableRes open val iconResId: Int, + open val isSelected: Boolean, + open val destructive: Boolean +) : VectorSharedAction { + + fun toBottomSheetItem(): BottomSheetActionItem_ { + return BottomSheetActionItem_().apply { + id("action_$title") + iconRes(iconResId) + text(title) + selected(isSelected) + destructive(destructive) + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt new file mode 100644 index 0000000000..67347c3220 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericController.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.ui.bottomsheet + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.epoxy.dividerItem + +/** + * Epoxy controller for generic bottom sheet actions + */ +abstract class BottomSheetGenericController + : TypedEpoxyController() { + + var listener: Listener? = null + + abstract fun getTitle(): String? + + open fun getSubTitle(): String? = null + + abstract fun getActions(state: State): List + + override fun buildModels(state: State?) { + state ?: return + // Title + getTitle()?.let { title -> + bottomSheetTitleItem { + id("title") + title(title) + subTitle(getSubTitle()) + } + + dividerItem { + id("title_separator") + } + } + // Actions + val actions = getActions(state) + val showIcons = actions.any { it.iconResId > 0 } + actions.forEach { action -> + action.toBottomSheetItem() + .showIcon(showIcons) + .listener(View.OnClickListener { listener?.didSelectAction(action) }) + .addTo(this) + } + } + + interface Listener { + fun didSelectAction(action: Action) + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericSharedActionViewModel.kt new file mode 100644 index 0000000000..49147b954a --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericSharedActionViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.app.core.ui.bottomsheet + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel + +/** + * Activity shared view model to handle bottom sheet quick actions + */ +abstract class BottomSheetGenericSharedActionViewModel : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt new file mode 100644 index 0000000000..38c81a7ef6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.bottomsheet + +import com.airbnb.mvrx.MvRxState + +abstract class BottomSheetGenericState : MvRxState diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt new file mode 100644 index 0000000000..6cc2c4c981 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetGenericViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.ui.bottomsheet + +import com.airbnb.mvrx.MvRxState +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel + +abstract class BottomSheetGenericViewModel(initialState: State) : + VectorViewModel(initialState) { + + override fun handle(action: EmptyAction) { + // No op + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt new file mode 100644 index 0000000000..27fb634480 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/bottomsheet/BottomSheetTitleItem.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.app.core.ui.bottomsheet + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide + +/** + * A title for bottom sheet, with an optional subtitle. It does not include the bottom separator. + */ +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_title) +abstract class BottomSheetTitleItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var title: String + + @EpoxyAttribute + var subTitle: String? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.title.text = title + holder.subtitle.setTextOrHide(subTitle) + } + + class Holder : VectorEpoxyHolder() { + val title by bind(R.id.itemBottomSheetTitleTitle) + val subtitle by bind(R.id.itemBottomSheetTitleSubtitle) + } +} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt index 169f24520b..3c48637e74 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/JumpToReadMarkerView.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.core.ui.views diff --git a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt index 8a908ad1d4..06bdeb9277 100644 --- a/vector/src/main/java/im/vector/app/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/app/core/utils/DataSource.kt @@ -44,7 +44,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD } override fun post(value: T) { - behaviorRelay.accept(value) + behaviorRelay.accept(value!!) } private fun createRelay(): BehaviorRelay { @@ -68,6 +68,6 @@ open class PublishDataSource : MutableDataSource { } override fun post(value: T) { - publishRelay.accept(value) + publishRelay.accept(value!!) } } diff --git a/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt index a5e0005c2a..bb38150797 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Debouncer.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.core.utils diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 4c6aa51348..45db8ea91d 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -48,6 +48,10 @@ import okio.buffer import okio.sink import okio.source import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.util.MimeTypes +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo import timber.log.Timber import java.io.File import java.io.FileInputStream @@ -138,7 +142,7 @@ fun openFileSelection(activity: Activity, fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection) fileIntent.addCategory(Intent.CATEGORY_OPENABLE) - fileIntent.type = "*/*" + fileIntent.type = MimeTypes.Any try { activityResultLauncher @@ -182,7 +186,7 @@ fun openCamera(activity: Activity, titlePrefix: String, requestCode: Int): Strin // The Galaxy S not only requires the name of the file to output the image to, but will also not // set the mime type of the picture it just took (!!!). We assume that the Galaxy S takes image/jpegs // so the attachment uploader doesn't freak out about there being no mimetype in the content database. - values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + values.put(MediaStore.Images.Media.MIME_TYPE, MimeTypes.Jpeg) var dummyUri: Uri? = null try { dummyUri = activity.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) @@ -344,10 +348,10 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) } val externalContentUri = when { - mediaMimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.startsWith("audio/") == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI } val uri = context.contentResolver.insert(externalContentUri, values) @@ -365,7 +369,7 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String notificationUtils.buildDownloadFileNotification( uri, filename, - mediaMimeType ?: "application/octet-stream" + mediaMimeType ?: MimeTypes.OctetStream ).let { notification -> notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) } @@ -385,10 +389,10 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str GlobalScope.launch(Dispatchers.IO) { val dest = when { - mediaMimeType?.startsWith("image/") == true -> Environment.DIRECTORY_PICTURES - mediaMimeType?.startsWith("video/") == true -> Environment.DIRECTORY_MOVIES - mediaMimeType?.startsWith("audio/") == true -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS + mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES + mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES + mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC + else -> Environment.DIRECTORY_DOWNLOADS } val downloadDir = Environment.getExternalStoragePublicDirectory(dest) try { @@ -405,7 +409,7 @@ private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: Str savedFile.name, title, true, - mediaMimeType ?: "application/octet-stream", + mediaMimeType ?: MimeTypes.OctetStream, savedFile.absolutePath, savedFile.length(), true) diff --git a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt index ab99ba61bd..aa36dd0959 100644 --- a/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/FileUtils.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.content.Context import timber.log.Timber import java.io.File +import java.util.Locale // Implementation should return true in case of success typealias ActionOnFile = (file: File) -> Boolean @@ -113,7 +114,7 @@ fun getFileExtension(fileUri: String): String? { val ext = filename.substring(dotPos + 1) if (ext.isNotBlank()) { - return ext.toLowerCase() + return ext.toLowerCase(Locale.ROOT) } } } diff --git a/vector/src/main/java/im/vector/app/core/utils/Handler.kt b/vector/src/main/java/im/vector/app/core/utils/Handler.kt index c7ec97f53e..fe8760a522 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Handler.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Handler.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.core.utils diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt index 9c9d8f8017..4e8dcaacb7 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt @@ -23,6 +23,9 @@ import im.vector.lib.multipicker.entity.MultiPickerFileType import im.vector.lib.multipicker.entity.MultiPickerImageType import im.vector.lib.multipicker.entity.MultiPickerVideoType import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeAudio +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeVideo import timber.log.Timber fun MultiPickerContactType.toContactAttachment(): ContactAttachment { @@ -59,10 +62,10 @@ fun MultiPickerAudioType.toContentAttachmentData(): ContentAttachmentData { private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type { return when { - mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE - mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO - mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO - else -> ContentAttachmentData.Type.FILE + mimeType?.isMimeTypeImage() == true -> ContentAttachmentData.Type.IMAGE + mimeType?.isMimeTypeVideo() == true -> ContentAttachmentData.Type.VIDEO + mimeType?.isMimeTypeAudio() == true -> ContentAttachmentData.Type.AUDIO + else -> ContentAttachmentData.Type.FILE } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt index bd13c0dac4..e35ab96365 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt @@ -17,11 +17,19 @@ package im.vector.app.features.attachments import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes + +private val listOfPreviewableMimeTypes = listOf( + MimeTypes.Jpeg, + MimeTypes.BadJpg, + MimeTypes.Png, + MimeTypes.Gif +) fun ContentAttachmentData.isPreviewable(): Boolean { // For now the preview only supports still image return type == ContentAttachmentData.Type.IMAGE - && listOf("image/jpeg", "image/png", "image/jpg").contains(getSafeMimeType() ?: "") + && listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "") } data class GroupedContentAttachmentData( diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index ba0250724c..f67b0946cc 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -59,7 +59,6 @@ data class AttachmentsPreviewArgs( ) : Parcelable class AttachmentsPreviewFragment @Inject constructor( - val viewModelFactory: AttachmentsPreviewViewModel.Factory, private val attachmentMiniaturePreviewController: AttachmentMiniaturePreviewController, private val attachmentBigPreviewController: AttachmentBigPreviewController, private val colorProvider: ColorProvider diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt index 59a0937d89..28d617e613 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewViewModel.kt @@ -17,31 +17,12 @@ package im.vector.app.features.attachments.preview -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -class AttachmentsPreviewViewModel @AssistedInject constructor(@Assisted initialState: AttachmentsPreviewViewState) +class AttachmentsPreviewViewModel(initialState: AttachmentsPreviewViewState) : VectorViewModel(initialState) { - @AssistedInject.Factory - interface Factory { - fun create(initialState: AttachmentsPreviewViewState): AttachmentsPreviewViewModel - } - - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: AttachmentsPreviewViewState): AttachmentsPreviewViewModel? { - val fragment: AttachmentsPreviewFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) - } - } - override fun handle(action: AttachmentsPreviewAction) { when (action) { is AttachmentsPreviewAction.SetCurrentAttachment -> handleSetCurrentAttachment(action) diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt index bd06f8cf0b..853f9f8997 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/Extensions.kt @@ -17,12 +17,14 @@ package im.vector.app.features.attachments.preview import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes +import org.matrix.android.sdk.api.util.MimeTypes.isMimeTypeImage /** * All images are editable, expect Gif */ fun ContentAttachmentData.isEditable(): Boolean { return type == ContentAttachmentData.Type.IMAGE - && getSafeMimeType()?.startsWith("image/") == true - && getSafeMimeType() != "image/gif" + && getSafeMimeType()?.isMimeTypeImage() == true + && getSafeMimeType() != MimeTypes.Gif } diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index db429f9e58..66d88f149a 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -44,7 +44,9 @@ enum class Command(val command: String, val parameters: String, @StringRes val d POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll), SHRUG("/shrug", "", R.string.command_description_shrug), PLAIN("/plain", "", R.string.command_description_plain), - DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session); + DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session), + CONFETTI("/confetti", "", R.string.command_confetti), + SNOW("/snow", "", R.string.command_snow); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 94de6bf265..d458751364 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -18,6 +18,7 @@ package im.vector.app.features.command import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn +import im.vector.app.features.home.room.detail.ChatEffect import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber @@ -291,6 +292,14 @@ object CommandParser { Command.DISCARD_SESSION.command -> { ParsedCommand.DiscardSession } + Command.CONFETTI.command -> { + val message = textMessage.substring(Command.CONFETTI.command.length).trim() + ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) + } + Command.SNOW.command -> { + val message = textMessage.substring(Command.SNOW.command.length).trim() + ParsedCommand.SendChatEffect(ChatEffect.SNOW, message) + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) diff --git a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt index 2f8531929a..d17faeafb8 100644 --- a/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/app/features/command/ParsedCommand.kt @@ -16,6 +16,7 @@ package im.vector.app.features.command +import im.vector.app.features.home.room.detail.ChatEffect import org.matrix.android.sdk.api.session.identity.ThreePid /** @@ -55,4 +56,5 @@ sealed class ParsedCommand { class SendShrug(val message: CharSequence) : ParsedCommand() class SendPoll(val question: String, val options: List) : ParsedCommand() object DiscardSession : ParsedCommand() + class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/QuadSLoadingFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/QuadSLoadingFragment.kt new file mode 100644 index 0000000000..a0ab1c86a7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/QuadSLoadingFragment.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.crypto.verification + +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseFragment +import javax.inject.Inject + +class QuadSLoadingFragment @Inject constructor() : VectorBaseFragment() { + override fun getLayoutResId() = R.layout.fragment_progress +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt index a32a9de97f..a5142ad8bf 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt @@ -31,5 +31,6 @@ sealed class VerificationAction : VectorViewModelAction { object SkipVerification : VerificationAction() object VerifyFromPassphrase : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() + object CancelledFromSsss : VerificationAction() object SecuredStorageHasBeenReset : VerificationAction() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 35ea96de6f..a9b76366df 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -155,6 +155,8 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { // all have been reset, so we are verified? viewModel.handle(VerificationAction.SecuredStorageHasBeenReset) } + } else { + viewModel.handle(VerificationAction.CancelledFromSsss) } } @@ -209,6 +211,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { return@withState } + if (state.selfVerificationMode && state.verifyingFrom4S) { + showFragment(QuadSLoadingFragment::class, Bundle()) + return@withState + } if (state.selfVerificationMode && state.verifiedFromPrivateKeys) { showFragment(VerificationConclusionFragment::class, Bundle().apply { putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe)) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index aa20a9a992..23ed9b6483 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session @@ -70,6 +71,7 @@ data class VerificationBottomSheetViewState( // true when we display the loading and we wait for the other (incoming request) val selfVerificationMode: Boolean = false, val verifiedFromPrivateKeys: Boolean = false, + val verifyingFrom4S: Boolean = false, val isMe: Boolean = false, val currentDeviceCanCrossSign: Boolean = false, val userWantsToCancel: Boolean = false, @@ -170,7 +172,9 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } else { // if the verification is already done you can't cancel anymore - if (state.pendingRequest.invoke()?.cancelConclusion != null || state.sasTransactionState is VerificationTxState.TerminalTxState) { + if (state.pendingRequest.invoke()?.cancelConclusion != null + || state.sasTransactionState is VerificationTxState.TerminalTxState + || state.verifyingFrom4S) { // you cannot cancel anymore } else { setState { @@ -346,6 +350,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) } is VerificationAction.VerifyFromPassphrase -> { + setState { copy(verifyingFrom4S = true) } _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore) } is VerificationAction.GotResultFromSsss -> { @@ -354,56 +359,73 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( VerificationAction.SecuredStorageHasBeenReset -> { if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) { setState { - copy(quadSHasBeenReset = true) + copy(quadSHasBeenReset = true, verifyingFrom4S = false) } } Unit } + VerificationAction.CancelledFromSsss -> { + setState { + copy(verifyingFrom4S = false) + } + } }.exhaustive } private fun handleSecretBackFromSSSS(action: VerificationAction.GotResultFromSsss) { - try { - action.cypherData.fromBase64().inputStream().use { ins -> - val res = session.loadSecureSecret>(ins, action.alias) - val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( - res?.get(MASTER_KEY_SSSS_NAME), - res?.get(USER_SIGNING_KEY_SSSS_NAME), - res?.get(SELF_SIGNING_KEY_SSSS_NAME) - ) - if (trustResult.isVerified()) { - // Sign this device and upload the signature - session.sessionParams.deviceId?.let { deviceId -> - session.cryptoService() - .crossSigningService().trustDevice(deviceId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - Timber.w(failure, "Failed to sign my device after recovery") - } - }) - } + viewModelScope.launch(Dispatchers.IO) { + try { + action.cypherData.fromBase64().inputStream().use { ins -> + val res = session.loadSecureSecret>(ins, action.alias) + val trustResult = session.cryptoService().crossSigningService().checkTrustFromPrivateKeys( + res?.get(MASTER_KEY_SSSS_NAME), + res?.get(USER_SIGNING_KEY_SSSS_NAME), + res?.get(SELF_SIGNING_KEY_SSSS_NAME) + ) + if (trustResult.isVerified()) { + // Sign this device and upload the signature + session.sessionParams.deviceId?.let { deviceId -> + session.cryptoService() + .crossSigningService().trustDevice(deviceId, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + Timber.w(failure, "Failed to sign my device after recovery") + } + }) + } - setState { - copy(verifiedFromPrivateKeys = true) - } + setState { + copy( + verifyingFrom4S = false, + verifiedFromPrivateKeys = true + ) + } - // try to get keybackup key - } else { - // POP UP something - _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(stringProvider.getString(R.string.error_failed_to_import_keys))) + // try the keybackup + tentativeRestoreBackup(res) + } else { + setState { + copy( + verifyingFrom4S = false + ) + } + // POP UP something + _viewEvents.post(VerificationBottomSheetViewEvents.ModalError(stringProvider.getString(R.string.error_failed_to_import_keys))) + } } - - // try the keybackup - tentativeRestoreBackup(res) - Unit + } catch (failure: Throwable) { + setState { + copy( + verifyingFrom4S = false + ) + } + _viewEvents.post( + VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage ?: stringProvider.getString(R.string.unexpected_error))) } - } catch (failure: Throwable) { - _viewEvents.post( - VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage ?: stringProvider.getString(R.string.unexpected_error))) } } private fun tentativeRestoreBackup(res: Map?) { - viewModelScope.launch(Dispatchers.IO) { + GlobalScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { Timber.v("## Keybackup secret not restored from SSSS") diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt index c9ad23f1a9..b59b24fe55 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt @@ -27,6 +27,9 @@ import im.vector.app.core.epoxy.onClick @EpoxyModelClass(layout = R.layout.item_settings_continue_cancel) abstract class SettingsContinueCancelItem : EpoxyModelWithHolder() { + @EpoxyAttribute + var continueText: String? = null + @EpoxyAttribute var continueOnClick: ClickListener? = null @@ -37,6 +40,8 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder { - mxSession.getTerms(TermsService.ServiceType.IdentityService, baseUrl, it) - } + val data = mxSession.getTerms(TermsService.ServiceType.IdentityService, baseUrl) // has all been accepted? val resp = data.serverResponse diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index 12538d314a..68e2e6b371 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -26,6 +26,7 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_form_text_input) @@ -65,9 +66,7 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputLayout.error = errorMessage // Update only if text is different and value is not null - if (value != null && holder.textInputEditText.text.toString() != value) { - holder.textInputEditText.setText(value) - } + holder.textInputEditText.setTextSafe(value) holder.textInputEditText.isEnabled = enabled inputType?.let { holder.textInputEditText.inputType = it } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt index eadae3ba0c..08fc435e11 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt @@ -26,6 +26,7 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_form_text_input_with_button) @@ -61,9 +62,7 @@ abstract class FormEditTextWithButtonItem : VectorEpoxyModel() { holder.switchView.isEnabled = enabled + holder.switchView.setOnCheckedChangeListener(null) holder.switchView.isChecked = switchChecked - holder.switchView.setOnCheckedChangeListener { _, isChecked -> listener?.invoke(isChecked) } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 7dde0edf32..e1837ccb1b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -162,11 +162,27 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet private fun handleIntent(intent: Intent?) { intent?.dataString?.let { deepLink -> - if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let + val resolvedLink = when { + deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink + deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { + // This is a bit ugly, but for now just convert to matrix.to link for compatibility + when { + deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) + deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length) + else -> null + }?.let { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) + } + } + else -> null + } - permalinkHandler.launch(this, deepLink, + permalinkHandler.launch( + context = this, + deepLink = resolvedLink, navigationInterceptor = this, - buildTask = true) + buildTask = true + ) // .delay(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { isHandled -> @@ -345,11 +361,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } - R.id.menu_home_setting -> { + R.id.menu_home_setting -> { navigator.openSettings(this) return true } @@ -390,5 +406,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet putExtra(MvRx.KEY_ARG, args) } } + + private const val ROOM_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" + private const val USER_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 680ec17415..90d128320b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -71,12 +71,18 @@ class HomeActivityViewModel @AssistedInject constructor( private var onceTrusted = false init { + cleanupFiles() observeInitialSync() mayBeInitializeCrossSigning() checkSessionPushIsOn() observeCrossSigningReset() } + private fun cleanupFiles() { + // Mitigation: delete all cached decrypted files each time the application is started. + activeSessionHolder.getSafeActiveSession()?.fileService()?.clearDecryptedCache() + } + private fun observeCrossSigningReset() { val safeActiveSession = activeSessionHolder.getSafeActiveSession() ?: return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt new file mode 100644 index 0000000000..e7136762d5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail + +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject + +enum class ChatEffect { + CONFETTI, + SNOW +} + +fun ChatEffect.toMessageType(): String { + return when (this) { + ChatEffect.CONFETTI -> MessageType.MSGTYPE_CONFETTI + ChatEffect.SNOW -> MessageType.MSGTYPE_SNOW + } +} + +/** + * A simple chat effect manager helper class + * Used by the view model to know if an event that become visible should trigger a chat effect. + * It also manages effect duration and some cool down, for example if an effect is currently playing, + * any other trigger will be ignored + * For now it uses visibility callback to check for an effect (that means that a fail to decrypt event - more + * precisely an event decrypted with a few delay won't trigger an effect; it's acceptable) + * Events that are more that 10s old won't trigger effects + */ +class ChatEffectManager @Inject constructor() { + + interface Delegate { + fun stopEffects() + fun shouldStartEffect(effect: ChatEffect) + } + + var delegate: Delegate? = null + + private var stopTimer: Timer? = null + + // an in memory store to avoid trigger twice for an event (quick close/open timeline) + private val alreadyPlayed = mutableListOf() + + fun checkForEffect(event: TimelineEvent) { + val age = event.root.ageLocalTs ?: 0 + val now = System.currentTimeMillis() + // messages older than 10s should not trigger any effect + if ((now - age) >= 10_000) return + val content = event.root.getClearContent()?.toModel() ?: return + val effect = findEffect(content, event) + if (effect != null) { + synchronized(this) { + if (hasAlreadyPlayed(event)) return + markAsAlreadyPlayed(event) + // there is already an effect playing, so ignore + if (stopTimer != null) return + delegate?.shouldStartEffect(effect) + stopTimer = Timer().apply { + schedule(object : TimerTask() { + override fun run() { + stopEffect() + } + }, 6_000) + } + } + } + } + + fun dispose() { + stopTimer?.cancel() + stopTimer = null + alreadyPlayed.clear() + } + + @Synchronized + private fun stopEffect() { + stopTimer = null + delegate?.stopEffects() + } + + private fun markAsAlreadyPlayed(event: TimelineEvent) { + alreadyPlayed.add(event.eventId) + // also put the tx id as fast way to deal with local echo + event.root.unsignedData?.transactionId?.let { + alreadyPlayed.add(it) + } + } + + private fun hasAlreadyPlayed(event: TimelineEvent): Boolean { + return alreadyPlayed.contains(event.eventId) + || (event.root.unsignedData?.transactionId?.let { alreadyPlayed.contains(it) } ?: false) + } + + private fun findEffect(content: MessageContent, event: TimelineEvent): ChatEffect? { + return when (content.msgType) { + MessageType.MSGTYPE_CONFETTI -> ChatEffect.CONFETTI + MessageType.MSGTYPE_SNOW -> ChatEffect.SNOW + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_TEXT -> { + event.root.getClearContent().toModel()?.body + ?.let { text -> + when { + EMOJIS_FOR_CONFETTI.any { text.contains(it) } -> ChatEffect.CONFETTI + EMOJIS_FOR_SNOW.any { text.contains(it) } -> ChatEffect.SNOW + else -> null + } + } + } + else -> null + } + } + + companion object { + private val EMOJIS_FOR_CONFETTI = listOf( + "🎉", + "🎊" + ) + private val EMOJIS_FOR_SNOW = listOf( + "⛄️", + "☃️", + "❄️" + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 8891218a11..e034e373f3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -98,4 +98,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class SetAvatarAction(val newAvatarUri: Uri, val newAvatarFileName: String) : RoomDetailAction() object QuickActionSetTopic : RoomDetailAction() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val transitionView: View?) : RoomDetailAction() + + // Preview URL + data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 3f5e476a5e..45efe1e15a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -21,6 +21,7 @@ import android.app.Activity import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration +import android.graphics.Color import android.graphics.Typeface import android.net.Uri import android.os.Build @@ -48,12 +49,13 @@ import androidx.core.text.toSpannable import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import butterknife.BindView +import androidx.transition.TransitionManager import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.addGlidePreloader @@ -69,6 +71,7 @@ import com.airbnb.mvrx.withState import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.jakewharton.rxbinding3.widget.textChanges +import com.vanniktech.emoji.EmojiPopup import im.vector.app.R import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper @@ -140,6 +143,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -165,8 +169,10 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* -import kotlinx.android.synthetic.main.merge_composer_layout.view.* +import kotlinx.android.synthetic.main.composer_layout.view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import nl.dionsegijn.konfetti.models.Shape +import nl.dionsegijn.konfetti.models.Size import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.commonmark.parser.Parser import org.matrix.android.sdk.api.MatrixCallback @@ -174,7 +180,6 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -185,7 +190,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent -import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -194,7 +198,6 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import timber.log.Timber @@ -289,8 +292,6 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils - @BindView(R.id.composerLayout) - lateinit var composerLayout: TextComposerView private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private var lockSendButton = false @@ -311,6 +312,7 @@ class RoomDetailFragment @Inject constructor( setupActiveCallView() setupJumpToBottomView() setupConfBannerView() + setupEmojiPopup() roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) @@ -381,6 +383,8 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> navigator.openBigImageViewer(requireActivity(), it.view, item) } + is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) + RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() }.exhaustive } @@ -389,6 +393,34 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleChatEffect(chatEffect: ChatEffect) { + when (chatEffect) { + ChatEffect.CONFETTI -> { + viewKonfetti.isVisible = true + viewKonfetti.build() + .addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA) + .setDirection(0.0, 359.0) + .setSpeed(2f, 5f) + .setFadeOutEnabled(true) + .setTimeToLive(2000L) + .addShapes(Shape.Square, Shape.Circle) + .addSizes(Size(12)) + .setPosition(-50f, viewKonfetti.width + 50f, -50f, -50f) + .streamFor(150, 3000L) + } + ChatEffect.SNOW -> { + viewSnowFall.isVisible = true + viewSnowFall.restartFalling() + } + } + } + private fun handleStopChatEffects() { + TransitionManager.beginDelayedTransition(rootConstraintLayout) + viewSnowFall.isVisible = false + // when gone the effect is a bit buggy + viewKonfetti.isInvisible = true + } + override fun onImageReady(uri: Uri?) { uri ?: return roomDetailViewModel.handle( @@ -478,6 +510,20 @@ class RoomDetailFragment @Inject constructor( } } + private fun setupEmojiPopup() { + val emojiPopup = EmojiPopup + .Builder + .fromRootView(rootConstraintLayout) + .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) + .setOnEmojiPopupShownListener { composerLayout?.composerEmojiButton?.setImageResource(R.drawable.ic_keyboard) } + .setOnEmojiPopupDismissListener { composerLayout?.composerEmojiButton?.setImageResource(R.drawable.ic_insert_emoji) } + .build(composerLayout.composerEditText) + + composerLayout.composerEmojiButton.debouncedClicks { + emojiPopup.toggle() + } + } + private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) } @@ -1090,18 +1136,6 @@ class RoomDetailFragment @Inject constructor( } } - private val writingFileActivityResultLauncher = registerForPermissionsResult { allGranted -> - if (allGranted) { - val pendingUri = roomDetailViewModel.pendingUri - if (pendingUri != null) { - roomDetailViewModel.pendingUri = null - sendUri(pendingUri) - } - } else { - cleanUpAfterPermissionNotGranted() - } - } - private fun setupComposer() { val composerEditText = composerLayout.composerEditText autoCompleter.setup(composerEditText) @@ -1147,14 +1181,7 @@ class RoomDetailFragment @Inject constructor( } override fun onRichContentSelected(contentUri: Uri): Boolean { - // We need WRITE_EXTERNAL permission - return if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, requireActivity(), writingFileActivityResultLauncher)) { - sendUri(contentUri) - } else { - roomDetailViewModel.pendingUri = contentUri - // Always intercept when we request some permission - true - } + return sendUri(contentUri) } } } @@ -1185,11 +1212,9 @@ class RoomDetailFragment @Inject constructor( } private fun sendUri(uri: Uri): Boolean { - roomDetailViewModel.preventAttachmentPreview = true val shareIntent = Intent(Intent.ACTION_SEND, uri) val isHandled = attachmentsHelper.handleShareIntent(requireContext(), shareIntent) if (!isHandled) { - roomDetailViewModel.preventAttachmentPreview = false Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show() } return isHandled @@ -1211,9 +1236,6 @@ class RoomDetailFragment @Inject constructor( scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) inviteView.visibility = View.GONE - val uid = session.myUserId - val meMember = state.myRoomMember() - avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView) if (state.tombstoneEvent == null) { if (state.canSendMessage) { composerLayout.visibility = View.VISIBLE @@ -1554,7 +1576,6 @@ class RoomDetailFragment @Inject constructor( private fun cleanUpAfterPermissionNotGranted() { // Reset all pending data roomDetailViewModel.pendingAction = null - roomDetailViewModel.pendingUri = null attachmentsHelper.pendingType = null } @@ -1630,6 +1651,10 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(itemAction) } + override fun getPreviewUrlRetriever(): PreviewUrlRetriever { + return roomDetailViewModel.previewUrlRetriever + } + override fun onRoomCreateLinkClicked(url: String) { permalinkHandler .launch(requireContext(), url, object : NavigationInterceptor { @@ -1652,17 +1677,20 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) } + override fun onPreviewUrlClicked(url: String) { + onUrlClicked(url, url) + } + + override fun onPreviewUrlCloseClicked(eventId: String, url: String) { + roomDetailViewModel.handle(RoomDetailAction.DoNotShowPreviewUrlFor(eventId, url)) + } + private fun onShareActionClicked(action: EventSharedAction.Share) { if (action.messageContent is MessageTextContent) { shareText(requireContext(), action.messageContent.body) } else if (action.messageContent is MessageWithAttachmentContent) { session.fileService().downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = action.eventId, - fileName = action.messageContent.body, - mimeType = action.messageContent.mimeType, - url = action.messageContent.getFileUrl(), - elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + messageContent = action.messageContent, callback = object : MatrixCallback { override fun onSuccess(data: File) { if (isAdded) { @@ -1692,12 +1720,7 @@ class RoomDetailFragment @Inject constructor( return } session.fileService().downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = action.eventId, - fileName = action.messageContent.body, - mimeType = action.messageContent.mimeType, - url = action.messageContent.getFileUrl(), - elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + messageContent = action.messageContent, callback = object : MatrixCallback { override fun onSuccess(data: File) { if (isAdded) { @@ -1959,24 +1982,18 @@ class RoomDetailFragment @Inject constructor( // AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { - if (roomDetailViewModel.preventAttachmentPreview) { - roomDetailViewModel.preventAttachmentPreview = false - roomDetailViewModel.handle(RoomDetailAction.SendMedia(attachments, false)) - } else { - val grouped = attachments.toGroupedContentAttachmentData() - if (grouped.notPreviewables.isNotEmpty()) { - // Send the not previewable attachments right now (?) - roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false)) - } - if (grouped.previewables.isNotEmpty()) { - val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables)) - contentAttachmentActivityResultLauncher.launch(intent) - } + val grouped = attachments.toGroupedContentAttachmentData() + if (grouped.notPreviewables.isNotEmpty()) { + // Send the not previewable attachments right now (?) + roomDetailViewModel.handle(RoomDetailAction.SendMedia(grouped.notPreviewables, false)) + } + if (grouped.previewables.isNotEmpty()) { + val intent = AttachmentsPreviewActivity.newIntent(requireContext(), AttachmentsPreviewArgs(grouped.previewables)) + contentAttachmentActivityResultLauncher.launch(intent) } } override fun onAttachmentsProcessFailed() { - roomDetailViewModel.preventAttachmentPreview = false Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index d5d94a0ca5..81d3d622e7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -95,4 +95,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { // TODO Remove object SlashCommandNotImplemented : SendMessageResult() + + data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() + object StopChatEffects : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a83dddc9ac..e4e7177e4f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -40,6 +40,7 @@ import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.raw.wellknown.getElementWellknown @@ -69,7 +70,6 @@ import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams @@ -80,7 +80,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.OptionItem -import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper @@ -92,7 +91,6 @@ import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.util.awaitCallback @@ -100,7 +98,6 @@ import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import timber.log.Timber import java.io.File -import java.lang.Exception import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -117,8 +114,9 @@ class RoomDetailViewModel @AssistedInject constructor( private val roomSummaryHolder: RoomSummaryHolder, private val typingHelper: TypingHelper, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val chatEffectManager: ChatEffectManager, timelineSettingsFactory: TimelineSettingsFactory -) : VectorViewModel(initialState), Timeline.Listener { +) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId @@ -128,15 +126,12 @@ class RoomDetailViewModel @AssistedInject constructor( private var timelineEvents = PublishRelay.create>() val timeline = room.createTimeline(eventId, timelineSettings) + // Same lifecycle than the ViewModel (survive to screen rotation) + val previewUrlRetriever = PreviewUrlRetriever(session) + // Slot to keep a pending action during permission request var pendingAction: RoomDetailAction? = null - // Slot to keep a pending uri during permission request - var pendingUri: Uri? = null - - // Slot to store if we want to prevent preview of attachment - var preventAttachmentPreview = false - private var trackUnreadMessages = AtomicBoolean(false) private var mostRecentDisplayedEvent: TimelineEvent? = null @@ -176,6 +171,7 @@ class RoomDetailViewModel @AssistedInject constructor( room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) + chatEffectManager.delegate = this } private fun observePowerLevel() { @@ -286,15 +282,18 @@ class RoomDetailViewModel @AssistedInject constructor( RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) ) } + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) }.exhaustive } + private fun handleDoNotShowPreviewUrlFor(action: RoomDetailAction.DoNotShowPreviewUrlFor) { + previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url) + } + private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { viewModelScope.launch(Dispatchers.IO) { try { - awaitCallback { - room.updateAvatar(action.newAvatarUri, action.newAvatarFileName, it) - } + room.updateAvatar(action.newAvatarUri, action.newAvatarFileName) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) } catch (failure: Throwable) { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) @@ -551,7 +550,7 @@ class RoomDetailViewModel @AssistedInject constructor( SendMode.EDIT(timelineEvent, currentDraft.text) } } - else -> null + else -> null } ?: SendMode.REGULAR("", fromSharing = false) ) } @@ -594,16 +593,16 @@ class RoomDetailViewModel @AssistedInject constructor( return@withState false } when (itemId) { - R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true - R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true - R.id.open_matrix_apps -> true + R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true + R.id.timeline_setting -> true + R.id.invite -> state.canInvite + R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true + R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> true // always show for discoverability - R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null - R.id.search -> true - else -> false + R.id.video_call -> true // always show for discoverability + R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null + R.id.search -> true + else -> false } } @@ -716,6 +715,11 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } + is ParsedCommand.SendChatEffect -> { + sendChatEffect(slashCommandResult) + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } is ParsedCommand.SendPoll -> { room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) @@ -804,6 +808,19 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { + // If message is blank, convert to an emote, with default message + if (sendChatEffect.message.isBlank()) { + val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { + ChatEffect.CONFETTI -> R.string.default_message_emote_confetti + ChatEffect.SNOW -> R.string.default_message_emote_snow + }) + room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) + } else { + room.sendTextMessage(sendChatEffect.message, sendChatEffect.chatEffect.toMessageType()) + } + } + private fun popDraft() = withState { if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { // If we were sharing, we want to get back our last value from draft @@ -854,8 +871,8 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - launchSlashCommandFlow { - room.updateTopic(changeTopic.topic, it) + launchSlashCommandFlowSuspendable { + room.updateTopic(changeTopic.topic) } } @@ -876,9 +893,9 @@ class RoomDetailViewModel @AssistedInject constructor( ?.content ?.toModel() ?: return - launchSlashCommandFlow { + launchSlashCommandFlowSuspendable { currentPowerLevelsContent.setUserPowerLevel(setUserPowerLevel.userId, setUserPowerLevel.powerLevel) - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it) + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent()) } } @@ -920,6 +937,19 @@ class RoomDetailViewModel @AssistedInject constructor( lambda.invoke(matrixCallback) } + private fun launchSlashCommandFlowSuspendable(block: suspend () -> Unit) { + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + viewModelScope.launch { + val event = try { + block() + RoomDetailViewEvents.SlashCommandResultOk + } catch (failure: Exception) { + RoomDetailViewEvents.SlashCommandResultError(failure) + } + _viewEvents.post(event) + } + } + private fun handleSendReaction(action: RoomDetailAction.SendReaction) { room.sendReaction(action.targetEventId, action.reaction) } @@ -972,9 +1002,22 @@ class RoomDetailViewModel @AssistedInject constructor( visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) } } + + // handle chat effects here + if (vectorPreferences.chatEffectsEnabled()) { + chatEffectManager.checkForEffect(action.event) + } } } + override fun shouldStartEffect(effect: ChatEffect) { + _viewEvents.post(RoomDetailViewEvents.StartChatEffect(effect)) + } + + override fun stopEffects() { + _viewEvents.post(RoomDetailViewEvents.StopChatEffects) + } + private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { timeline.paginate(action.direction, PAGINATION_COUNT) } @@ -1010,10 +1053,10 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { - val mxcUrl = action.messageFileContent.getFileUrl() + val mxcUrl = action.messageFileContent.getFileUrl() ?: return val isLocalSendingFile = action.senderId == session.myUserId - && mxcUrl?.startsWith("content://") ?: false - val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false + && mxcUrl.startsWith("content://") + val isDownloaded = session.fileService().isFileInCache(action.messageFileContent) if (isLocalSendingFile) { tryOrNull { Uri.parse(mxcUrl) }?.let { _viewEvents.post(RoomDetailViewEvents.OpenFile( @@ -1024,7 +1067,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } else if (isDownloaded) { // we can open it - session.fileService().getTemporarySharableURI(mxcUrl!!, action.messageFileContent.mimeType)?.let { uri -> + session.fileService().getTemporarySharableURI(action.messageFileContent)?.let { uri -> _viewEvents.post(RoomDetailViewEvents.OpenFile( action.messageFileContent.mimeType, uri, @@ -1033,12 +1076,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } else { session.fileService().downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = action.eventId, - fileName = action.messageFileContent.getFileName(), - mimeType = action.messageFileContent.mimeType, - url = mxcUrl, - elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), + messageContent = action.messageFileContent, callback = object : MatrixCallback { override fun onSuccess(data: File) { _viewEvents.post(RoomDetailViewEvents.DownloadFileState( @@ -1350,6 +1388,17 @@ class RoomDetailViewModel @AssistedInject constructor( override fun onTimelineUpdated(snapshot: List) { timelineEvents.accept(snapshot) + + // PreviewUrl + if (vectorPreferences.showUrlPreviews()) { + withState { state -> + snapshot + .takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false } + ?.forEach { + previewUrlRetriever.getPreviewUrl(it.root, viewModelScope) + } + } + } } override fun onTimelineFailure(throwable: Throwable) { @@ -1370,6 +1419,8 @@ class RoomDetailViewModel @AssistedInject constructor( if (vectorPreferences.sendTypingNotifs()) { room.userStopsTyping() } + chatEffectManager.delegate = null + chatEffectManager.dispose() super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt index 353ab783db..2257e5ee81 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/ComposerEditText.kt @@ -24,16 +24,16 @@ import android.text.Editable import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection -import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat +import com.vanniktech.emoji.EmojiEditText import im.vector.app.core.extensions.ooi import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.features.html.PillImageSpan import timber.log.Timber class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle) - : AppCompatEditText(context, attrs, defStyleAttr) { + : EmojiEditText(context, attrs, defStyleAttr) { interface Callback { fun onRichContentSelected(contentUri: Uri): Boolean diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index f4b14571c0..f232e9a65e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -36,7 +36,7 @@ import androidx.transition.TransitionSet import butterknife.BindView import butterknife.ButterKnife import im.vector.app.R -import kotlinx.android.synthetic.main.merge_composer_layout.view.* +import kotlinx.android.synthetic.main.composer_layout.view.* import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel /** @@ -72,8 +72,8 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib @BindView(R.id.composerEditText) lateinit var composerEditText: ComposerEditText - @BindView(R.id.composer_avatar_view) - lateinit var composerAvatarImageView: ImageView + @BindView(R.id.composer_emoji) + lateinit var composerEmojiButton: ImageButton @BindView(R.id.composer_shield) lateinit var composerShieldImageView: ImageView @@ -86,7 +86,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib get() = composerEditText.text init { - inflate(context, R.layout.merge_composer_layout, this) + inflate(context, R.layout.composer_layout, this) ButterKnife.bind(this) collapse(false) composerEditText.callback = object : ComposerEditText.Callback { @@ -110,20 +110,20 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib } fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { - if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) { + if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) { // ignore we good return } - currentConstraintSetId = R.layout.constraint_set_composer_layout_compact + currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { - if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) { + if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) { // ignore we good return } - currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded + currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index bddc7fa126..ba3ffe3174 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -48,6 +48,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences @@ -76,7 +77,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { - interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { + interface Callback : + BaseCallback, + ReactionPillCallback, + AvatarCallback, + UrlClickCallback, + ReadReceiptsCallback, + PreviewUrlCallback { fun onLoadMore(direction: Timeline.Direction) fun onEventInvisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent) @@ -91,6 +98,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // TODO move all callbacks to this? fun onTimelineItemAction(itemAction: RoomDetailAction) + + fun getPreviewUrlRetriever(): PreviewUrlRetriever } interface ReactionPillCallback { @@ -118,6 +127,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onUrlLongClicked(url: String): Boolean } + interface PreviewUrlCallback { + fun onPreviewUrlClicked(url: String) + fun onPreviewUrlCloseClicked(eventId: String, url: String) + } + // Map eventId to adapter position private val adapterPositionMapping = HashMap() private val modelCache = arrayListOf() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index f77e39c245..e88c1f3797 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -82,10 +82,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat when (cryptoError) { MXCryptoError.ErrorType.KEYS_WITHHELD -> { span { - apply { - drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { - image(it, "baseline") - } + drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { + image(it, "baseline") + +" " } span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) { textStyle = "italic" @@ -95,10 +94,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } else -> { span { - apply { - drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { - image(it, "baseline") - } + drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { + image(it, "baseline") + +" " } span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) { textStyle = "italic" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2b067ccf3f..27696f5b28 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -84,9 +84,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL +import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import javax.inject.Inject @@ -144,16 +146,16 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -164,7 +166,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { return when (messageContent.optionType) { - OPTION_TYPE_POLL -> { + OPTION_TYPE_POLL -> { MessagePollItem_() .attributes(attributes) .callback(callback) @@ -204,7 +206,12 @@ class MessageItemFactory @Inject constructor( return MessageFileItem_() .attributes(attributes) .izLocalFile(fileUrl.isLocalFile()) - .izDownloaded(session.fileService().isFileInCache(fileUrl, messageContent.mimeType)) + .izDownloaded(session.fileService().isFileInCache( + fileUrl, + messageContent.getFileName(), + messageContent.mimeType, + messageContent.encryptedFileInfo?.toElementToDecrypt()) + ) .mxcUrl(fileUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) @@ -264,7 +271,7 @@ class MessageItemFactory @Inject constructor( .attributes(attributes) .leftGuideline(avatarSizeProvider.leftGuideline) .izLocalFile(messageContent.getFileUrl().isLocalFile()) - .izDownloaded(session.fileService().isFileInCache(mxcUrl, messageContent.mimeType)) + .izDownloaded(session.fileService().isFileInCache(messageContent)) .mxcUrl(mxcUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) @@ -305,7 +312,7 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .playable(messageContent.info?.mimeType == "image/gif") + .playable(messageContent.info?.mimeType == MimeTypes.Gif) .highlighted(highlight) .mediaData(data) .apply { @@ -371,7 +378,7 @@ class MessageItemFactory @Inject constructor( val codeVisitor = CodeVisitor() codeVisitor.visit(localFormattedBody) when (codeVisitor.codeKind) { - CodeVisitor.Kind.BLOCK -> { + CodeVisitor.Kind.BLOCK -> { val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) if (codeFormattedBlock == null) { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) @@ -387,7 +394,7 @@ class MessageItemFactory @Inject constructor( buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) } } - CodeVisitor.Kind.NONE -> { + CodeVisitor.Kind.NONE -> { buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) } } @@ -424,6 +431,9 @@ class MessageItemFactory @Inject constructor( } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .searchForPills(isFormatted) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) @@ -529,6 +539,9 @@ class MessageItemFactory @Inject constructor( } } .leftGuideline(avatarSizeProvider.leftGuideline) + .previewUrlRetriever(callback?.getPreviewUrlRetriever()) + .imageContentRenderer(imageContentRenderer) + .previewUrlCallback(callback) .attributes(attributes) .highlighted(highlight) .movementMethod(createLinkMovementMethod(callback)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 575f28b610..243cbbd0e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -53,7 +53,6 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_AVATAR, EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_HISTORY_VISIBILITY, @@ -79,6 +78,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me encryptedItemFactory.create(event, nextEvent, highlight, callback) } } + EventType.STATE_ROOM_ALIASES, EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_KEY, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index c4cc2e87b0..c725f5b7dc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -182,8 +182,8 @@ class NoticeEventFormatter @Inject constructor( } } - private fun formatDebug(event: Event): CharSequence? { - return "{ \"type\": ${event.getClearType()} }" + private fun formatDebug(event: Event): CharSequence { + return "Debug: event type \"${event.getClearType()}\"" } private fun formatRoomCreateEvent(event: Event, rs: RoomSummary?): CharSequence? { @@ -260,13 +260,13 @@ class NoticeEventFormatter @Inject constructor( private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null - val formattedVisibility = roomHistoryVisibilityFormatter.format(historyVisibility) + val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) return if (event.isSentByCurrentUser()) { sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you, - formattedVisibility) + historyVisibilitySuffix) } else { sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility, - senderName, formattedVisibility) + senderName, historyVisibilitySuffix) } } @@ -465,21 +465,72 @@ class NoticeEventFormatter @Inject constructor( private fun formatRoomCanonicalAliasEvent(event: Event, senderName: String?): String? { val eventContent: RoomCanonicalAliasContent? = event.getClearContent().toModel() - val canonicalAlias = eventContent?.canonicalAlias - return canonicalAlias - ?.takeIf { it.isNotBlank() } - ?.let { + val prevContent: RoomCanonicalAliasContent? = event.resolvedPrevContent().toModel() + val canonicalAlias = eventContent?.canonicalAlias?.takeIf { it.isNotEmpty() } + val prevCanonicalAlias = prevContent?.canonicalAlias?.takeIf { it.isNotEmpty() } + val altAliases = eventContent?.alternativeAliases.orEmpty() + val prevAltAliases = prevContent?.alternativeAliases.orEmpty() + val added = altAliases - prevAltAliases + val removed = prevAltAliases - altAliases + + return when { + added.isEmpty() && removed.isEmpty() && canonicalAlias == prevCanonicalAlias -> { + // No difference between the two events say something as we can't simply hide the event from here + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_no_change_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_no_change, senderName) + } + } + added.isEmpty() && removed.isEmpty() -> { + // Canonical has changed + if (canonicalAlias != null) { if (event.isSentByCurrentUser()) { - sp.getString(R.string.notice_room_canonical_alias_set_by_you, it) + sp.getString(R.string.notice_room_canonical_alias_set_by_you, canonicalAlias) } else { - sp.getString(R.string.notice_room_canonical_alias_set, senderName, it) + sp.getString(R.string.notice_room_canonical_alias_set, senderName, canonicalAlias) + } + } else { + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_unset_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_unset, senderName) } } - ?: if (event.isSentByCurrentUser()) { - sp.getString(R.string.notice_room_canonical_alias_unset_by_you) + } + added.isEmpty() && canonicalAlias == prevCanonicalAlias -> { + // Some alternative has been removed + if (event.isSentByCurrentUser()) { + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_removed_by_you, removed.size, removed.joinToString()) } else { - sp.getString(R.string.notice_room_canonical_alias_unset, senderName) + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_removed, removed.size, senderName, removed.joinToString()) } + } + removed.isEmpty() && canonicalAlias == prevCanonicalAlias -> { + // Some alternative has been added + if (event.isSentByCurrentUser()) { + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_added_by_you, added.size, added.joinToString()) + } else { + sp.getQuantityString(R.plurals.notice_room_canonical_alias_alternative_added, added.size, senderName, added.joinToString()) + } + } + canonicalAlias == prevCanonicalAlias -> { + // Alternative added and removed + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_alternative_changed_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_alternative_changed, senderName) + } + } + else -> { + // Main and removed, or main and added, or main and added and removed + if (event.isSentByCurrentUser()) { + sp.getString(R.string.notice_room_canonical_alias_main_and_alternative_changed_by_you) + } else { + sp.getString(R.string.notice_room_canonical_alias_main_and_alternative_changed, senderName) + } + } + } } private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt index 4563e6a6ed..14769bc95b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/RoomHistoryVisibilityFormatter.kt @@ -24,13 +24,21 @@ import javax.inject.Inject class RoomHistoryVisibilityFormatter @Inject constructor( private val stringProvider: StringProvider ) { + fun getNoticeSuffix(roomHistoryVisibility: RoomHistoryVisibility): String { + return stringProvider.getString(when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE -> R.string.notice_room_visibility_world_readable + RoomHistoryVisibility.SHARED -> R.string.notice_room_visibility_shared + RoomHistoryVisibility.INVITED -> R.string.notice_room_visibility_invited + RoomHistoryVisibility.JOINED -> R.string.notice_room_visibility_joined + }) + } - fun format(roomHistoryVisibility: RoomHistoryVisibility): String { - return when (roomHistoryVisibility) { - RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) - } + fun getSetting(roomHistoryVisibility: RoomHistoryVisibility): String { + return stringProvider.getString(when (roomHistoryVisibility) { + RoomHistoryVisibility.WORLD_READABLE -> R.string.room_settings_read_history_entry_anyone + RoomHistoryVisibility.SHARED -> R.string.room_settings_read_history_entry_members_only_option_time_shared + RoomHistoryVisibility.INVITED -> R.string.room_settings_read_history_entry_members_only_invited + RoomHistoryVisibility.JOINED -> R.string.room_settings_read_history_entry_members_only_joined + }) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index f7a1a18d9f..8a8bf364e1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.features.home.room.detail.timeline.helper diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 3297f14622..c120fa671c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -1,19 +1,17 @@ /* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.app.features.home.room.detail.timeline.helper diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt index 3317612a6c..1983b05ed3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt @@ -17,12 +17,17 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.resources.UserPreferencesProvider +import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import javax.inject.Inject -class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { +class TimelineSettingsFactory @Inject constructor( + private val userPreferencesProvider: UserPreferencesProvider, + private val session: Session +) { fun create(): TimelineSettings { return if (userPreferencesProvider.shouldShowHiddenEvents()) { @@ -35,7 +40,7 @@ class TimelineSettingsFactory @Inject constructor(private val userPreferencesPro filterTypes = false), buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { - val allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES.filterDisplayableTypes() + val allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES.createAllowedEventTypeFilters() TimelineSettings( initialSize = 30, filters = TimelineEventFilters( @@ -48,12 +53,12 @@ class TimelineSettingsFactory @Inject constructor(private val userPreferencesPro } } - private fun List.filterDisplayableTypes(): List { - return filter { type -> - when (type) { - EventType.STATE_ROOM_MEMBER -> userPreferencesProvider.shouldShowRoomMemberStateEvents() - else -> true - } + private fun List.createAllowedEventTypeFilters(): List { + return map { + EventTypeFilter( + eventType = it, + stateKey = if (it == EventType.STATE_ROOM_MEMBER && userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index feba62dea3..66d9808d2b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -23,7 +23,12 @@ import androidx.core.widget.TextViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R +import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState +import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView +import im.vector.app.features.media.ImageContentRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageTextItem : AbsMessageItem() { @@ -37,10 +42,27 @@ abstract class MessageTextItem : AbsMessageItem() { @EpoxyAttribute var useBigFont: Boolean = false + @EpoxyAttribute + var previewUrlRetriever: PreviewUrlRetriever? = null + + @EpoxyAttribute + var previewUrlCallback: TimelineEventController.PreviewUrlCallback? = null + + @EpoxyAttribute + var imageContentRenderer: ImageContentRenderer? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var movementMethod: MovementMethod? = null + private val previewUrlViewUpdater = PreviewUrlViewUpdater() + override fun bind(holder: Holder) { + // Preview URL + previewUrlViewUpdater.previewUrlView = holder.previewUrlView + previewUrlViewUpdater.imageContentRenderer = imageContentRenderer + previewUrlRetriever?.addListener(attributes.informationData.eventId, previewUrlViewUpdater) + holder.previewUrlView.delegate = previewUrlCallback + if (useBigFont) { holder.messageView.textSize = 44F } else { @@ -65,12 +87,29 @@ abstract class MessageTextItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) } + override fun unbind(holder: Holder) { + super.unbind(holder) + previewUrlViewUpdater.previewUrlView = null + previewUrlViewUpdater.imageContentRenderer = null + previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater) + } + override fun getViewType() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val messageView by bind(R.id.messageTextView) + val previewUrlView by bind(R.id.messageUrlPreview) } + inner class PreviewUrlViewUpdater : PreviewUrlRetriever.PreviewUrlRetrieverListener { + var previewUrlView: PreviewUrlView? = null + var imageContentRenderer: ImageContentRenderer? = null + + override fun onStateUpdated(state: PreviewUrlUiState) { + val safeImageContentRenderer = imageContentRenderer ?: return + previewUrlView?.render(state, safeImageContentRenderer) + } + } companion object { private const val STUB_ID = R.id.messageContentTextStub } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt new file mode 100644 index 0000000000..695661feeb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.url + +import im.vector.app.BuildConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.cache.CacheStrategy +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.Event + +class PreviewUrlRetriever(session: Session) { + private val mediaService = session.mediaService() + + private val data = mutableMapOf() + private val listeners = mutableMapOf>() + + // In memory list + private val blockedUrl = mutableSetOf() + + fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) { + val eventId = event.eventId ?: return + + synchronized(data) { + if (data[eventId] == null) { + // Keep only the first URL for the moment + val url = mediaService.extractUrls(event) + .firstOrNull() + ?.takeIf { it !in blockedUrl } + if (url == null) { + updateState(eventId, PreviewUrlUiState.NoUrl) + } else { + updateState(eventId, PreviewUrlUiState.Loading) + } + url + } else { + // Already handled + null + } + }?.let { urlToRetrieve -> + coroutineScope.launch { + runCatching { + mediaService.getPreviewUrl( + url = urlToRetrieve, + timestamp = null, + cacheStrategy = if (BuildConfig.DEBUG) CacheStrategy.NoCache else CacheStrategy.TtlCache(CACHE_VALIDITY, false) + ) + }.fold( + { + synchronized(data) { + // Blocked after the request has been sent? + if (urlToRetrieve in blockedUrl) { + updateState(eventId, PreviewUrlUiState.NoUrl) + } else { + updateState(eventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it)) + } + } + }, + { + synchronized(data) { + updateState(eventId, PreviewUrlUiState.Error(it)) + } + } + ) + } + } + } + + fun doNotShowPreviewUrlFor(eventId: String, url: String) { + blockedUrl.add(url) + + // Notify the listener + synchronized(data) { + data[eventId] + ?.takeIf { it is PreviewUrlUiState.Data && it.url == url } + ?.let { + updateState(eventId, PreviewUrlUiState.NoUrl) + } + } + } + + private fun updateState(eventId: String, state: PreviewUrlUiState) { + data[eventId] = state + // Notify the listener + listeners[eventId].orEmpty().forEach { + it.onStateUpdated(state) + } + } + + // Called by the Epoxy item during binding + fun addListener(key: String, listener: PreviewUrlRetrieverListener) { + listeners.getOrPut(key) { mutableSetOf() }.add(listener) + + // Give the current state if any + synchronized(data) { + listener.onStateUpdated(data[key] ?: PreviewUrlUiState.Unknown) + } + } + + // Called by the Epoxy item during unbinding + fun removeListener(key: String, listener: PreviewUrlRetrieverListener) { + listeners[key]?.remove(listener) + } + + interface PreviewUrlRetrieverListener { + fun onStateUpdated(state: PreviewUrlUiState) + } + + companion object { + // One week in millis + private const val CACHE_VALIDITY: Long = 7 * 24 * 3_600 * 1_000 + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt new file mode 100644 index 0000000000..a8f8f7b0cb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlUiState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.url + +import org.matrix.android.sdk.api.session.media.PreviewUrlData + +/** + * The state representing a preview url UI state for an Event + */ +sealed class PreviewUrlUiState { + // No info + object Unknown : PreviewUrlUiState() + + // The event does not contain any URLs + object NoUrl : PreviewUrlUiState() + + // Loading + object Loading : PreviewUrlUiState() + + // Error + data class Error(val throwable: Throwable) : PreviewUrlUiState() + + // PreviewUrl data + data class Data(val eventId: String, + val url: String, + val previewUrlData: PreviewUrlData) : PreviewUrlUiState() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt new file mode 100755 index 0000000000..9d8f438683 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.url + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.media.ImageContentRenderer +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.media.PreviewUrlData + +/** + * A View to display a PreviewUrl and some other state + */ +class PreviewUrlView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener { + + @BindView(R.id.url_preview_title) + lateinit var titleView: TextView + + @BindView(R.id.url_preview_image) + lateinit var imageView: ImageView + + @BindView(R.id.url_preview_description) + lateinit var descriptionView: TextView + + @BindView(R.id.url_preview_site) + lateinit var siteView: TextView + + @BindView(R.id.url_preview_close) + lateinit var closeView: View + + var delegate: TimelineEventController.PreviewUrlCallback? = null + + init { + setupView() + } + + private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown + + /** + * This methods is responsible for rendering the view according to the newState + * + * @param newState the newState representing the view + */ + fun render(newState: PreviewUrlUiState, + imageContentRenderer: ImageContentRenderer, + force: Boolean = false) { + if (newState == state && !force) { + return + } + + state = newState + + hideAll() + when (newState) { + PreviewUrlUiState.Unknown, + PreviewUrlUiState.NoUrl -> renderHidden() + PreviewUrlUiState.Loading -> renderLoading() + is PreviewUrlUiState.Error -> renderHidden() + is PreviewUrlUiState.Data -> renderData(newState.previewUrlData, imageContentRenderer) + } + } + + override fun onClick(v: View?) { + when (val finalState = state) { + is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url) + else -> Unit + } + } + + private fun onCloseClick() { + when (val finalState = state) { + is PreviewUrlUiState.Data -> delegate?.onPreviewUrlCloseClicked(finalState.eventId, finalState.url) + else -> Unit + } + } + + // PRIVATE METHODS **************************************************************************************************************************************** + + private fun setupView() { + inflate(context, R.layout.url_preview, this) + ButterKnife.bind(this) + + setOnClickListener(this) + closeView.setOnClickListener { onCloseClick() } + } + + private fun renderHidden() { + isVisible = false + } + + private fun renderLoading() { + // Just hide for the moment + isVisible = false + } + + private fun renderData(previewUrlData: PreviewUrlData, imageContentRenderer: ImageContentRenderer) { + isVisible = true + titleView.setTextOrHide(previewUrlData.title) + imageView.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, imageView) }.orFalse() + descriptionView.setTextOrHide(previewUrlData.description) + siteView.setTextOrHide(previewUrlData.siteName.takeIf { it != previewUrlData.title }) + } + + /** + * Hide all views that are not visible in all state + */ + private fun hideAll() { + titleView.isVisible = false + imageView.isVisible = false + descriptionView.isVisible = false + siteView.isVisible = false + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt index e3a5db4b97..f41104cae1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -77,6 +77,7 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R override fun onDestroyView() { recyclerView.cleanup() + roomListActionsEpoxyController.listener = null super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt new file mode 100644 index 0000000000..9b0a154100 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login + +import android.content.ComponentName +import android.net.Uri +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession +import com.airbnb.mvrx.withState +import im.vector.app.core.utils.openUrlInChromeCustomTab + +abstract class AbstractSSOLoginFragment : AbstractLoginFragment() { + + // For sso + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsSession: CustomTabsSession? = null + + override fun onStart() { + super.onStart() + val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() } + if (hasSSO) { + val packageName = CustomTabsClient.getPackageName(requireContext(), null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + customTabsClient = client + .also { it.warmup(0L) } + prefetchIfNeeded() + } + + override fun onServiceDisconnected(name: ComponentName?) { + } + } + .also { + CustomTabsClient.bindCustomTabsService( + requireContext(), + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + } + + override fun onStop() { + super.onStop() + val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() } + if (hasSSO) { + customTabsServiceConnection?.let { requireContext().unbindService(it) } + customTabsServiceConnection = null + } + } + + private fun prefetchUrl(url: String) { + if (customTabsSession == null) { + customTabsSession = customTabsClient?.newSession(null) + } + + customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + } + + fun openInCustomTab(ssoUrl: String) { + openUrlInChromeCustomTab(requireContext(), customTabsSession, ssoUrl) + } + + private fun prefetchIfNeeded() { + withState(loginViewModel) { state -> + if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { + // in this case we can prefetch (not other cases for privacy concerns) + prefetchUrl(state.getSsoUrl(null)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt index eb5aa86b3b..2b4e3d6be0 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt @@ -18,6 +18,7 @@ package im.vector.app.features.login import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.internal.network.ssl.Fingerprint @@ -59,8 +60,13 @@ sealed class LoginAction : VectorViewModelAction { object ResetLogin : ResetAction() object ResetResetPassword : ResetAction() + // Homeserver history + object ClearHomeServerHistory : LoginAction() + // For the soft logout case - data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() + data class SetupSsoForSessionRecovery(val homeServerUrl: String, + val deviceId: String, + val ssoIdentityProviders: List?) : LoginAction() data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 01e835b4e3..2e60fea660 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -157,11 +157,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) is LoginViewEvents.OnLoginFlowRetrieved -> addFragmentToBackstack(R.id.loginFragmentContainer, - if (loginViewEvents.isSso) { - LoginSignUpSignInSsoFragment::class.java - } else { - LoginSignUpSignInSelectionFragment::class.java - }, + LoginSignUpSignInSelectionFragment::class.java, option = commonOption) is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) is LoginViewEvents.OnForgetPasswordClicked -> @@ -252,7 +248,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc // It depends on the LoginMode when (state.loginMode) { LoginMode.Unknown, - LoginMode.Sso -> error("Developer error") + is LoginMode.Sso -> error("Developer error") + is LoginMode.SsoAndPassword, LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG, diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 3ee0d6d9df..7d98f1f8ee 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -37,6 +37,7 @@ import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.fragment_login.* +import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -50,7 +51,7 @@ import javax.inject.Inject * In signup mode: * - the user is asked for login and password */ -class LoginFragment @Inject constructor() : AbstractLoginFragment() { +class LoginFragment @Inject constructor() : AbstractSSOLoginFragment() { private var passwordShown = false private var isSignupMode = false @@ -83,11 +84,13 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { SignMode.SignUp -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP } SignMode.SignIn, SignMode.SignInWithMatrixId -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) + loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN } }.exhaustive } @@ -169,6 +172,19 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { ServerType.Unknown -> Unit /* Should not happen */ } loginPasswordNotice.isVisible = false + + if (state.loginMode is LoginMode.SsoAndPassword) { + loginSocialLoginContainer.isVisible = true + loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders + loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { + override fun onProviderSelected(id: String?) { + openInCustomTab(state.getSsoUrl(id)) + } + } + } else { + loginSocialLoginContainer.isVisible = false + loginSocialLoginButtons.ssoIdentityProviders = null + } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt index 9a930dcd86..14accefc00 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt @@ -16,9 +16,31 @@ package im.vector.app.features.login -enum class LoginMode { - Unknown, - Password, - Sso, - Unsupported +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider + +sealed class LoginMode : Parcelable +/** because persist state */ { + @Parcelize object Unknown : LoginMode() + @Parcelize object Password : LoginMode() + @Parcelize data class Sso(val ssoIdentityProviders: List?) : LoginMode() + @Parcelize data class SsoAndPassword(val ssoIdentityProviders: List?) : LoginMode() + @Parcelize object Unsupported : LoginMode() +} + +fun LoginMode.ssoIdentityProviders() : List? { + return when (this) { + is LoginMode.Sso -> ssoIdentityProviders + is LoginMode.SsoAndPassword -> ssoIdentityProviders + else -> null + } +} + +fun LoginMode.hasSso() : Boolean { + return when (this) { + is LoginMode.Sso -> true + is LoginMode.SsoAndPassword -> true + else -> false + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt index f4595de634..c9460e1a41 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt @@ -83,7 +83,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt index af959fecd4..1915cdd204 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt @@ -20,9 +20,13 @@ import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo +import android.widget.ArrayAdapter +import androidx.core.view.isInvisible import androidx.core.view.isVisible import butterknife.OnClick +import com.google.android.material.textfield.TextInputLayout import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.utils.ensureProtocol @@ -55,6 +59,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { + loginServerUrlFormHomeServerUrl.dismissDropDown() submit() return@setOnEditorActionListener true } @@ -81,6 +86,15 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice) } } + val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList() + loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter( + requireContext(), + R.layout.item_completion_homeserver, + completions + )) + loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU + .takeIf { completions.isNotEmpty() } + ?: TextInputLayout.END_ICON_NONE } @OnClick(R.id.loginServerUrlFormLearnMore) @@ -88,6 +102,11 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK) } + @OnClick(R.id.loginServerUrlFormClearHistory) + fun clearHistory() { + loginViewModel.handle(LoginAction.ClearHomeServerHistory) + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetHomeServerUrl) } @@ -105,7 +124,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) } else -> { - loginServerUrlFormHomeServerUrl.setText(serverUrl) + loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/) loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) } } @@ -129,9 +148,11 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() override fun updateWithState(state: LoginViewState) { setupUi(state) + loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty() + if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt index fa2f6b9df8..ec931f89a2 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt @@ -18,6 +18,7 @@ package im.vector.app.features.login import androidx.core.view.isVisible import butterknife.OnClick +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.toReducedUrl import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* @@ -26,11 +27,11 @@ import javax.inject.Inject /** * In this screen, the user is asked to sign up or to sign in to the homeserver */ -open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { +class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLoginFragment() { override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection - protected fun setupUi(state: LoginViewState) { + private fun setupUi(state: LoginViewState) { when (state.serverType) { ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) @@ -38,29 +39,61 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl()) loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text) } - ServerType.EMS -> { + ServerType.EMS -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services) loginSignupSigninServerIcon.isVisible = true loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular) loginSignupSigninText.text = state.homeServerUrl.toReducedUrl() } - ServerType.Other -> { + ServerType.Other -> { loginSignupSigninServerIcon.isVisible = false loginSignupSigninTitle.text = getString(R.string.login_server_other_title) loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl()) } - ServerType.Unknown -> Unit /* Should not happen */ + ServerType.Unknown -> Unit /* Should not happen */ + } + + when (state.loginMode) { + is LoginMode.SsoAndPassword -> { + loginSignupSigninSignInSocialLoginContainer.isVisible = true + loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders() + loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { + override fun onProviderSelected(id: String?) { + val url = withState(loginViewModel) { it.getSsoUrl(id) } + openInCustomTab(url) + } + } + } + else -> { + // SSO only is managed without container as well as No sso + loginSignupSigninSignInSocialLoginContainer.isVisible = false + loginSignupSigninSocialLoginButtons.ssoIdentityProviders = null + } } } - private fun setupButtons() { - loginSignupSigninSubmit.text = getString(R.string.login_signup) - loginSignupSigninSignIn.isVisible = true + private fun setupButtons(state: LoginViewState) { + when (state.loginMode) { + is LoginMode.Sso -> { + // change to only one button that is sign in with sso + loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + loginSignupSigninSignIn.isVisible = false + } + else -> { + loginSignupSigninSubmit.text = getString(R.string.login_signup) + loginSignupSigninSignIn.isVisible = true + } + } } @OnClick(R.id.loginSignupSigninSubmit) - open fun submit() { - loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + fun submit() = withState(loginViewModel) { state -> + if (state.loginMode is LoginMode.Sso) { + openInCustomTab(state.getSsoUrl(null)) + } else { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + } + Unit } @OnClick(R.id.loginSignupSigninSignIn) @@ -74,6 +107,6 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo override fun updateWithState(state: LoginViewState) { setupUi(state) - setupButtons() + setupButtons(state) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt deleted file mode 100644 index 8d33ec2e27..0000000000 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.login - -import android.content.ComponentName -import android.net.Uri -import androidx.browser.customtabs.CustomTabsClient -import androidx.browser.customtabs.CustomTabsServiceConnection -import androidx.browser.customtabs.CustomTabsSession -import androidx.core.view.isVisible -import im.vector.app.R -import im.vector.app.core.utils.openUrlInChromeCustomTab -import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* -import javax.inject.Inject - -/** - * In this screen, the user is asked to sign up or to sign in using SSO - * This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened. - */ -open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() { - - private var ssoUrl: String? = null - private var customTabsServiceConnection: CustomTabsServiceConnection? = null - private var customTabsClient: CustomTabsClient? = null - private var customTabsSession: CustomTabsSession? = null - - override fun onStart() { - super.onStart() - - val packageName = CustomTabsClient.getPackageName(requireContext(), null) - - // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device - if (packageName != null) { - customTabsServiceConnection = object : CustomTabsServiceConnection() { - override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { - customTabsClient = client - .also { it.warmup(0L) } - } - - override fun onServiceDisconnected(name: ComponentName?) { - } - } - .also { - CustomTabsClient.bindCustomTabsService( - requireContext(), - // Despite the API, packageName cannot be null - packageName, - it - ) - } - } - } - - private fun prefetchUrl(url: String) { - if (ssoUrl != null) return - - ssoUrl = url - if (customTabsSession == null) { - customTabsSession = customTabsClient?.newSession(null) - } - - customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) - } - - override fun onStop() { - super.onStop() - customTabsServiceConnection?.let { requireContext().unbindService(it) } - customTabsServiceConnection = null - } - - private fun setupButtons() { - loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) - loginSignupSigninSignIn.isVisible = false - } - - override fun submit() { - ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) } - } - - override fun updateWithState(state: LoginViewState) { - setupUi(state) - setupButtons() - prefetchUrl(state.getSsoUrl()) - } -} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewEvents.kt index 3bc2948f89..dc14a0091d 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewEvents.kt @@ -34,7 +34,7 @@ sealed class LoginViewEvents : VectorViewEvents { object OpenServerSelection : LoginViewEvents() data class OnServerSelectionDone(val serverType: ServerType) : LoginViewEvents() - data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents() + object OnLoginFlowRetrieved : LoginViewEvents() data class OnSignModeSelected(val signMode: SignMode) : LoginViewEvents() object OnForgetPasswordClicked : LoginViewEvents() object OnResetPasswordSendThreePidDone : LoginViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 1f47916538..0a6dbcaae2 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -38,6 +38,7 @@ import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.features.signout.soft.SoftLogoutActivity import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowTypes @@ -63,7 +64,8 @@ class LoginViewModel @AssistedInject constructor( private val activeSessionHolder: ActiveSessionHolder, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val reAuthHelper: ReAuthHelper, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val homeServerHistoryService: HomeServerHistoryService ) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -71,6 +73,16 @@ class LoginViewModel @AssistedInject constructor( fun create(initialState: LoginViewState): LoginViewModel } + init { + getKnownCustomHomeServersUrls() + } + + private fun getKnownCustomHomeServersUrls() { + setState { + copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls()) + } + } + companion object : MvRxViewModelFactory { @JvmStatic @@ -121,6 +133,7 @@ class LoginViewModel @AssistedInject constructor( is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + LoginAction.ClearHomeServerHistory -> handleClearHomeServerHistory() is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) }.exhaustive } @@ -129,10 +142,11 @@ class LoginViewModel @AssistedInject constructor( // It happen when we get the login flow, or during direct authentication. // So alter the homeserver config and retrieve again the login flow when (val finalLastAction = lastAction) { - is LoginAction.UpdateHomeServer -> + is LoginAction.UpdateHomeServer -> { currentHomeServerConnectionConfig ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } ?.let { getLoginFlow(it) } + } is LoginAction.LoginOrRegister -> handleDirectLogin( finalLastAction, @@ -145,6 +159,16 @@ class LoginViewModel @AssistedInject constructor( } } + private fun rememberHomeServer(homeServerUrl: String) { + homeServerHistoryService.addHomeServerToHistory(homeServerUrl) + getKnownCustomHomeServersUrls() + } + + private fun handleClearHomeServerHistory() { + homeServerHistoryService.clearHistory() + getKnownCustomHomeServersUrls() + } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { val safeLoginWizard = loginWizard @@ -184,7 +208,7 @@ class LoginViewModel @AssistedInject constructor( setState { copy( signMode = SignMode.SignIn, - loginMode = LoginMode.Sso, + loginMode = LoginMode.Sso(action.ssoIdentityProviders), homeServerUrl = action.homeServerUrl, deviceId = action.deviceId ) @@ -713,7 +737,6 @@ class LoginViewModel @AssistedInject constructor( private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) { val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) - if (homeServerConnectionConfig == null) { // This is invalid _viewEvents.post(LoginViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) @@ -751,13 +774,19 @@ class LoginViewModel @AssistedInject constructor( } override fun onSuccess(data: LoginFlowResult) { + // Valid Homeserver, add it to the history. + // Note: we add what the user has input, data.homeServerUrl can be different + rememberHomeServer(homeServerConnectionConfig.homeServerUri.toString()) + when (data) { is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } // FIXME We should post a view event here normally? diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt index 194ed668bc..383fd4a54e 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt @@ -51,7 +51,8 @@ data class LoginViewState( val loginMode: LoginMode = LoginMode.Unknown, @PersistState // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable - val loginModeSupportedTypes: List = emptyList() + val loginModeSupportedTypes: List = emptyList(), + val knownCustomHomeServersUrls: List = emptyList() ) : MvRxState { fun isLoading(): Boolean { @@ -68,10 +69,13 @@ data class LoginViewState( return asyncLoginAction is Success } - fun getSsoUrl(): String { + fun getSsoUrl(providerId: String?): String { return buildString { append(homeServerUrl?.trim { it == '/' }) append(SSO_REDIRECT_PATH) + if (providerId != null) { + append("/$providerId") + } // Set a redirect url we will intercept later appendParamToUrl(SSO_REDIRECT_URL_PARAM, VECTOR_REDIRECT_URL) deviceId?.takeIf { it.isNotBlank() }?.let { diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt new file mode 100644 index 0000000000..9290479a7a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.login + +import android.content.Context +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import im.vector.app.R +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider + +class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) + : LinearLayout(context, attrs, defStyle) { + + interface InteractionListener { + fun onProviderSelected(id: String?) + } + + enum class Mode { + MODE_SIGN_IN, + MODE_SIGN_UP, + MODE_CONTINUE, + } + + var ssoIdentityProviders: List? = null + set(newProviders) { + if (newProviders != ssoIdentityProviders) { + field = newProviders + update() + } + } + + var mode: Mode = Mode.MODE_CONTINUE + set(value) { + if (value != mode) { + field = value + update() + } + } + + var listener: InteractionListener? = null + + private fun update() { + val cachedViews = emptyMap().toMutableMap() + children.filterIsInstance().forEach { + cachedViews[it.getTag(R.id.loginSignupSigninSocialLoginButtons)?.toString() ?: ""] = it + } + removeAllViews() + if (ssoIdentityProviders.isNullOrEmpty()) { + // Put a default sign in with sso button + MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply { + transformationMethod = null + textAlignment = View.TEXT_ALIGNMENT_CENTER + }.let { + it.text = getButtonTitle(context.getString(R.string.login_social_sso)) + it.textAlignment = View.TEXT_ALIGNMENT_CENTER + it.setOnClickListener { + listener?.onProviderSelected(null) + } + addView(it) + } + return + } + + ssoIdentityProviders?.forEach { identityProvider -> + // Use some heuristic to render buttons according to branding guidelines + val button: MaterialButton = cachedViews[identityProvider.id] + ?: when (identityProvider.id) { + SsoIdentityProvider.ID_GOOGLE -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_google_style) + } + SsoIdentityProvider.ID_GITHUB -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_github_style) + } + SsoIdentityProvider.ID_APPLE -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style) + } + SsoIdentityProvider.ID_FACEBOOK -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style) + } + SsoIdentityProvider.ID_TWITTER -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style) + } + else -> { + // TODO Use iconUrl + MaterialButton(context, null, R.attr.materialButtonStyle).apply { + transformationMethod = null + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + } + } + button.text = getButtonTitle(identityProvider.name) + button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id) + button.setOnClickListener { + listener?.onProviderSelected(identityProvider.id) + } + addView(button) + } + } + + private fun getButtonTitle(providerName: String?): String { + return when (mode) { + Mode.MODE_SIGN_IN -> context.getString(R.string.login_social_signin_with, providerName) + Mode.MODE_SIGN_UP -> context.getString(R.string.login_social_signup_with, providerName) + Mode.MODE_CONTINUE -> context.getString(R.string.login_social_continue_with, providerName) + } + } + + init { + this.orientation = VERTICAL + gravity = Gravity.CENTER + clipToPadding = false + clipChildren = false + if (isInEditMode) { + ssoIdentityProviders = listOf( + SsoIdentityProvider(SsoIdentityProvider.ID_GOOGLE, "Google", null), + SsoIdentityProvider(SsoIdentityProvider.ID_FACEBOOK, "Facebook", null), + SsoIdentityProvider(SsoIdentityProvider.ID_APPLE, "Apple", null), + SsoIdentityProvider(SsoIdentityProvider.ID_GITHUB, "GitHub", null), + SsoIdentityProvider(SsoIdentityProvider.ID_TWITTER, "Twitter", null), + SsoIdentityProvider("Custom_pro", "SSO", null) + ) + } + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0) + val modeAttr = typedArray.getInt(R.styleable.SocialLoginButtonsView_signMode, 2) + mode = when (modeAttr) { + 0 -> Mode.MODE_SIGN_IN + 1 -> Mode.MODE_SIGN_UP + else -> Mode.MODE_CONTINUE + } + typedArray.recycle() + update() + } + + fun dpToPx(dp: Int): Int { + val resources = context.resources + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt() + } +} diff --git a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt index e23b905919..90b17f80d7 100644 --- a/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/BaseAttachmentProvider.kt @@ -153,12 +153,10 @@ abstract class BaseAttachmentProvider( } else { target.onVideoFileLoading(info.uid) fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = data.eventId, - mimeType = data.mimeType, - elementToDecrypt = data.elementToDecrypt, fileName = data.filename, + mimeType = data.mimeType, url = data.url, + elementToDecrypt = data.elementToDecrypt, callback = object : MatrixCallback { override fun onSuccess(data: File) { target.onVideoFileReady(info.uid, data) diff --git a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt index 18312b4aa0..328d8f943e 100644 --- a/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/DataAttachmentRoomProvider.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.MimeTypes import java.io.File class DataAttachmentRoomProvider( @@ -38,7 +39,7 @@ class DataAttachmentRoomProvider( return getItem(position).let { when (it) { is ImageContentRenderer.Data -> { - if (it.mimeType == "image/gif") { + if (it.mimeType == MimeTypes.Gif) { AttachmentInfo.AnimatedImage( uid = it.eventId, url = it.url ?: "", @@ -77,11 +78,9 @@ class DataAttachmentRoomProvider( override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { val item = getItem(position) fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = item.eventId, fileName = item.filename, mimeType = item.mimeType, - url = item.url ?: "", + url = item.url, elementToDecrypt = item.elementToDecrypt, callback = object : MatrixCallback { override fun onSuccess(data: File) { diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 187c2e85c3..4670c82db1 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -23,6 +23,7 @@ import android.view.View import android.widget.ImageView import androidx.core.view.updateLayoutParams import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestListener @@ -83,6 +84,19 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: STICKER } + /** + * For url preview + */ + fun render(mxcUrl: String, imageView: ImageView): Boolean { + val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() + val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false + + GlideApp.with(imageView) + .load(imageUrl) + .into(imageView) + return true + } + /** * For gallery */ @@ -129,6 +143,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: GlideApp .with(contextView) .load(data) + .diskCacheStrategy(DiskCacheStrategy.NONE) } else { // Clear image val resolvedUrl = resolveUrl(data) @@ -183,6 +198,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: GlideApp .with(imageView) .load(data) + .diskCacheStrategy(DiskCacheStrategy.NONE) } else { // Clear image val resolvedUrl = resolveUrl(data) @@ -214,20 +230,22 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } - fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { + private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { return createGlideRequest(data, mode, GlideApp.with(imageView), size) } fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest { return if (data.elementToDecrypt != null) { // Encrypted image - glideRequests.load(data) + glideRequests + .load(data) + .diskCacheStrategy(DiskCacheStrategy.NONE) } else { // Clear image val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = when (mode) { Mode.FULL_SIZE, - Mode.STICKER -> resolveUrl(data) + Mode.STICKER -> resolveUrl(data) Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) } // Fallback to base url @@ -295,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: finalHeight = min(maxImageWidth * height / width, maxImageHeight) finalWidth = finalHeight * width / height } - Mode.STICKER -> { + Mode.STICKER -> { // limit on width val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2) finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp) diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index 1e2761dde0..53c5dac9ad 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import java.io.File @@ -56,7 +57,7 @@ class RoomEventsAttachmentProvider( allowNonMxcUrls = it.root.sendState.isSending() ) - if (content.mimeType == "image/gif") { + if (content.mimeType == MimeTypes.Gif) { AttachmentInfo.AnimatedImage( uid = it.eventId, url = content.url ?: "", @@ -125,8 +126,6 @@ class RoomEventsAttachmentProvider( as? MessageWithAttachmentContent ?: return@let fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = timelineEvent.eventId, fileName = messageContent.body, mimeType = messageContent.mimeType, url = messageContent.getFileUrl(), diff --git a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt index f8cd09ce2f..d8eddc7331 100644 --- a/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/VideoContentRenderer.kt @@ -27,7 +27,6 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.utils.isLocalFile import kotlinx.android.parcel.Parcelize import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import timber.log.Timber import java.io.File @@ -76,8 +75,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: activeSessionHolder.getActiveSession().fileService() .downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = data.eventId, fileName = data.filename, mimeType = data.mimeType, url = data.url, @@ -116,8 +113,6 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: activeSessionHolder.getActiveSession().fileService() .downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = data.eventId, fileName = data.filename, mimeType = data.mimeType, url = data.url, diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index f1149d8990..a7d69c783c 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.rx.rx import javax.inject.Inject @@ -111,7 +112,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti private fun PermalinkData.RoomLink.getRoomId(): Single> { val session = activeSessionHolder.getSafeActiveSession() return if (isRoomAlias && session != null) { - session.rx().getRoomIdByAlias(roomIdOrAlias, true).subscribeOn(Schedulers.io()) + session.rx().getRoomIdByAlias(roomIdOrAlias, true).map { it.getOrNull()?.roomId.toOptional() }.subscribeOn(Schedulers.io()) } else { Single.just(Optional.from(roomIdOrAlias)) } @@ -149,16 +150,28 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti navigator.openRoom(context, roomId, eventId, buildTask) } else -> { - val roomPreviewData = RoomPreviewData( - roomId = roomId, - eventId = eventId, - roomAlias = roomAlias ?: roomSummary?.canonicalAlias, - roomName = roomSummary?.displayName, - avatarUrl = roomSummary?.avatarUrl, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) + if (roomSummary == null) { + // we don't know this room, try to peek + val roomPreviewData = RoomPreviewData( + roomId = roomId, + roomAlias = roomAlias, + peekFromServer = true, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } else { + val roomPreviewData = RoomPreviewData( + roomId = roomId, + eventId = eventId, + roomAlias = roomAlias ?: roomSummary.canonicalAlias, + roomName = roomSummary.displayName, + avatarUrl = roomSummary.avatarUrl, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt index b6e238c2dc..1aa4846f38 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt @@ -56,6 +56,7 @@ class PinFragment @Inject constructor( when (fragmentArgs.pinMode) { PinMode.CREATE -> showCreateFragment() PinMode.AUTH -> showAuthFragment() + PinMode.MODIFY -> showCreateFragment() // No need to create another function for now because texts are generic } } @@ -73,6 +74,10 @@ class PinFragment @Inject constructor( Toast.makeText(requireContext(), getString(R.string.create_pin_confirm_failure), Toast.LENGTH_SHORT).show() } + override fun onPinCodeEnteredFirst(pinCode: String?): Boolean { + return false + } + override fun onCodeCreated(encodedCode: String) { lifecycleScope.launch { pinCodeStore.storeEncodedPin(encodedCode) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinMode.kt b/vector/src/main/java/im/vector/app/features/pin/PinMode.kt index c24ac5adf2..9801912bd6 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinMode.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinMode.kt @@ -18,5 +18,6 @@ package im.vector.app.features.pin enum class PinMode { CREATE, - AUTH + AUTH, + MODIFY } diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index b2257b250a..c677a99175 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -130,7 +130,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy() { var listener: Listener? = null @@ -104,13 +104,8 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin value(viewState.roomType.aliasLocalPart) homeServer(":" + viewState.homeServerName) errorMessage( - when ((viewState.asyncCreateRoomRequest as? Fail)?.error) { - is CreateRoomFailure.RoomAliasError.AliasEmpty -> R.string.create_room_alias_empty - is CreateRoomFailure.RoomAliasError.AliasNotAvailable -> R.string.create_room_alias_already_in_use - is CreateRoomFailure.RoomAliasError.AliasInvalid -> R.string.create_room_alias_invalid - else -> null - } - ?.let { stringProvider.getString(it) } + roomAliasErrorFormatter.format( + (((viewState.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) ) onTextChange { value -> listener?.setAliasLocalPart(value) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index fb90752764..204a99929b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -84,7 +84,7 @@ class CreateRoomFragment @Inject constructor( override fun showFailure(throwable: Throwable) { // Note: RoomAliasError are displayed directly in the form - if (throwable !is CreateRoomFailure.RoomAliasError) { + if (throwable !is CreateRoomFailure.AliasError) { super.showFailure(throwable) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt index 041a5c5c51..2a30545a47 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt @@ -27,6 +27,7 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_room_alias_text_input) @@ -62,9 +63,7 @@ abstract class RoomAliasEditItem : VectorEpoxyModel() holder.textInputLayout.error = errorMessage // Update only if text is different and value is not null - if (value != null && holder.textInputEditText.text.toString() != value) { - holder.textInputEditText.setText(value) - } + holder.textInputEditText.setTextSafe(value) holder.textInputEditText.isEnabled = enabled holder.textInputEditText.addTextChangedListener(onTextChangeListener) holder.homeServerText.text = homeServer diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt new file mode 100644 index 0000000000..7a23a79ab3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasErrorFormatter.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory.createroom + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import javax.inject.Inject + +class RoomAliasErrorFormatter @Inject constructor( + private val stringProvider: StringProvider +) { + fun format(roomAliasError: RoomAliasError?): String? { + return when (roomAliasError) { + is RoomAliasError.AliasEmpty -> R.string.create_room_alias_empty + is RoomAliasError.AliasNotAvailable -> R.string.create_room_alias_already_in_use + is RoomAliasError.AliasInvalid -> R.string.create_room_alias_invalid + else -> null + } + ?.let { stringProvider.getString(it) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt new file mode 100644 index 0000000000..918264e594 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/PeekingState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory.roompreview + +enum class PeekingState { + FOUND, + NOT_FOUND, + NO_ACCESS +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 4876bd7f98..3585c8bbf2 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -40,6 +40,7 @@ data class RoomPreviewData( val worldReadable: Boolean = false, val avatarUrl: String? = null, val homeServers: List = emptyList(), + val peekFromServer: Boolean = false, val buildTask: Boolean = false ) : Parcelable { val matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 108c3bacf1..bc4552fc11 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -20,6 +20,8 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import androidx.transition.TransitionManager +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -30,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomdirectory.JoinState import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.* +import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject /** @@ -48,22 +51,6 @@ class RoomPreviewNoPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(roomPreviewNoPreviewToolbar) - val titleText = roomPreviewData.roomName ?: roomPreviewData.roomAlias ?: roomPreviewData.roomId - - // Toolbar - avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar) - roomPreviewNoPreviewToolbarTitle.text = titleText - - // Screen - avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar) - roomPreviewNoPreviewName.text = titleText - roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic) - - if (roomPreviewData.worldReadable) { - roomPreviewNoPreviewLabel.setText(R.string.room_preview_world_readable_room_not_supported_yet) - } else { - roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview) - } roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback { override fun onButtonClicked() { @@ -100,7 +87,62 @@ class RoomPreviewNoPreviewFragment @Inject constructor( // Quit this screen requireActivity().finish() // Open room - navigator.openRoom(requireActivity(), roomPreviewData.roomId, roomPreviewData.eventId, roomPreviewData.buildTask) + navigator.openRoom(requireActivity(), state.roomId, roomPreviewData.eventId, roomPreviewData.buildTask) + } + + val bestName = state.roomName ?: state.roomAlias ?: state.roomId + when (state.peekingState) { + is Loading -> { + roomPreviewPeekingProgress.isVisible = true + roomPreviewNoPreviewJoin.isVisible = false + } + is Success -> { + roomPreviewPeekingProgress.isVisible = false + when (state.peekingState.invoke()) { + PeekingState.FOUND -> { + // show join buttons + roomPreviewNoPreviewJoin.isVisible = true + renderState(bestName, state.matrixItem(), state.roomTopic) + } + PeekingState.NO_ACCESS -> { + roomPreviewNoPreviewJoin.isVisible = true + roomPreviewNoPreviewLabel.isVisible = true + roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview_join) + renderState(bestName, state.matrixItem().takeIf { state.roomAlias != null }, state.roomTopic) + } + else -> { + roomPreviewNoPreviewJoin.isVisible = false + roomPreviewNoPreviewLabel.isVisible = true + roomPreviewNoPreviewLabel.setText(R.string.room_preview_not_found) + renderState(bestName, null, state.roomTopic) + } + } + } + else -> { + // Render with initial state, no peeking + roomPreviewPeekingProgress.isVisible = false + roomPreviewNoPreviewJoin.isVisible = true + renderState(bestName, state.matrixItem(), state.roomTopic) + roomPreviewNoPreviewLabel.isVisible = false + } } } + + private fun renderState(roomName: String, matrixItem: MatrixItem?, topic: String?) { + // Toolbar + if (matrixItem != null) { + roomPreviewNoPreviewToolbarAvatar.isVisible = true + roomPreviewNoPreviewAvatar.isVisible = true + avatarRenderer.render(matrixItem, roomPreviewNoPreviewToolbarAvatar) + avatarRenderer.render(matrixItem, roomPreviewNoPreviewAvatar) + } else { + roomPreviewNoPreviewToolbarAvatar.isVisible = false + roomPreviewNoPreviewAvatar.isVisible = false + } + roomPreviewNoPreviewToolbarTitle.text = roomName + + // Screen + roomPreviewNoPreviewName.text = roomName + roomPreviewNoPreviewTopic.setTextOrHide(topic) + } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 900ba537b5..72c4c58a42 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -16,8 +16,11 @@ package im.vector.app.features.roomdirectory.roompreview +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -25,12 +28,17 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.roomdirectory.JoinState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber @@ -56,6 +64,56 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini // Observe joined room (from the sync) observeRoomSummary() observeMembershipChanges() + + if (initialState.shouldPeekFromServer) { + peekRoomFromServer() + } + } + + private fun peekRoomFromServer() { + setState { + copy(peekingState = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + val peekResult = tryOrNull { + awaitCallback { + session.peekRoom(initialState.roomAlias ?: initialState.roomId, it) + } + } + + when (peekResult) { + is PeekResult.Success -> { + setState { + copy( + roomId = peekResult.roomId, + avatarUrl = peekResult.avatarUrl, + roomAlias = peekResult.alias ?: initialState.roomAlias, + roomTopic = peekResult.topic, + homeServers = peekResult.viaServers, + peekingState = Success(PeekingState.FOUND) + ) + } + } + is PeekResult.PeekingNotAllowed -> { + setState { + copy( + roomId = peekResult.roomId, + roomAlias = peekResult.alias ?: initialState.roomAlias, + homeServers = peekResult.viaServers, + peekingState = Success(PeekingState.NO_ACCESS) + ) + } + } + PeekResult.UnknownAlias, + null -> { + setState { + copy( + peekingState = Success(PeekingState.NOT_FOUND) + ) + } + } + } + } } private fun observeRoomSummary() { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt index 6816e54481..5a73f29ca3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -16,13 +16,23 @@ package im.vector.app.features.roomdirectory.roompreview +import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomdirectory.JoinState +import org.matrix.android.sdk.api.util.MatrixItem data class RoomPreviewViewState( + val peekingState: Async = Uninitialized, // The room id val roomId: String = "", val roomAlias: String? = null, + + val roomName: String? = null, + val roomTopic: String? = null, + val avatarUrl: String? = null, + + val shouldPeekFromServer: Boolean = false, /** * Can be empty when the server is the current user's home server. */ @@ -36,6 +46,14 @@ data class RoomPreviewViewState( constructor(args: RoomPreviewData) : this( roomId = args.roomId, roomAlias = args.roomAlias, - homeServers = args.homeServers + homeServers = args.homeServers, + roomName = args.roomName, + roomTopic = args.topic, + avatarUrl = args.avatarUrl, + shouldPeekFromServer = args.peekFromServer ) + + fun matrixItem() : MatrixItem { + return MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) + } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 78562ea351..39b5884308 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -166,9 +166,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v viewModelScope.launch { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) try { - awaitCallback { - room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it) - } + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent()) _viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess) } catch (failure: Throwable) { _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 609042ffa4..696725d001 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -36,6 +36,7 @@ import im.vector.app.features.room.RequireActiveMembershipViewState import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment +import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import javax.inject.Inject @@ -98,10 +99,11 @@ class RoomProfileActivity : .observe() .subscribe { sharedAction -> when (sharedAction) { - is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() - is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() - is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() - is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() + is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() + is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() + is RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() + is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() + is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() } } .disposeOnDestroy() @@ -135,6 +137,10 @@ class RoomProfileActivity : addFragmentToBackstack(R.id.simpleFragmentContainer, RoomSettingsFragment::class.java, roomProfileArgs) } + private fun openRoomAlias() { + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomAliasFragment::class.java, roomProfileArgs) + } + private fun openRoomMembers() { addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt index 0052ddee99..83a610cf1b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt @@ -23,6 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction */ sealed class RoomProfileSharedAction : VectorSharedAction { object OpenRoomSettings : RoomProfileSharedAction() + object OpenRoomAliasesSettings : RoomProfileSharedAction() object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasAction.kt new file mode 100644 index 0000000000..80e1603453 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasAction.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility + +sealed class RoomAliasAction : VectorViewModelAction { + // Canonical + object ToggleManualPublishForm : RoomAliasAction() + data class SetNewAlias(val alias: String) : RoomAliasAction() + object ManualPublishAlias : RoomAliasAction() + data class PublishAlias(val alias: String) : RoomAliasAction() + data class UnpublishAlias(val alias: String) : RoomAliasAction() + data class SetCanonicalAlias(val canonicalAlias: String?) : RoomAliasAction() + + // Room directory + data class SetRoomDirectoryVisibility(val roomDirectoryVisibility: RoomDirectoryVisibility) : RoomAliasAction() + + // Local + data class RemoveLocalAlias(val alias: String) : RoomAliasAction() + object ToggleAddLocalAliasForm : RoomAliasAction() + data class SetNewLocalAliasLocalPart(val aliasLocalPart: String) : RoomAliasAction() + object AddLocalAlias : RoomAliasAction() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt new file mode 100644 index 0000000000..0b695031c5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias + +import android.text.InputType +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.epoxy.profiles.buildProfileSection +import im.vector.app.core.epoxy.profiles.profileActionItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.discovery.settingsButtonItem +import im.vector.app.features.discovery.settingsContinueCancelItem +import im.vector.app.features.discovery.settingsInfoItem +import im.vector.app.features.form.formEditTextItem +import im.vector.app.features.form.formSwitchItem +import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import im.vector.app.features.roomdirectory.createroom.roomAliasEditItem +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import javax.inject.Inject + +class RoomAliasController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter, + private val colorProvider: ColorProvider, + private val roomAliasErrorFormatter: RoomAliasErrorFormatter +) : TypedEpoxyController() { + + interface Callback { + fun toggleManualPublishForm() + fun setNewAlias(alias: String) + fun addAlias() + fun setRoomDirectoryVisibility(roomDirectoryVisibility: RoomDirectoryVisibility) + fun toggleLocalAliasForm() + fun setNewLocalAliasLocalPart(aliasLocalPart: String) + fun addLocalAlias() + fun openAliasDetail(alias: String) + } + + var callback: Callback? = null + + init { + setData(null) + } + + override fun buildModels(data: RoomAliasViewState?) { + data ?: return + + // Published alias + buildPublishInfo(data) + // Room directory visibility + buildRoomDirectoryVisibility(data) + // Local alias + buildLocalInfo(data) + } + + private fun buildRoomDirectoryVisibility(data: RoomAliasViewState) { + when (data.roomDirectoryVisibility) { + Uninitialized -> Unit + is Loading -> Unit + is Success -> { + formSwitchItem { + id("roomVisibility") + title(stringProvider.getString(R.string.room_alias_publish_to_directory, data.homeServerName)) + showDivider(false) + switchChecked(data.roomDirectoryVisibility() == RoomDirectoryVisibility.PUBLIC) + listener { + if (it) { + callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PUBLIC) + } else { + callback?.setRoomDirectoryVisibility(RoomDirectoryVisibility.PRIVATE) + } + } + } + } + is Fail -> { + errorWithRetryItem { + text(stringProvider.getString(R.string.room_alias_publish_to_directory_error, + errorFormatter.toHumanReadable(data.roomDirectoryVisibility.error))) + } + } + } + } + + private fun buildPublishInfo(data: RoomAliasViewState) { + buildProfileSection( + stringProvider.getString(R.string.room_alias_published_alias_title) + ) + settingsInfoItem { + id("publishedInfo") + helperTextResId(R.string.room_alias_published_alias_subtitle) + } + + data.canonicalAlias + ?.takeIf { it.isNotEmpty() } + ?.let { canonicalAlias -> + + profileActionItem { + id("canonical") + title(data.canonicalAlias) + subtitle(stringProvider.getString(R.string.room_alias_published_alias_main)) + listener { callback?.openAliasDetail(canonicalAlias) } + } + } + + if (data.alternativeAliases.isEmpty()) { + settingsInfoItem { + id("otherPublishedEmpty") + if (data.actionPermissions.canChangeCanonicalAlias) { + helperTextResId(R.string.room_alias_address_empty_can_add) + } else { + helperTextResId(R.string.room_alias_address_empty) + } + } + } else { + settingsInfoItem { + id("otherPublished") + helperTextResId(R.string.room_alias_published_other) + } + data.alternativeAliases.forEachIndexed { idx, altAlias -> + profileActionItem { + id("alt_$idx") + title(altAlias) + listener { callback?.openAliasDetail(altAlias) } + } + } + } + + if (data.actionPermissions.canChangeCanonicalAlias) { + buildPublishManuallyForm(data) + } + } + + private fun buildPublishManuallyForm(data: RoomAliasViewState) { + when (data.publishManuallyState) { + RoomAliasViewState.AddAliasState.Hidden -> Unit + RoomAliasViewState.AddAliasState.Closed -> { + settingsButtonItem { + id("publishManually") + colorProvider(colorProvider) + buttonTitleId(R.string.room_alias_published_alias_add_manually) + buttonClickListener { callback?.toggleManualPublishForm() } + } + } + is RoomAliasViewState.AddAliasState.Editing -> { + formEditTextItem { + id("publishManuallyEdit") + value(data.publishManuallyState.value) + showBottomSeparator(false) + hint(stringProvider.getString(R.string.room_alias_address_hint)) + inputType(InputType.TYPE_CLASS_TEXT) + onTextChange { text -> + callback?.setNewAlias(text) + } + } + settingsContinueCancelItem { + id("publishManuallySubmit") + continueText(stringProvider.getString(R.string.room_alias_published_alias_add_manually_submit)) + continueOnClick { callback?.addAlias() } + cancelOnClick { callback?.toggleManualPublishForm() } + } + } + } + } + + private fun buildLocalInfo(data: RoomAliasViewState) { + buildProfileSection( + stringProvider.getString(R.string.room_alias_local_address_title) + ) + settingsInfoItem { + id("localInfo") + helperText(stringProvider.getString(R.string.room_alias_local_address_subtitle, data.homeServerName)) + } + + when (val localAliases = data.localAliases) { + is Uninitialized -> { + loadingItem { + id("loadingAliases") + } + } + is Success -> { + if (localAliases().isEmpty()) { + settingsInfoItem { + id("locEmpty") + helperTextResId(R.string.room_alias_local_address_empty) + } + } else { + localAliases().forEachIndexed { idx, localAlias -> + profileActionItem { + id("loc_$idx") + title(localAlias) + listener { callback?.openAliasDetail(localAlias) } + } + } + } + } + is Fail -> { + errorWithRetryItem { + id("alt_error") + text(errorFormatter.toHumanReadable(localAliases.error)) + } + } + } + + // Add local + buildAddLocalAlias(data) + } + + private fun buildAddLocalAlias(data: RoomAliasViewState) { + when (data.newLocalAliasState) { + RoomAliasViewState.AddAliasState.Hidden -> Unit + RoomAliasViewState.AddAliasState.Closed -> { + settingsButtonItem { + id("newLocalAliasButton") + colorProvider(colorProvider) + buttonTitleId(R.string.room_alias_local_address_add) + buttonClickListener { callback?.toggleLocalAliasForm() } + } + } + is RoomAliasViewState.AddAliasState.Editing -> { + roomAliasEditItem { + id("newLocalAlias") + value(data.newLocalAliasState.value) + homeServer(":" + data.homeServerName) + showBottomSeparator(false) + errorMessage(roomAliasErrorFormatter.format((data.newLocalAliasState.asyncRequest as? Fail)?.error as? RoomAliasError)) + onTextChange { value -> + callback?.setNewLocalAliasLocalPart(value) + } + } + settingsContinueCancelItem { + id("newLocalAliasSubmit") + continueText(stringProvider.getString(R.string.action_add)) + continueOnClick { callback?.addLocalAlias() } + cancelOnClick { callback?.toggleLocalAliasForm() } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt new file mode 100644 index 0000000000..56c3e76828 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasFragment.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.dialogs.withColoredButton +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.shareText +import im.vector.app.core.utils.toast +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedAction +import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel +import kotlinx.android.synthetic.main.fragment_room_setting_generic.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class RoomAliasFragment @Inject constructor( + val viewModelFactory: RoomAliasViewModel.Factory, + private val controller: RoomAliasController, + private val avatarRenderer: AvatarRenderer +) : + VectorBaseFragment(), + RoomAliasController.Callback { + + private val viewModel: RoomAliasViewModel by fragmentViewModel() + private lateinit var sharedActionViewModel: RoomAliasBottomSheetSharedActionViewModel + + private val roomProfileArgs: RoomProfileArgs by args() + + override fun getLayoutResId() = R.layout.fragment_room_setting_generic + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(RoomAliasBottomSheetSharedActionViewModel::class.java) + + controller.callback = this + setupToolbar(roomSettingsToolbar) + roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) + waiting_view_status_text.setText(R.string.please_wait) + waiting_view_status_text.isVisible = true + + viewModel.observeViewEvents { + when (it) { + is RoomAliasViewEvents.Failure -> showFailure(it.throwable) + RoomAliasViewEvents.Success -> showSuccess() + }.exhaustive + } + + sharedActionViewModel + .observe() + .subscribe { handleAliasAction(it) } + .disposeOnDestroyView() + } + + private fun handleAliasAction(action: RoomAliasBottomSheetSharedAction?) { + when (action) { + is RoomAliasBottomSheetSharedAction.ShareAlias -> shareAlias(action.matrixTo) + is RoomAliasBottomSheetSharedAction.PublishAlias -> viewModel.handle(RoomAliasAction.PublishAlias(action.alias)) + is RoomAliasBottomSheetSharedAction.UnPublishAlias -> unpublishAlias(action.alias) + is RoomAliasBottomSheetSharedAction.DeleteAlias -> removeLocalAlias(action.alias) + is RoomAliasBottomSheetSharedAction.SetMainAlias -> viewModel.handle(RoomAliasAction.SetCanonicalAlias(action.alias)) + RoomAliasBottomSheetSharedAction.UnsetMainAlias -> viewModel.handle(RoomAliasAction.SetCanonicalAlias(canonicalAlias = null)) + null -> Unit + } + } + + private fun shareAlias(matrixTo: String) { + shareText(requireContext(), matrixTo) + } + + override fun showFailure(throwable: Throwable) { + if (throwable !is RoomAliasError) { + super.showFailure(throwable) + } + } + + private fun showSuccess() { + activity?.toast(R.string.room_settings_save_success) + } + + override fun onDestroyView() { + controller.callback = null + roomSettingsRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + waiting_view.isVisible = state.isLoading + controller.setData(state) + renderRoomSummary(state) + } + + private fun renderRoomSummary(state: RoomAliasViewState) { + state.roomSummary()?.let { + roomSettingsToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView) + } + } + + private fun unpublishAlias(alias: String) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_confirmation) + .setMessage(getString(R.string.room_alias_unpublish_confirmation, alias)) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.action_unpublish) { _, _ -> + viewModel.handle(RoomAliasAction.UnpublishAlias(alias)) + } + .show() + .withColoredButton(DialogInterface.BUTTON_POSITIVE) + } + + override fun toggleManualPublishForm() { + viewModel.handle(RoomAliasAction.ToggleManualPublishForm) + } + + override fun setNewAlias(alias: String) { + viewModel.handle(RoomAliasAction.SetNewAlias(alias)) + } + + override fun addAlias() { + viewModel.handle(RoomAliasAction.ManualPublishAlias) + } + + override fun setRoomDirectoryVisibility(roomDirectoryVisibility: RoomDirectoryVisibility) { + viewModel.handle(RoomAliasAction.SetRoomDirectoryVisibility(roomDirectoryVisibility)) + } + + override fun toggleLocalAliasForm() { + viewModel.handle(RoomAliasAction.ToggleAddLocalAliasForm) + } + + override fun setNewLocalAliasLocalPart(aliasLocalPart: String) { + viewModel.handle(RoomAliasAction.SetNewLocalAliasLocalPart(aliasLocalPart)) + } + + override fun addLocalAlias() { + viewModel.handle(RoomAliasAction.AddLocalAlias) + } + + override fun openAliasDetail(alias: String) = withState(viewModel) { state -> + RoomAliasBottomSheet + .newInstance( + alias = alias, + isPublished = alias in state.allPublishedAliases, + isMainAlias = alias == state.canonicalAlias, + isLocal = alias in state.localAliases().orEmpty(), + canEditCanonicalAlias = state.actionPermissions.canChangeCanonicalAlias + ) + .show(childFragmentManager, "ROOM_ALIAS_ACTIONS") + } + + private fun removeLocalAlias(alias: String) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_confirmation) + .setMessage(getString(R.string.room_alias_delete_confirmation, alias)) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.delete) { _, _ -> + viewModel.handle(RoomAliasAction.RemoveLocalAlias(alias)) + } + .show() + .withColoredButton(DialogInterface.BUTTON_POSITIVE) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewEvents.kt new file mode 100644 index 0000000000..bbd44741b5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewEvents.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.app.features.roomprofile.alias + +import im.vector.app.core.platform.VectorViewEvents + +/** + * Transient events for room settings screen + */ +sealed class RoomAliasViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : RoomAliasViewEvents() + object Success : RoomAliasViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt new file mode 100644 index 0000000000..f470eeefc2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt @@ -0,0 +1,382 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.rx.mapOptional +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: RoomAliasViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomAliasViewState): RoomAliasViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomAliasViewState): RoomAliasViewModel? { + val fragment: RoomAliasFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + initHomeServerName() + observeRoomSummary() + observePowerLevel() + observeRoomCanonicalAlias() + fetchRoomAlias() + fetchRoomDirectoryVisibility() + } + + private fun fetchRoomDirectoryVisibility() { + setState { + copy( + roomDirectoryVisibility = Loading() + ) + } + viewModelScope.launch { + runCatching { + session.getRoomDirectoryVisibility(room.roomId) + }.fold( + { + setState { + copy( + roomDirectoryVisibility = Success(it) + ) + } + }, + { + setState { + copy( + roomDirectoryVisibility = Fail(it) + ) + } + } + ) + } + } + + private fun initHomeServerName() { + setState { + copy( + homeServerName = session.myUserId.substringAfter(":") + ) + } + } + + private fun fetchRoomAlias() { + setState { + copy( + localAliases = Loading() + ) + } + + viewModelScope.launch { + runCatching { room.getRoomAliases() } + .fold( + { + setState { copy(localAliases = Success(it.sorted())) } + }, + { + setState { copy(localAliases = Fail(it)) } + } + ) + } + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { async -> + copy( + roomSummary = async + ) + } + } + + private fun observePowerLevel() { + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + val permissions = RoomAliasViewState.ActionPermissions( + canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend( + userId = session.myUserId, + isState = true, + eventType = EventType.STATE_ROOM_CANONICAL_ALIAS + ) + ) + setState { + val newPublishManuallyState = if (permissions.canChangeCanonicalAlias) { + when (publishManuallyState) { + RoomAliasViewState.AddAliasState.Hidden -> RoomAliasViewState.AddAliasState.Closed + else -> publishManuallyState + } + } else { + RoomAliasViewState.AddAliasState.Hidden + } + copy( + actionPermissions = permissions, + publishManuallyState = newPublishManuallyState + ) + } + } + .disposeOnClear() + } + + /** + * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. + */ + private fun observeRoomCanonicalAlias() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_CANONICAL_ALIAS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + setState { + copy( + canonicalAlias = it.canonicalAlias, + alternativeAliases = it.alternativeAliases.orEmpty().sorted() + ) + } + } + .disposeOnClear() + } + + override fun handle(action: RoomAliasAction) { + when (action) { + RoomAliasAction.ToggleManualPublishForm -> handleToggleManualPublishForm() + is RoomAliasAction.SetNewAlias -> handleSetNewAlias(action) + is RoomAliasAction.ManualPublishAlias -> handleManualPublishAlias() + is RoomAliasAction.UnpublishAlias -> handleUnpublishAlias(action) + is RoomAliasAction.SetCanonicalAlias -> handleSetCanonicalAlias(action) + is RoomAliasAction.SetRoomDirectoryVisibility -> handleSetRoomDirectoryVisibility(action) + RoomAliasAction.ToggleAddLocalAliasForm -> handleToggleAddLocalAliasForm() + is RoomAliasAction.SetNewLocalAliasLocalPart -> handleSetNewLocalAliasLocalPart(action) + RoomAliasAction.AddLocalAlias -> handleAddLocalAlias() + is RoomAliasAction.RemoveLocalAlias -> handleRemoveLocalAlias(action) + is RoomAliasAction.PublishAlias -> handlePublishAlias(action) + }.exhaustive + } + + private fun handleSetRoomDirectoryVisibility(action: RoomAliasAction.SetRoomDirectoryVisibility) { + postLoading(true) + viewModelScope.launch { + runCatching { + session.setRoomDirectoryVisibility(room.roomId, action.roomDirectoryVisibility) + }.fold( + { + setState { + copy( + isLoading = false, + // Local echo, no need to fetch the data from the server again + roomDirectoryVisibility = Success(action.roomDirectoryVisibility) + ) + } + }, + { + postLoading(false) + _viewEvents.post(RoomAliasViewEvents.Failure(it)) + } + ) + } + } + + private fun handleToggleAddLocalAliasForm() { + setState { + copy( + newLocalAliasState = when (newLocalAliasState) { + RoomAliasViewState.AddAliasState.Hidden -> RoomAliasViewState.AddAliasState.Hidden + RoomAliasViewState.AddAliasState.Closed -> RoomAliasViewState.AddAliasState.Editing("", Uninitialized) + is RoomAliasViewState.AddAliasState.Editing -> RoomAliasViewState.AddAliasState.Closed + } + ) + } + } + + private fun handleToggleManualPublishForm() { + setState { + copy( + publishManuallyState = when (publishManuallyState) { + RoomAliasViewState.AddAliasState.Hidden -> RoomAliasViewState.AddAliasState.Hidden + RoomAliasViewState.AddAliasState.Closed -> RoomAliasViewState.AddAliasState.Editing("", Uninitialized) + is RoomAliasViewState.AddAliasState.Editing -> RoomAliasViewState.AddAliasState.Closed + } + ) + } + } + + private fun handleSetNewAlias(action: RoomAliasAction.SetNewAlias) { + setState { + copy( + publishManuallyState = RoomAliasViewState.AddAliasState.Editing(action.alias, Uninitialized) + ) + } + } + + private fun handleSetNewLocalAliasLocalPart(action: RoomAliasAction.SetNewLocalAliasLocalPart) { + setState { + copy( + newLocalAliasState = RoomAliasViewState.AddAliasState.Editing(action.aliasLocalPart, Uninitialized) + ) + } + } + + private fun handleManualPublishAlias() = withState { state -> + val newAlias = (state.publishManuallyState as? RoomAliasViewState.AddAliasState.Editing)?.value ?: return@withState + updateCanonicalAlias( + canonicalAlias = state.canonicalAlias, + alternativeAliases = state.alternativeAliases + newAlias, + closeForm = true + ) + } + + private fun handlePublishAlias(action: RoomAliasAction.PublishAlias) = withState { state -> + updateCanonicalAlias( + canonicalAlias = state.canonicalAlias, + alternativeAliases = state.alternativeAliases + action.alias, + closeForm = false + ) + } + + private fun handleUnpublishAlias(action: RoomAliasAction.UnpublishAlias) = withState { state -> + updateCanonicalAlias( + // We can also unpublish the canonical alias + canonicalAlias = state.canonicalAlias.takeIf { it != action.alias }, + alternativeAliases = state.alternativeAliases - action.alias, + closeForm = false + ) + } + + private fun handleSetCanonicalAlias(action: RoomAliasAction.SetCanonicalAlias) = withState { state -> + updateCanonicalAlias( + canonicalAlias = action.canonicalAlias, + // Ensure the previous canonical alias is moved to the alt aliases + alternativeAliases = state.allPublishedAliases, + closeForm = false + ) + } + + private fun updateCanonicalAlias(canonicalAlias: String?, alternativeAliases: List, closeForm: Boolean) { + postLoading(true) + viewModelScope.launch { + try { + room.updateCanonicalAlias(canonicalAlias, alternativeAliases) + setState { + copy( + isLoading = false, + publishManuallyState = if (closeForm) RoomAliasViewState.AddAliasState.Closed else publishManuallyState + ) + } + } catch (failure: Throwable) { + postLoading(false) + _viewEvents.post(RoomAliasViewEvents.Failure(failure)) + } + } + } + + private fun handleAddLocalAlias() = withState { state -> + val previousState = (state.newLocalAliasState as? RoomAliasViewState.AddAliasState.Editing) ?: return@withState + + setState { + copy( + isLoading = true, + newLocalAliasState = previousState.copy(asyncRequest = Loading()) + ) + } + viewModelScope.launch { + runCatching { room.addAlias(previousState.value) } + .onFailure { + setState { + copy( + isLoading = false, + newLocalAliasState = previousState.copy(asyncRequest = Fail(it)) + ) + } + _viewEvents.post(RoomAliasViewEvents.Failure(it)) + } + .onSuccess { + setState { + copy( + isLoading = false, + newLocalAliasState = RoomAliasViewState.AddAliasState.Closed, + // Local echo + localAliases = Success((localAliases().orEmpty() + previousState.value).sorted()) + ) + } + fetchRoomAlias() + } + } + } + + private fun handleRemoveLocalAlias(action: RoomAliasAction.RemoveLocalAlias) { + postLoading(true) + viewModelScope.launch { + runCatching { session.deleteRoomAlias(action.alias) } + .onFailure { + setState { + copy(isLoading = false) + } + _viewEvents.post(RoomAliasViewEvents.Failure(it)) + } + .onSuccess { + // Local echo + setState { + copy( + isLoading = false, + // Local echo + localAliases = Success(localAliases().orEmpty() - action.alias) + ) + } + fetchRoomAlias() + } + } + } + + private fun postLoading(isLoading: Boolean) { + setState { + copy(isLoading = isLoading) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt new file mode 100644 index 0000000000..f6341f4f64 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewState.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.roomprofile.RoomProfileArgs +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class RoomAliasViewState( + val roomId: String, + val homeServerName: String = "", + val roomSummary: Async = Uninitialized, + val actionPermissions: ActionPermissions = ActionPermissions(), + val roomDirectoryVisibility: Async = Uninitialized, + val isLoading: Boolean = false, + val canonicalAlias: String? = null, + val alternativeAliases: List = emptyList(), + val publishManuallyState: AddAliasState = AddAliasState.Hidden, + val localAliases: Async> = Uninitialized, + val newLocalAliasState: AddAliasState = AddAliasState.Closed +) : MvRxState { + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + val allPublishedAliases: List + get() = (alternativeAliases + listOfNotNull(canonicalAlias)).distinct() + + data class ActionPermissions( + val canChangeCanonicalAlias: Boolean = false + ) + + sealed class AddAliasState { + object Hidden : AddAliasState() + object Closed : AddAliasState() + data class Editing(val value: String, val asyncRequest: Async = Uninitialized) : AddAliasState() + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt new file mode 100644 index 0000000000..86702d1507 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheet.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias.detail + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import kotlinx.android.parcel.Parcelize +import javax.inject.Inject + +@Parcelize +data class RoomAliasBottomSheetArgs( + val alias: String, + val isPublished: Boolean, + val isMainAlias: Boolean, + val isLocal: Boolean, + val canEditCanonicalAlias: Boolean +) : Parcelable + +/** + * Bottom sheet fragment that shows room alias information with list of contextual actions + */ +class RoomAliasBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomAliasBottomSheetController.Listener { + + private lateinit var sharedActionViewModel: RoomAliasBottomSheetSharedActionViewModel + @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool + @Inject lateinit var roomAliasBottomSheetViewModelFactory: RoomAliasBottomSheetViewModel.Factory + @Inject lateinit var controller: RoomAliasBottomSheetController + + private val viewModel: RoomAliasBottomSheetViewModel by fragmentViewModel(RoomAliasBottomSheetViewModel::class) + + @BindView(R.id.bottomSheetRecyclerView) + lateinit var recyclerView: RecyclerView + + override val showExpanded = true + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutResId() = R.layout.bottom_sheet_generic_list + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + sharedActionViewModel = activityViewModelProvider.get(RoomAliasBottomSheetSharedActionViewModel::class.java) + recyclerView.configureWith(controller, viewPool = sharedViewPool, hasFixedSize = false, disableItemAnimation = true) + controller.listener = this + } + + override fun onDestroyView() { + recyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { + controller.setData(it) + super.invalidate() + } + + override fun didSelectMenuAction(quickAction: RoomAliasBottomSheetSharedAction) { + sharedActionViewModel.post(quickAction) + + dismiss() + } + + companion object { + fun newInstance(alias: String, + isPublished: Boolean, + isMainAlias: Boolean, + isLocal: Boolean, + canEditCanonicalAlias: Boolean): RoomAliasBottomSheet { + return RoomAliasBottomSheet().apply { + setArguments(RoomAliasBottomSheetArgs( + alias = alias, + isPublished = isPublished, + isMainAlias = isMainAlias, + isLocal = isLocal, + canEditCanonicalAlias = canEditCanonicalAlias + )) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt new file mode 100644 index 0000000000..157037c13d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetController.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.roomprofile.alias.detail + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem +import im.vector.app.core.epoxy.dividerItem +import im.vector.app.core.epoxy.profiles.profileActionItem +import javax.inject.Inject + +/** + * Epoxy controller for room alias actions + */ +class RoomAliasBottomSheetController @Inject constructor() : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(state: RoomAliasBottomSheetState) { + profileActionItem { + id("alias") + title(state.alias) + subtitle(state.matrixToLink) + editable(false) + } + + // Notifications + dividerItem { + id("aliasSeparator") + } + + var idx = 0 + // Share + state.matrixToLink?.let { + RoomAliasBottomSheetSharedAction.ShareAlias(it).toBottomSheetItem(++idx) + } + + // Action on published alias + if (state.isPublished) { + // Published address + if (state.canEditCanonicalAlias) { + if (state.isMainAlias) { + RoomAliasBottomSheetSharedAction.UnsetMainAlias.toBottomSheetItem(++idx) + } else { + RoomAliasBottomSheetSharedAction.SetMainAlias(state.alias).toBottomSheetItem(++idx) + } + RoomAliasBottomSheetSharedAction.UnPublishAlias(state.alias).toBottomSheetItem(++idx) + } + } + + if (state.isLocal) { + // Local address + if (state.canEditCanonicalAlias && state.isPublished.not()) { + // Publish + RoomAliasBottomSheetSharedAction.PublishAlias(state.alias).toBottomSheetItem(++idx) + } + // Delete + RoomAliasBottomSheetSharedAction.DeleteAlias(state.alias).toBottomSheetItem(++idx) + } + } + + private fun RoomAliasBottomSheetSharedAction.toBottomSheetItem(index: Int) { + return bottomSheetActionItem { + id("action_$index") + iconRes(iconResId) + textRes(titleRes) + destructive(this@toBottomSheetItem.destructive) + listener(View.OnClickListener { listener?.didSelectMenuAction(this@toBottomSheetItem) }) + } + } + + interface Listener { + fun didSelectMenuAction(quickAction: RoomAliasBottomSheetSharedAction) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt new file mode 100644 index 0000000000..13909c401f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedAction.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias.detail + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import im.vector.app.R +import im.vector.app.core.platform.VectorSharedAction + +sealed class RoomAliasBottomSheetSharedAction( + @StringRes val titleRes: Int, + @DrawableRes val iconResId: Int = 0, + val destructive: Boolean = false) + : VectorSharedAction { + + data class ShareAlias(val matrixTo: String) : RoomAliasBottomSheetSharedAction( + R.string.share, + R.drawable.ic_material_share + ) + + data class PublishAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.room_alias_action_publish + ) + + data class UnPublishAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.room_alias_action_unpublish + ) + + data class DeleteAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.delete, + R.drawable.ic_trash_24, + true + ) + + data class SetMainAlias(val alias: String) : RoomAliasBottomSheetSharedAction( + R.string.room_settings_set_main_address + ) + + object UnsetMainAlias : RoomAliasBottomSheetSharedAction( + R.string.room_settings_unset_main_address + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedActionViewModel.kt new file mode 100644 index 0000000000..5f71783515 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetSharedActionViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.app.features.roomprofile.alias.detail + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +/** + * Activity shared view model to handle room alias quick actions + */ +class RoomAliasBottomSheetSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt new file mode 100644 index 0000000000..a61075cef6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetState.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.alias.detail + +import com.airbnb.mvrx.MvRxState + +data class RoomAliasBottomSheetState( + val alias: String, + val matrixToLink: String? = null, + val isPublished: Boolean, + val isMainAlias: Boolean, + val isLocal: Boolean, + val canEditCanonicalAlias: Boolean +) : MvRxState { + + constructor(args: RoomAliasBottomSheetArgs) : this( + alias = args.alias, + isPublished = args.isPublished, + isMainAlias = args.isMainAlias, + isLocal = args.isLocal, + canEditCanonicalAlias = args.canEditCanonicalAlias + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt new file mode 100644 index 0000000000..7f723cae53 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.roomprofile.alias.detail + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import org.matrix.android.sdk.api.session.Session + +class RoomAliasBottomSheetViewModel @AssistedInject constructor( + @Assisted initialState: RoomAliasBottomSheetState, + session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel? { + val fragment: RoomAliasBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.roomAliasBottomSheetViewModelFactory.create(state) + } + } + + init { + setState { + copy( + matrixToLink = session.permalinkService().createPermalink(alias) + ) + } + } + + override fun handle(action: EmptyAction) { + // No op + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListAction.kt similarity index 82% rename from vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListAction.kt index ca7d567d90..8f6f5afba1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListAction.kt @@ -19,8 +19,8 @@ package im.vector.app.features.roomprofile.banned import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -sealed class RoomBannedListMemberAction : VectorViewModelAction { - data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() - data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() - data class Filter(val filter: String) : RoomBannedListMemberAction() +sealed class RoomBannedMemberListAction : VectorViewModelAction { + data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedMemberListAction() + data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedMemberListAction() + data class Filter(val filter: String) : RoomBannedMemberListAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index 797e6c8aa3..349321c87a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -37,18 +37,18 @@ import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomBannedMemberListFragment @Inject constructor( - val viewModelFactory: RoomBannedListMemberViewModel.Factory, + val viewModelFactory: RoomBannedMemberListViewModel.Factory, private val roomMemberListController: RoomBannedMemberListController, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), RoomBannedMemberListController.Callback { - private val viewModel: RoomBannedListMemberViewModel by fragmentViewModel() + private val viewModel: RoomBannedMemberListViewModel by fragmentViewModel() private val roomProfileArgs: RoomProfileArgs by args() override fun getLayoutResId() = R.layout.fragment_room_setting_generic override fun onUnbanClicked(roomMember: RoomMemberSummary) { - viewModel.handle(RoomBannedListMemberAction.QueryInfo(roomMember)) + viewModel.handle(RoomBannedMemberListAction.QueryInfo(roomMember)) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -60,7 +60,7 @@ class RoomBannedMemberListFragment @Inject constructor( viewModel.observeViewEvents { when (it) { - is RoomBannedViewEvents.ShowBannedInfo -> { + is RoomBannedMemberListViewEvents.ShowBannedInfo -> { val canBan = withState(viewModel) { state -> state.canUserBan } AlertDialog.Builder(requireActivity()) .setTitle(getString(R.string.member_banned_by, it.bannedByUserId)) @@ -69,13 +69,13 @@ class RoomBannedMemberListFragment @Inject constructor( .apply { if (canBan) { setNegativeButton(R.string.room_participants_action_unban) { _, _ -> - viewModel.handle(RoomBannedListMemberAction.UnBanUser(it.roomMemberSummary)) + viewModel.handle(RoomBannedMemberListAction.UnBanUser(it.roomMemberSummary)) } } } .show() } - is RoomBannedViewEvents.ToastError -> { + is RoomBannedMemberListViewEvents.ToastError -> { requireActivity().toast(it.info) } } @@ -96,7 +96,7 @@ class RoomBannedMemberListFragment @Inject constructor( } override fun onQueryTextChange(newText: String): Boolean { - viewModel.handle(RoomBannedListMemberAction.Filter(newText)) + viewModel.handle(RoomBannedMemberListAction.Filter(newText)) return true } }) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewEvents.kt similarity index 83% rename from vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedViewEvents.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewEvents.kt index 6b59debe96..4b1dc018ee 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewEvents.kt @@ -19,7 +19,7 @@ package im.vector.app.features.roomprofile.banned import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -sealed class RoomBannedViewEvents : VectorViewEvents { - data class ShowBannedInfo(val bannedByUserId: String, val banReason: String, val roomMemberSummary: RoomMemberSummary) : RoomBannedViewEvents() - data class ToastError(val info: String) : RoomBannedViewEvents() +sealed class RoomBannedMemberListViewEvents : VectorViewEvents { + data class ShowBannedInfo(val bannedByUserId: String, val banReason: String, val roomMemberSummary: RoomMemberSummary) : RoomBannedMemberListViewEvents() + data class ToastError(val info: String) : RoomBannedMemberListViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt similarity index 84% rename from vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt index 1cce2f96cb..0cecd22fa0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt @@ -42,14 +42,14 @@ import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap -class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initialState: RoomBannedMemberListViewState, +class RoomBannedMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomBannedMemberListViewState, private val stringProvider: StringProvider, private val session: Session) - : VectorViewModel(initialState) { + : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { - fun create(initialState: RoomBannedMemberListViewState): RoomBannedListMemberViewModel + fun create(initialState: RoomBannedMemberListViewState): RoomBannedMemberListViewModel } private val room = session.getRoom(initialState.roomId)!! @@ -78,24 +78,24 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia }.disposeOnClear() } - companion object : MvRxViewModelFactory { + companion object : MvRxViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomBannedMemberListViewState): RoomBannedListMemberViewModel? { + override fun create(viewModelContext: ViewModelContext, state: RoomBannedMemberListViewState): RoomBannedMemberListViewModel? { val fragment: RoomBannedMemberListFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.viewModelFactory.create(state) } } - override fun handle(action: RoomBannedListMemberAction) { + override fun handle(action: RoomBannedMemberListAction) { when (action) { - is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary) - is RoomBannedListMemberAction.UnBanUser -> unBanUser(action.roomMemberSummary) - is RoomBannedListMemberAction.Filter -> handleFilter(action) + is RoomBannedMemberListAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary) + is RoomBannedMemberListAction.UnBanUser -> unBanUser(action.roomMemberSummary) + is RoomBannedMemberListAction.Filter -> handleFilter(action) }.exhaustive } - private fun handleFilter(action: RoomBannedListMemberAction.Filter) { + private fun handleFilter(action: RoomBannedMemberListAction.Filter) { setState { copy( filter = action.filter @@ -114,7 +114,7 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia val reason = content.reason val bannedBy = bannedEvent?.senderId ?: return - _viewEvents.post(RoomBannedViewEvents.ShowBannedInfo(bannedBy, reason ?: "", roomMemberSummary)) + _viewEvents.post(RoomBannedMemberListViewEvents.ShowBannedInfo(bannedBy, reason ?: "", roomMemberSummary)) } private fun unBanUser(roomMemberSummary: RoomMemberSummary) { @@ -127,7 +127,7 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia room.unban(roomMemberSummary.userId, null, it) } } catch (failure: Throwable) { - _viewEvents.post(RoomBannedViewEvents.ToastError(stringProvider.getString(R.string.failed_to_unban))) + _viewEvents.post(RoomBannedMemberListViewEvents.ToastError(stringProvider.getString(R.string.failed_to_unban))) } finally { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 9e402c675b..9f15e62b3b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -30,7 +30,6 @@ import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.BiFunction import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue @@ -197,8 +196,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState room.sendStateEvent( eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE, stateKey = action.stateKey, - body = emptyMap(), - callback = NoOpMatrixCallback() + body = emptyMap() ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt index f0a7b38478..867c605030 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt @@ -17,14 +17,17 @@ package im.vector.app.features.roomprofile.settings import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules sealed class RoomSettingsAction : VectorViewModelAction { data class SetAvatarAction(val avatarAction: RoomSettingsViewState.AvatarAction) : RoomSettingsAction() data class SetRoomName(val newName: String) : RoomSettingsAction() data class SetRoomTopic(val newTopic: String) : RoomSettingsAction() data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction() - data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction() + data class SetRoomJoinRule(val roomJoinRule: RoomJoinRules?, val roomGuestAccess: GuestAccess?) : RoomSettingsAction() + object Save : RoomSettingsAction() object Cancel : RoomSettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 3c73e6ed46..bf3c1f87f8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -26,9 +26,8 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -46,7 +45,8 @@ class RoomSettingsController @Inject constructor( fun onNameChanged(name: String) fun onTopicChanged(topic: String) fun onHistoryVisibilityClicked() - fun onAliasChanged(alias: String) + fun onRoomAliasesClicked() + fun onJoinRuleClicked() } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -60,20 +60,17 @@ class RoomSettingsController @Inject constructor( override fun buildModels(data: RoomSettingsViewState?) { val roomSummary = data?.roomSummary?.invoke() ?: return - val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: "" - val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) } - formEditableAvatarItem { id("avatar") enabled(data.actionPermissions.canChangeAvatar) when (val avatarAction = data.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> { + RoomSettingsViewState.AvatarAction.None -> { // Use the current value avatarRenderer(avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) } - RoomSettingsViewState.AvatarAction.DeleteAvatar -> + RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null) is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri) @@ -108,30 +105,48 @@ class RoomSettingsController @Inject constructor( } } - formEditTextItem { - id("alias") - enabled(data.actionPermissions.canChangeCanonicalAlias) - value(data.newCanonicalAlias ?: roomSummary.canonicalAlias) - hint(stringProvider.getString(R.string.room_settings_addresses_add_new_address)) - - onTextChange { text -> - callback?.onAliasChanged(text) - } - } + buildProfileAction( + id = "alias", + title = stringProvider.getString(R.string.room_settings_alias_title), + subtitle = stringProvider.getString(R.string.room_settings_alias_subtitle), + dividerColor = dividerColor, + divider = true, + editable = true, + action = { callback?.onRoomAliasesClicked() } + ) buildProfileAction( id = "historyReadability", title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), - subtitle = newHistoryVisibility ?: historyVisibility, + subtitle = roomHistoryVisibilityFormatter.getSetting(data.newHistoryVisibility ?: data.currentHistoryVisibility), + dividerColor = dividerColor, + divider = true, + editable = data.actionPermissions.canChangeHistoryVisibility, + action = { if (data.actionPermissions.canChangeHistoryVisibility) callback?.onHistoryVisibilityClicked() } + ) + + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_title), + subtitle = data.getJoinRuleWording(), dividerColor = dividerColor, divider = false, - editable = data.actionPermissions.canChangeHistoryReadability, - action = { if (data.actionPermissions.canChangeHistoryReadability) callback?.onHistoryVisibilityClicked() } + editable = data.actionPermissions.canChangeJoinRule, + action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } ) } - private fun formatRoomHistoryVisibilityEvent(event: Event): String? { - val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null - return roomHistoryVisibilityFormatter.format(historyVisibility) + private fun RoomSettingsViewState.getJoinRuleWording(): String { + val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules + val guestAccess = newRoomJoinRules.newGuestAccess ?: currentGuestAccess + return stringProvider.getString(if (joinRule == RoomJoinRules.INVITE) { + R.string.room_settings_room_access_entry_only_invited + } else { + if (guestAccess == GuestAccess.CanJoin) { + R.string.room_settings_room_access_entry_anyone_with_link_including_guest + } else { + R.string.room_settings_room_access_entry_anyone_with_link_apart_guest + } + }) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 6637b7d943..d8c8c41936 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -37,13 +37,15 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.toast import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.RoomProfileSharedAction +import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel +import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel import kotlinx.android.synthetic.main.fragment_room_setting_generic.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.util.toMatrixItem import java.util.UUID import javax.inject.Inject @@ -51,7 +53,6 @@ import javax.inject.Inject class RoomSettingsFragment @Inject constructor( val viewModelFactory: RoomSettingsViewModel.Factory, private val controller: RoomSettingsController, - private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, colorProvider: ColorProvider, private val avatarRenderer: AvatarRenderer ) : @@ -61,6 +62,10 @@ class RoomSettingsFragment @Inject constructor( GalleryOrCameraDialogHelper.Listener { private val viewModel: RoomSettingsViewModel by fragmentViewModel() + private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel + private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel + private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel + private val roomProfileArgs: RoomProfileArgs by args() private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) @@ -70,6 +75,9 @@ class RoomSettingsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) + setupRoomHistoryVisibilitySharedActionViewModel() + setupRoomJoinRuleSharedActionViewModel() controller.callback = this setupToolbar(roomSettingsToolbar) roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) @@ -88,6 +96,26 @@ class RoomSettingsFragment @Inject constructor( } } + private fun setupRoomJoinRuleSharedActionViewModel() { + roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) + roomJoinRuleSharedActionViewModel + .observe() + .subscribe { action -> + viewModel.handle(RoomSettingsAction.SetRoomJoinRule(action.roomJoinRule, action.roomGuestAccess)) + } + .disposeOnDestroyView() + } + + private fun setupRoomHistoryVisibilitySharedActionViewModel() { + roomHistoryVisibilitySharedActionViewModel = activityViewModelProvider.get(RoomHistoryVisibilitySharedActionViewModel::class.java) + roomHistoryVisibilitySharedActionViewModel + .observe() + .subscribe { action -> + viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(action.roomHistoryVisibility)) + } + .disposeOnDestroyView() + } + private fun showSuccess() { activity?.toast(R.string.room_settings_save_success) } @@ -137,35 +165,20 @@ class RoomSettingsFragment @Inject constructor( } override fun onHistoryVisibilityClicked() = withState(viewModel) { state -> - val historyVisibilities = arrayOf( - RoomHistoryVisibility.SHARED, - RoomHistoryVisibility.INVITED, - RoomHistoryVisibility.JOINED, - RoomHistoryVisibility.WORLD_READABLE - ) - val currentHistoryVisibility = - state.newHistoryVisibility ?: state.historyVisibilityEvent?.getClearContent().toModel()?.historyVisibility - val currentHistoryVisibilityIndex = historyVisibilities.indexOf(currentHistoryVisibility) - - AlertDialog.Builder(requireContext()).apply { - setTitle(R.string.room_settings_room_read_history_rules_pref_title) - setSingleChoiceItems( - historyVisibilities - .map { roomHistoryVisibilityFormatter.format(it) } - .toTypedArray(), - currentHistoryVisibilityIndex) { dialog, which -> - if (which != currentHistoryVisibilityIndex) { - viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(historyVisibilities[which])) - } - dialog.cancel() - } - show() - } - return@withState + val currentHistoryVisibility = state.newHistoryVisibility ?: state.currentHistoryVisibility + RoomHistoryVisibilityBottomSheet.newInstance(currentHistoryVisibility) + .show(childFragmentManager, "RoomHistoryVisibilityBottomSheet") } - override fun onAliasChanged(alias: String) { - viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias)) + override fun onRoomAliasesClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings) + } + + override fun onJoinRuleClicked() = withState(viewModel) { state -> + val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules + val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess + RoomJoinRuleBottomSheet.newInstance(currentJoinRule, currentGuestAccess) + .show(childFragmentManager, "RoomJoinRuleBottomSheet") } override fun onImageReady(uri: Uri?) { @@ -182,10 +195,10 @@ class RoomSettingsFragment @Inject constructor( override fun onAvatarDelete() { withState(viewModel) { when (it.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> { + RoomSettingsViewState.AvatarAction.None -> { viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar)) } - RoomSettingsViewState.AvatarAction.DeleteAvatar -> { + RoomSettingsViewState.AvatarAction.DeleteAvatar -> { /* Should not happen */ Unit } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 05a75a585b..48ff38f92e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -33,6 +33,9 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent +import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.rx.mapOptional import org.matrix.android.sdk.rx.rx @@ -60,6 +63,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: init { observeRoomSummary() + observeRoomHistoryVisibility() + observeJoinRule() + observeGuestAccess() observeRoomAvatar() observeState() } @@ -68,14 +74,14 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: selectSubscribe( RoomSettingsViewState::avatarAction, RoomSettingsViewState::newName, - RoomSettingsViewState::newCanonicalAlias, RoomSettingsViewState::newTopic, RoomSettingsViewState::newHistoryVisibility, + RoomSettingsViewState::newRoomJoinRules, RoomSettingsViewState::roomSummary) { avatarAction, newName, - newCanonicalAlias, newTopic, newHistoryVisibility, + newJoinRule, asyncSummary -> val summary = asyncSummary() setState { @@ -83,8 +89,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None || summary?.name != newName || summary?.topic != newTopic - || summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() } - || newHistoryVisibility != null + || (newHistoryVisibility != null && newHistoryVisibility != currentHistoryVisibility) + || newJoinRule.hasChanged() ) } } @@ -96,11 +102,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: .execute { async -> val roomSummary = async.invoke() copy( - historyVisibilityEvent = room.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY), roomSummary = async, newName = roomSummary?.name, - newTopic = roomSummary?.topic, - newCanonicalAlias = roomSummary?.canonicalAlias + newTopic = roomSummary?.topic ) } @@ -113,16 +117,57 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR), canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME), canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC), - canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_CANONICAL_ALIAS), - canChangeHistoryReadability = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_HISTORY_VISIBILITY) + canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_ROOM_HISTORY_VISIBILITY), + canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_ROOM_JOIN_RULES) + && powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_ROOM_GUEST_ACCESS) ) setState { copy(actionPermissions = permissions) } } .disposeOnClear() } + private fun observeRoomHistoryVisibility() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + it.historyVisibility?.let { + setState { copy(currentHistoryVisibility = it) } + } + } + .disposeOnClear() + } + + private fun observeGuestAccess() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + it.joinRules?.let { + setState { copy(currentRoomJoinRules = it) } + } + } + .disposeOnClear() + } + + private fun observeJoinRule() { + room.rx() + .liveStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { + it.guestAccess?.let { + setState { copy(currentGuestAccess = it) } + } + } + .disposeOnClear() + } + /** * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. */ @@ -143,12 +188,21 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) } is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) } is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) } - is RoomSettingsAction.SetRoomCanonicalAlias -> setState { copy(newCanonicalAlias = action.newCanonicalAlias) } + is RoomSettingsAction.SetRoomJoinRule -> handleSetRoomJoinRule(action) is RoomSettingsAction.Save -> saveSettings() is RoomSettingsAction.Cancel -> cancel() }.exhaustive } + private fun handleSetRoomJoinRule(action: RoomSettingsAction.SetRoomJoinRule) = withState { state -> + setState { + copy(newRoomJoinRules = RoomSettingsViewState.NewJoinRule( + action.roomJoinRule.takeIf { it != state.currentRoomJoinRules }, + action.roomGuestAccess.takeIf { it != state.currentGuestAccess } + )) + } + } + private fun handleSetAvatarAction(action: RoomSettingsAction.SetAvatarAction) { setState { deletePendingAvatar(this) @@ -191,15 +245,14 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: operationList.add(room.rx().updateTopic(state.newTopic ?: "")) } - if (state.newCanonicalAlias != null && summary?.canonicalAlias != state.newCanonicalAlias.takeIf { it.isNotEmpty() }) { - operationList.add(room.rx().addRoomAlias(state.newCanonicalAlias)) - operationList.add(room.rx().updateCanonicalAlias(state.newCanonicalAlias)) - } - if (state.newHistoryVisibility != null) { operationList.add(room.rx().updateHistoryReadability(state.newHistoryVisibility)) } + if (state.newRoomJoinRules.hasChanged()) { + operationList.add(room.rx().updateJoinRule(state.newRoomJoinRules.newJoinRules, state.newRoomJoinRules.newGuestAccess)) + } + Observable .fromIterable(operationList) .concatMapCompletable { it } @@ -210,7 +263,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: deletePendingAvatar(this) copy( avatarAction = RoomSettingsViewState.AvatarAction.None, - newHistoryVisibility = null + newHistoryVisibility = null, + newRoomJoinRules = RoomSettingsViewState.NewJoinRule() ) } _viewEvents.post(RoomSettingsViewEvents.Success) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index 2cadc8f798..7403917d48 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -21,13 +21,17 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomprofile.RoomProfileArgs -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary data class RoomSettingsViewState( val roomId: String, - val historyVisibilityEvent: Event? = null, + // Default value: https://matrix.org/docs/spec/client_server/r0.6.1#id88 + val currentHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.SHARED, + val currentRoomJoinRules: RoomJoinRules = RoomJoinRules.INVITE, + val currentGuestAccess: GuestAccess? = null, val roomSummary: Async = Uninitialized, val isLoading: Boolean = false, val currentRoomAvatarUrl: String? = null, @@ -35,7 +39,7 @@ data class RoomSettingsViewState( val newName: String? = null, val newTopic: String? = null, val newHistoryVisibility: RoomHistoryVisibility? = null, - val newCanonicalAlias: String? = null, + val newRoomJoinRules: NewJoinRule = NewJoinRule(), val showSaveAction: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { @@ -46,8 +50,8 @@ data class RoomSettingsViewState( val canChangeAvatar: Boolean = false, val canChangeName: Boolean = false, val canChangeTopic: Boolean = false, - val canChangeCanonicalAlias: Boolean = false, - val canChangeHistoryReadability: Boolean = false + val canChangeHistoryVisibility: Boolean = false, + val canChangeJoinRule: Boolean = false ) sealed class AvatarAction { @@ -56,4 +60,11 @@ data class RoomSettingsViewState( data class UpdateAvatar(val newAvatarUri: Uri, val newAvatarFileName: String) : AvatarAction() } + + data class NewJoinRule( + val newJoinRules: RoomJoinRules? = null, + val newGuestAccess: GuestAccess? = null + ) { + fun hasChanged() = newJoinRules != null || newGuestAccess != null + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityAction.kt new file mode 100644 index 0000000000..3c989a7dbe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityAction.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.historyvisibility + +import androidx.annotation.DrawableRes +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericAction +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility + +class RoomHistoryVisibilityAction( + val roomHistoryVisibility: RoomHistoryVisibility, + title: String, + @DrawableRes iconResId: Int, + isSelected: Boolean +) : BottomSheetGenericAction( + title = title, + iconResId = iconResId, + isSelected = isSelected, + destructive = false +) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt new file mode 100644 index 0000000000..c12dc621a9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityBottomSheet.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.historyvisibility + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.ui.bottomsheet.BottomSheetGeneric +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import javax.inject.Inject + +@Parcelize +data class RoomHistoryVisibilityBottomSheetArgs( + val currentRoomHistoryVisibility: RoomHistoryVisibility +) : Parcelable + +class RoomHistoryVisibilityBottomSheet : BottomSheetGeneric() { + + private lateinit var roomHistoryVisibilitySharedActionViewModel: RoomHistoryVisibilitySharedActionViewModel + @Inject lateinit var controller: RoomHistoryVisibilityController + private val viewModel: RoomHistoryVisibilityViewModel by fragmentViewModel(RoomHistoryVisibilityViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getController(): BottomSheetGenericController = controller + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + roomHistoryVisibilitySharedActionViewModel = activityViewModelProvider.get(RoomHistoryVisibilitySharedActionViewModel::class.java) + } + + override fun didSelectAction(action: RoomHistoryVisibilityAction) { + roomHistoryVisibilitySharedActionViewModel.post(action) + dismiss() + } + + override fun invalidate() = withState(viewModel) { + controller.setData(it) + super.invalidate() + } + + companion object { + fun newInstance(currentRoomHistoryVisibility: RoomHistoryVisibility): RoomHistoryVisibilityBottomSheet { + return RoomHistoryVisibilityBottomSheet().apply { + setArguments(RoomHistoryVisibilityBottomSheetArgs(currentRoomHistoryVisibility)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityController.kt new file mode 100644 index 0000000000..a4899711f7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityController.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.historyvisibility + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import javax.inject.Inject + +class RoomHistoryVisibilityController @Inject constructor( + private val historyVisibilityFormatter: RoomHistoryVisibilityFormatter, + private val stringProvider: StringProvider +) : BottomSheetGenericController() { + + override fun getTitle() = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_dialog_title) + + override fun getSubTitle() = stringProvider.getString(R.string.room_settings_room_read_history_dialog_subtitle) + + override fun getActions(state: RoomHistoryVisibilityState): List { + return listOf( + RoomHistoryVisibility.WORLD_READABLE, + RoomHistoryVisibility.SHARED, + RoomHistoryVisibility.INVITED, + RoomHistoryVisibility.JOINED + ) + .map { roomHistoryVisibility -> + RoomHistoryVisibilityAction( + roomHistoryVisibility = roomHistoryVisibility, + title = historyVisibilityFormatter.getSetting(roomHistoryVisibility), + iconResId = 0, + isSelected = roomHistoryVisibility == state.currentRoomHistoryVisibility + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt new file mode 100644 index 0000000000..31c1c2631c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilitySharedActionViewModel.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.app.features.roomprofile.settings.historyvisibility + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class RoomHistoryVisibilitySharedActionViewModel @Inject constructor() + : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityState.kt new file mode 100644 index 0000000000..0b651d5664 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.historyvisibility + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericState +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility + +data class RoomHistoryVisibilityState( + val currentRoomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.SHARED +) : BottomSheetGenericState() { + + constructor(args: RoomHistoryVisibilityBottomSheetArgs) : this(currentRoomHistoryVisibility = args.currentRoomHistoryVisibility) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt new file mode 100644 index 0000000000..c2a8ae967f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/historyvisibility/RoomHistoryVisibilityViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.historyvisibility + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel + +class RoomHistoryVisibilityViewModel(initialState: RoomHistoryVisibilityState) + : BottomSheetGenericViewModel(initialState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAction.kt new file mode 100644 index 0000000000..6f71669002 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAction.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.joinrule + +import androidx.annotation.DrawableRes +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericAction +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules + +class RoomJoinRuleAction( + val roomJoinRule: RoomJoinRules, + val roomGuestAccess: GuestAccess?, + title: String, + @DrawableRes iconResId: Int, + isSelected: Boolean +) : BottomSheetGenericAction( + title = title, + iconResId = iconResId, + isSelected = isSelected, + destructive = false +) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt new file mode 100644 index 0000000000..66c6be6086 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.joinrule + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.ui.bottomsheet.BottomSheetGeneric +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +@Parcelize +data class RoomJoinRuleBottomSheetArgs( + val currentRoomJoinRule: RoomJoinRules, + val currentGuestAccess: GuestAccess? +) : Parcelable + +class RoomJoinRuleBottomSheet : BottomSheetGeneric() { + + private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel + @Inject lateinit var controller: RoomJoinRuleController + private val viewModel: RoomJoinRuleViewModel by fragmentViewModel(RoomJoinRuleViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getController(): BottomSheetGenericController = controller + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) + } + + override fun didSelectAction(action: RoomJoinRuleAction) { + roomJoinRuleSharedActionViewModel.post(action) + dismiss() + } + + override fun invalidate() = withState(viewModel) { + controller.setData(it) + super.invalidate() + } + + companion object { + fun newInstance(currentRoomJoinRule: RoomJoinRules, currentGuestAccess: GuestAccess?): RoomJoinRuleBottomSheet { + return RoomJoinRuleBottomSheet().apply { + setArguments(RoomJoinRuleBottomSheetArgs(currentRoomJoinRule, currentGuestAccess)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt new file mode 100644 index 0000000000..ab00396dbe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.joinrule + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericController +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +class RoomJoinRuleController @Inject constructor( + private val stringProvider: StringProvider +) : BottomSheetGenericController() { + + override fun getTitle() = stringProvider.getString(R.string.room_settings_room_access_rules_pref_dialog_title) + + override fun getActions(state: RoomJoinRuleState): List { + return listOf( + RoomJoinRuleAction( + roomJoinRule = RoomJoinRules.INVITE, + roomGuestAccess = null, + title = stringProvider.getString(R.string.room_settings_room_access_entry_only_invited), + iconResId = 0, + isSelected = state.currentRoomJoinRule == RoomJoinRules.INVITE + ), + RoomJoinRuleAction( + roomJoinRule = RoomJoinRules.PUBLIC, + roomGuestAccess = GuestAccess.Forbidden, + title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_apart_guest), + iconResId = 0, + isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC && state.currentGuestAccess == GuestAccess.Forbidden + ), + RoomJoinRuleAction( + roomJoinRule = RoomJoinRules.PUBLIC, + roomGuestAccess = GuestAccess.CanJoin, + title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_including_guest), + iconResId = 0, + isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC && state.currentGuestAccess == GuestAccess.CanJoin + ) + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt new file mode 100644 index 0000000000..934b0dfc76 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleSharedActionViewModel.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package im.vector.app.features.roomprofile.settings.joinrule + +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class RoomJoinRuleSharedActionViewModel @Inject constructor() + : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt new file mode 100644 index 0000000000..ec16b02d60 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.joinrule + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericState +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules + +data class RoomJoinRuleState( + val currentRoomJoinRule: RoomJoinRules = RoomJoinRules.INVITE, + val currentGuestAccess: GuestAccess? = null +) : BottomSheetGenericState() { + + constructor(args: RoomJoinRuleBottomSheetArgs) : this( + currentRoomJoinRule = args.currentRoomJoinRule, + currentGuestAccess = args.currentGuestAccess + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt new file mode 100644 index 0000000000..4305bfa72d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.settings.joinrule + +import im.vector.app.core.ui.bottomsheet.BottomSheetGenericViewModel + +class RoomJoinRuleViewModel(initialState: RoomJoinRuleState) + : BottomSheetGenericViewModel(initialState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt index 76b1a9e0c3..b62b633a36 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -30,11 +30,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.model.message.MessageType -import org.matrix.android.sdk.api.session.room.model.message.getFileUrl -import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult -import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -90,9 +86,7 @@ class RoomUploadsViewModel @AssistedInject constructor( viewModelScope.launch { try { - val result = awaitCallback { - room.getUploads(20, token, it) - } + val result = room.getUploads(20, token) token = result.nextToken @@ -137,12 +131,7 @@ class RoomUploadsViewModel @AssistedInject constructor( try { val file = awaitCallback { session.fileService().downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = action.uploadEvent.eventId, - fileName = action.uploadEvent.contentWithAttachmentContent.body, - url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(), - mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType, - elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), + messageContent = action.uploadEvent.contentWithAttachmentContent, callback = it ) } @@ -158,12 +147,7 @@ class RoomUploadsViewModel @AssistedInject constructor( try { val file = awaitCallback { session.fileService().downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = action.uploadEvent.eventId, - fileName = action.uploadEvent.contentWithAttachmentContent.body, - mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType, - url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(), - elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), + messageContent = action.uploadEvent.contentWithAttachmentContent, callback = it) } _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 5872c1fa1c..16be2b1552 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -97,6 +97,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY = "SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY" private const val SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY" private const val SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER" + private const val SETTINGS_ENABLE_CHAT_EFFECTS = "SETTINGS_ENABLE_CHAT_EFFECTS" // Help private const val SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY = "SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY" @@ -165,6 +166,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { // Security const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE" const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG" + const val SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG = "SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG" private const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG" private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG" const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" @@ -782,6 +784,15 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false) } + /** + * Tells if the user wants to see URL previews in the timeline + * + * @return true if the user wants to see URL previews in the timeline + */ + fun showUrlPreviews(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true) + } + /** * Enable or disable the analytics tracking. * @@ -859,6 +870,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG, true) } + fun chatEffectsEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_ENABLE_CHAT_EFFECTS, true) + } + /** * Return true if Pin code is disabled, or if user set the settings to see full notification content */ diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt index b1ccabfb76..5a7ceb4084 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt @@ -58,7 +58,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService @@ -214,7 +213,9 @@ class VectorSettingsGeneralFragment @Inject constructor( it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> // Disable it while updating the state, will be re-enabled by the account data listener. it.isEnabled = false - session.integrationManagerService().setIntegrationEnabled(newValue as Boolean, NoOpMatrixCallback()) + lifecycleScope.launch { + session.integrationManagerService().setIntegrationEnabled(newValue as Boolean) + } true } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt index 37465258f6..1a04dab950 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt @@ -21,6 +21,7 @@ import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.preference.VectorPreference import im.vector.app.features.navigation.Navigator import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.pin.PinCodeStore @@ -41,6 +42,10 @@ class VectorSettingsPinFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!! } + private val changePinCodePref by lazy { + findPreference(VectorPreferences.SETTINGS_SECURITY_CHANGE_PIN_CODE_FLAG)!! + } + private val useCompleteNotificationPref by lazy { findPreference(VectorPreferences.SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG)!! } @@ -74,6 +79,17 @@ class VectorSettingsPinFragment @Inject constructor( } true } + + changePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (hasPinCode) { + navigator.openPinCode( + requireContext(), + pinActivityResultLauncher, + PinMode.MODIFY + ) + } + true + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index a84a10f74c..841a239701 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -22,7 +22,6 @@ import android.widget.CheckedTextView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.preference.Preference -import androidx.preference.SwitchPreference import im.vector.app.R import im.vector.app.core.extensions.restart import im.vector.app.core.preference.VectorListPreference @@ -64,9 +63,9 @@ class VectorSettingsPreferencesFragment @Inject constructor( } // Url preview + /* + TODO Note: we keep the setting client side for now findPreference(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let { - /* - TODO it.isChecked = session.isURLPreviewEnabled it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> @@ -100,8 +99,8 @@ class VectorSettingsPreferencesFragment @Inject constructor( false } - */ } + */ // update keep medias period findPreference(VectorPreferences.SETTINGS_MEDIA_SAVING_PERIOD_KEY)!!.let { diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 989fc0aadb..89fa4a982a 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -107,7 +107,7 @@ class SoftLogoutController @Inject constructor( } is Success -> { when (state.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Password -> { + LoginMode.Password -> { loginPasswordFormItem { id("passwordForm") stringProvider(stringProvider) @@ -120,21 +120,23 @@ class SoftLogoutController @Inject constructor( submitClickListener { password -> listener?.signinSubmit(password) } } } - LoginMode.Sso -> { + is LoginMode.Sso -> { loginCenterButtonItem { id("sso") text(stringProvider.getString(R.string.login_signin_sso)) listener { listener?.signinFallbackSubmit() } } } - LoginMode.Unsupported -> { + is LoginMode.SsoAndPassword -> { + } + LoginMode.Unsupported -> { loginCenterButtonItem { id("fallback") text(stringProvider.getString(R.string.login_signin)) listener { listener?.signinFallbackSubmit() } } } - LoginMode.Unknown -> Unit // Should not happen + LoginMode.Unknown -> Unit // Should not happen } } } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index dbd5028401..b85c9a8763 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -54,14 +54,27 @@ class SoftLogoutFragment @Inject constructor( softLogoutViewModel.subscribe(this) { softLogoutViewState -> softLogoutController.update(softLogoutViewState) - - when (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Sso, + when (val mode = softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { + is LoginMode.SsoAndPassword -> { + loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + mode.ssoIdentityProviders + )) + } + is LoginMode.Sso -> { + loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + mode.ssoIdentityProviders + )) + } LoginMode.Unsupported -> { // Prepare the loginViewModel for a SSO/login fallback recovery loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, - softLogoutViewState.deviceId + softLogoutViewState.deviceId, + null )) } else -> Unit diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 4f6110aab1..f1d9a66342 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -105,9 +105,11 @@ class SoftLogoutViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } setState { diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt index df822807ee..89d6e970cc 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt @@ -28,8 +28,6 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.terms.GetTermsResponse -import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber class ReviewTermsViewModel @AssistedInject constructor( @@ -94,15 +92,12 @@ class ReviewTermsViewModel @AssistedInject constructor( viewModelScope.launch { try { - awaitCallback { - session.agreeToTerms( - termsArgs.type, - termsArgs.baseURL, - agreedUrls, - termsArgs.token, - it - ) - } + session.agreeToTerms( + termsArgs.type, + termsArgs.baseURL, + agreedUrls, + termsArgs.token + ) _viewEvents.post(ReviewTermsViewEvents.Success) } catch (failure: Throwable) { Timber.e(failure, "Failed to agree to terms") @@ -122,9 +117,7 @@ class ReviewTermsViewModel @AssistedInject constructor( viewModelScope.launch { try { - val data = awaitCallback { - session.getTerms(termsArgs.type, termsArgs.baseURL, it) - } + val data = session.getTerms(termsArgs.type, termsArgs.baseURL) val terms = data.serverResponse.getLocalizedTerms(action.preferredLanguageCode).map { Term(it.localizedUrl ?: "", it.localizedName ?: "", diff --git a/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt index 3aba6a4dad..847caeab4c 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt @@ -24,23 +24,19 @@ import im.vector.app.R * Note that style for light theme is default and is declared in the Android Manifest */ sealed class ActivityOtherThemes(@StyleRes val dark: Int, - @StyleRes val black: Int, - @StyleRes val status: Int) { + @StyleRes val black: Int) { object Default : ActivityOtherThemes( R.style.AppTheme_Dark, - R.style.AppTheme_Black, - R.style.AppTheme_Status + R.style.AppTheme_Black ) object AttachmentsPreview : ActivityOtherThemes( - R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview ) object VectorAttachmentsPreview : ActivityOtherThemes( - R.style.AppTheme_Transparent, R.style.AppTheme_Transparent, R.style.AppTheme_Transparent ) diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index 18faa07954..bba6b9c253 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -24,6 +24,7 @@ import android.view.Menu import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.core.content.ContextCompat +import androidx.core.content.edit import androidx.core.graphics.drawable.DrawableCompat import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences @@ -41,7 +42,6 @@ object ThemeUtils { private const val THEME_DARK_VALUE = "dark" private const val THEME_LIGHT_VALUE = "light" private const val THEME_BLACK_VALUE = "black" - private const val THEME_STATUS_VALUE = "status" private var currentTheme = AtomicReference(null) @@ -58,9 +58,8 @@ object ThemeUtils { */ fun isLightTheme(context: Context): Boolean { return when (getApplicationTheme(context)) { - THEME_LIGHT_VALUE, - THEME_STATUS_VALUE -> true - else -> false + THEME_LIGHT_VALUE -> true + else -> false } } @@ -73,8 +72,13 @@ object ThemeUtils { fun getApplicationTheme(context: Context): String { val currentTheme = this.currentTheme.get() return if (currentTheme == null) { - val themeFromPref = DefaultSharedPreferences.getInstance(context) - .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + val prefs = DefaultSharedPreferences.getInstance(context) + var themeFromPref = prefs.getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + if (themeFromPref == "status") { + // Migrate to light theme, which is the closest theme + themeFromPref = THEME_LIGHT_VALUE + prefs.edit { putString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) } + } this.currentTheme.set(themeFromPref) themeFromPref } else { @@ -92,7 +96,6 @@ object ThemeUtils { when (aTheme) { THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) - THEME_STATUS_VALUE -> context.setTheme(R.style.AppTheme_Status) else -> context.setTheme(R.style.AppTheme_Light) } @@ -109,7 +112,6 @@ object ThemeUtils { when (getApplicationTheme(activity)) { THEME_DARK_VALUE -> activity.setTheme(otherThemes.dark) THEME_BLACK_VALUE -> activity.setTheme(otherThemes.black) - THEME_STATUS_VALUE -> activity.setTheme(otherThemes.status) } mColorByAttr.clear() diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index a4d759250d..3906ea687c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -21,6 +21,9 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.resources.StringProvider +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes @@ -310,12 +313,13 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo val params = HashMap() params["status"] = status - room.sendStateEvent( - eventType = EventType.PLUMBING, - stateKey = null, - body = params, - callback = createWidgetAPICallback(widgetPostAPIMediator, eventData) - ) + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + room.sendStateEvent( + eventType = EventType.PLUMBING, + stateKey = null, + body = params + ) + } } /** @@ -333,12 +337,14 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo Timber.d(description) val content = eventData["content"] as JsonDict val stateKey = "_$userId" - room.sendStateEvent( - eventType = EventType.BOT_OPTIONS, - stateKey = stateKey, - body = content, - callback = createWidgetAPICallback(widgetPostAPIMediator, eventData) - ) + + launchWidgetAPIAction(widgetPostAPIMediator, eventData) { + room.sendStateEvent( + eventType = EventType.BOT_OPTIONS, + stateKey = stateKey, + body = content + ) + } } /** @@ -456,4 +462,19 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo private fun createWidgetAPICallback(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict): WidgetAPICallback { return WidgetAPICallback(widgetPostAPIMediator, eventData, stringProvider) } + + private fun launchWidgetAPIAction(widgetPostAPIMediator: WidgetPostAPIMediator, eventData: JsonDict, block: suspend () -> Unit): Job { + return GlobalScope.launch { + kotlin.runCatching { + block() + }.fold( + onSuccess = { + widgetPostAPIMediator.sendSuccess(eventData) + }, + onFailure = { + widgetPostAPIMediator.sendError(stringProvider.getString(R.string.widget_integration_failed_to_send_request), eventData) + } + ) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt index cb40e5672b..eb588ec9ae 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.WidgetType -import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.net.URL @@ -106,14 +105,11 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in if (state.permissionData()?.isWebviewWidget.orFalse()) { WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false) } else { - awaitCallback { - session.integrationManagerService().setNativeWidgetDomainAllowed( - state.permissionData.invoke()?.widget?.type?.preferred ?: "", - state.permissionData.invoke()?.widgetDomain ?: "", - false, - it - ) - } + session.integrationManagerService().setNativeWidgetDomainAllowed( + state.permissionData.invoke()?.widget?.type?.preferred ?: "", + state.permissionData.invoke()?.widgetDomain ?: "", + false + ) } } catch (failure: Throwable) { Timber.v("Failure revoking widget: ${state.widgetId}") @@ -131,14 +127,11 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in if (state.permissionData()?.isWebviewWidget.orFalse()) { WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true) } else { - awaitCallback { - session.integrationManagerService().setNativeWidgetDomainAllowed( - state.permissionData.invoke()?.widget?.type?.preferred ?: "", - state.permissionData.invoke()?.widgetDomain ?: "", - true, - it - ) - } + session.integrationManagerService().setNativeWidgetDomainAllowed( + state.permissionData.invoke()?.widget?.type?.preferred ?: "", + state.permissionData.invoke()?.widgetDomain ?: "", + true + ) } } catch (failure: Throwable) { Timber.v("Failure allowing widget: ${state.widgetId}") diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt index 871e73592d..5664609a99 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/WidgetPermissionsHelper.kt @@ -19,7 +19,6 @@ package im.vector.app.features.widgets.permissions import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.widgets.WidgetService -import org.matrix.android.sdk.internal.util.awaitCallback class WidgetPermissionsHelper(private val integrationManagerService: IntegrationManagerService, private val widgetService: WidgetService) { @@ -30,8 +29,6 @@ class WidgetPermissionsHelper(private val integrationManagerService: Integration widgetId = QueryStringValue.Equals(widgetId, QueryStringValue.Case.SENSITIVE) ).firstOrNull() val eventId = widget?.event?.eventId ?: return - awaitCallback { - integrationManagerService.setWidgetAllowed(eventId, allow, it) - } + integrationManagerService.setWidgetAllowed(eventId, allow) } } diff --git a/vector/src/main/res/color/button_social_google_background_selector_dark.xml b/vector/src/main/res/color/button_social_google_background_selector_dark.xml new file mode 100644 index 0000000000..1369414e58 --- /dev/null +++ b/vector/src/main/res/color/button_social_google_background_selector_dark.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/color/button_social_google_background_selector_light.xml b/vector/src/main/res/color/button_social_google_background_selector_light.xml new file mode 100644 index 0000000000..bcac13885b --- /dev/null +++ b/vector/src/main/res/color/button_social_google_background_selector_light.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable-anydpi-v26/snow.png b/vector/src/main/res/drawable-anydpi-v26/snow.png new file mode 100644 index 0000000000..0142772ced Binary files /dev/null and b/vector/src/main/res/drawable-anydpi-v26/snow.png differ diff --git a/vector/src/main/res/drawable/bg_send.xml b/vector/src/main/res/drawable/bg_send.xml new file mode 100644 index 0000000000..8ab95bf5c5 --- /dev/null +++ b/vector/src/main/res/drawable/bg_send.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_attachment.xml b/vector/src/main/res/drawable/ic_attachment.xml index 944998c1cd..ea01e94372 100644 --- a/vector/src/main/res/drawable/ic_attachment.xml +++ b/vector/src/main/res/drawable/ic_attachment.xml @@ -1,7 +1,21 @@ - - + + + + diff --git a/vector/src/main/res/drawable/ic_close_24dp.xml b/vector/src/main/res/drawable/ic_close_24dp.xml new file mode 100644 index 0000000000..d69c331210 --- /dev/null +++ b/vector/src/main/res/drawable/ic_close_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_insert_emoji.xml b/vector/src/main/res/drawable/ic_insert_emoji.xml new file mode 100644 index 0000000000..c35000342b --- /dev/null +++ b/vector/src/main/res/drawable/ic_insert_emoji.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_keyboard.xml b/vector/src/main/res/drawable/ic_keyboard.xml new file mode 100644 index 0000000000..5e5d431abb --- /dev/null +++ b/vector/src/main/res/drawable/ic_keyboard.xml @@ -0,0 +1,4 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_apple.xml b/vector/src/main/res/drawable/ic_social_apple.xml new file mode 100644 index 0000000000..5d745a12a3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_apple.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_facebook.xml b/vector/src/main/res/drawable/ic_social_facebook.xml new file mode 100644 index 0000000000..77cb8be760 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_facebook.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_github.xml b/vector/src/main/res/drawable/ic_social_github.xml new file mode 100644 index 0000000000..602f16aa60 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_github.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_google.xml b/vector/src/main/res/drawable/ic_social_google.xml new file mode 100644 index 0000000000..1518c4cb29 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_google.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_social_twitter.xml b/vector/src/main/res/drawable/ic_social_twitter.xml new file mode 100644 index 0000000000..7139c3c9bc --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_twitter.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_trash_24.xml b/vector/src/main/res/drawable/ic_trash_24.xml index 266855d50c..27ad2e29d7 100644 --- a/vector/src/main/res/drawable/ic_trash_24.xml +++ b/vector/src/main/res/drawable/ic_trash_24.xml @@ -1,41 +1,47 @@ - - - - - + + + + + diff --git a/vector/src/main/res/layout/merge_composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml similarity index 91% rename from vector/src/main/res/layout/merge_composer_layout.xml rename to vector/src/main/res/layout/composer_layout.xml index 908b3f009b..cb0b37d844 100644 --- a/vector/src/main/res/layout/merge_composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:constraintSet="@layout/constraint_set_composer_layout_compact" + tools:constraintSet="@layout/composer_layout_constraint_set_compact" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - - + app:tint="?riotx_text_primary" + tools:ignore="MissingConstraints,MissingPrefix" /> - - - - - + + + + + + diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml similarity index 79% rename from vector/src/main/res/layout/constraint_set_composer_layout_compact.xml rename to vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index ac67db6a64..a4dfcf019c 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -10,7 +10,7 @@ app:layout_constraintStart_toStartOf="parent"> + tools:src="@drawable/ic_edit" /> - - - - - - - - + app:tint="@color/riotx_notice" + tools:ignore="MissingPrefix" + tools:visibility="visible" /> + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml similarity index 81% rename from vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml rename to vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml index dba996309e..8a76c0547e 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_expanded.xml @@ -10,7 +10,7 @@ app:layout_constraintStart_toStartOf="parent"> + app:layout_constraintEnd_toEndOf="@id/related_message_background" + app:layout_constraintStart_toStartOf="@+id/related_message_background" + app:layout_constraintTop_toTopOf="@id/related_message_background" /> + app:layout_constraintBottom_toBottomOf="@id/related_message_background" + app:layout_constraintEnd_toEndOf="@id/related_message_background" + app:layout_constraintStart_toStartOf="@+id/related_message_background" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_edit" /> - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index ca7d267c80..da41878365 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -19,9 +19,9 @@ android:id="@+id/loginServerIcon" android:layout_width="wrap_content" android:layout_height="wrap_content" - tools:src="@drawable/ic_logo_matrix_org" app:tint="?riotx_text_primary" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_logo_matrix_org" /> @@ -136,6 +136,35 @@ + + + + + + + + +
    diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index 37122e8e43..a441fee3be 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -53,14 +53,14 @@ - + + + tools:ignore="MissingPrefix,UnknownId" + tools:src="@drawable/ic_logo_matrix_org" + tools:visibility="visible" /> + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_progress.xml b/vector/src/main/res/layout/fragment_progress.xml new file mode 100644 index 0000000000..a7a2076209 --- /dev/null +++ b/vector/src/main/res/layout/fragment_progress.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 33f462c0d1..705e4cd882 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -225,4 +225,17 @@ app:maxImageSize="16dp" app:tint="@color/black" /> + + + + diff --git a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml index 2f4db6f116..906c7a21ab 100644 --- a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml +++ b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml @@ -54,6 +54,14 @@ + + + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_room_actions_notifications_all" /> - + app:layout_constraintStart_toEndOf="@id/actionIcon" + app:layout_constraintTop_toTopOf="parent"> + + + - + tools:ignore="MissingPrefix" + tools:visibility="visible" /> diff --git a/vector/src/main/res/layout/item_bottom_sheet_title.xml b/vector/src/main/res/layout/item_bottom_sheet_title.xml new file mode 100644 index 0000000000..5113c43f39 --- /dev/null +++ b/vector/src/main/res/layout/item_bottom_sheet_title.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/item_completion_homeserver.xml b/vector/src/main/res/layout/item_completion_homeserver.xml new file mode 100644 index 0000000000..ec1cb037eb --- /dev/null +++ b/vector/src/main/res/layout/item_completion_homeserver.xml @@ -0,0 +1,15 @@ + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_form_text_input.xml b/vector/src/main/res/layout/item_form_text_input.xml index 594bfc1788..f7ce8e1c9f 100644 --- a/vector/src/main/res/layout/item_form_text_input.xml +++ b/vector/src/main/res/layout/item_form_text_input.xml @@ -20,10 +20,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> + diff --git a/vector/src/main/res/layout/item_room_alias_text_input.xml b/vector/src/main/res/layout/item_room_alias_text_input.xml index 9216fc6b7e..fd7a99f0f0 100644 --- a/vector/src/main/res/layout/item_room_alias_text_input.xml +++ b/vector/src/main/res/layout/item_room_alias_text_input.xml @@ -30,11 +30,13 @@ app:layout_constraintStart_toEndOf="@+id/itemRoomAliasHash" app:layout_constraintTop_toTopOf="parent"> + diff --git a/vector/src/main/res/layout/item_settings_three_pid.xml b/vector/src/main/res/layout/item_settings_three_pid.xml index a175788d86..0040840ce9 100644 --- a/vector/src/main/res/layout/item_settings_three_pid.xml +++ b/vector/src/main/res/layout/item_settings_three_pid.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?riotx_background" android:minHeight="64dp" android:paddingStart="@dimen/layout_horizontal_margin" android:paddingEnd="@dimen/layout_horizontal_margin"> @@ -12,19 +13,20 @@ android:id="@+id/item_settings_three_pid_icon" android:layout_width="16dp" android:layout_height="16dp" + android:layout_marginEnd="8dp" android:scaleType="center" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/item_settings_three_pid_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/ic_phone" app:tint="?riotx_text_secondary" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_phone" /> diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml index 59396db0e5..7bdd0dd1e3 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml @@ -1,9 +1,26 @@ - + android:orientation="vertical"> + + + + + + diff --git a/vector/src/main/res/layout/url_preview.xml b/vector/src/main/res/layout/url_preview.xml new file mode 100644 index 0000000000..a8c287b471 --- /dev/null +++ b/vector/src/main/res/layout/url_preview.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index dd6a401164..ea8cff2da2 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -1673,7 +1673,7 @@ Odešle daný emote zabarvený jako duha Časová osa Editor zpráv - Zapnout šifrování end-to-end + Zapnout šifrování end-to-end… Jakmile zapnuto, šifrování nelze vypnout. Zapnout šifrování\? Jakmile zapnuto, šifrování místnosti nelze vypnout. Zprávy odeslané v zašifrované místnosti nemohou být čteny serverem, ale pouze účastníky místnosti. Zapnutím šifrování mohou boty a můstky přestat správně pracovat. @@ -2225,4 +2225,63 @@ Nemáte oprávnění k zahájení hovoru Nemáte oprávnění k zahájení konferenčního hovoru Resetovat + Odkaz na Matrix + QR kód nebyl oskenován! + Neplatný QR kód (neplatné URI)! + Sobě nelze zaslat přimou zprávu! + Sdílet textem + Hledat kontakty v Matrixu + Nastavit avatara + Souhlas uživatele nebyl udělen. + Sdělte tento kód lidem, aby s Vámi mohli po skenování zahájit konverzaci. + Můj kód + Sdílet můj kód + Skenovat QR kód + To není platný matrixový QR kód + 🔐️ Přidej se ke mně v elementu + Ahoj, mluv se mnou v Elementu: %s + Pozvat přátele + Přidat lidi + "Téma: " + Přidat téma + %s, abyste dali lidem vědět, o čem tato místnost je. + Toto je počátek Vaší historie přímých zpráv s %s. + Toto je počátek této konverzace. + Toto je počátek %s. + Exportovat audit + K zapnutí šifrování v této místnosti nemáte oprávnění. + Přímá zpráva + Zakládám místnost… + Některé znaky nejsou dovoleny + Prosím, zadejte adresu místnosti + Tato adresa je již obsazena + Adresa místnosti + Můžete zapnout, pokud bude tato místnost využita pro spolupráci interních týmů na Vašem homeserveru. Později nelze změnit. + Trvale blokovat vstup do této místnosti všem, kdo nejsou členy %s + Skrýt pokročilé + Ukázat pokročilé + %1$d z %2$d + Založit novou přímou konverzaci pomocí Matrix ID + Založit novou přímou konverzaci pomocí skenu QR kódu + Abyste nalezli existující kontakty, jež znáte, souhlasíte s odesláním svých kontaktních údajů (telefonní čísla a/nebo emailové adresy) na nastavený server pro identity (%1$s)\? +\n +\nZa účelem soukromí budou data před odesláním hašována. + Poslat emailové adresy a telefonní čísla + Udělit souhlas + Zrušit můj souhlas + Neudělili jste souhlas pro odeslání emailových adres a telefonních čísel na tento server pro identity za účelem nalezení dalších uživatelů podle svých kontaktů. + Udělili jste souhlas pro odeslání emailových adres a telefonních čísel na tento server pro identity za účelem nalezení dalších uživatelů podle svých kontaktů. + Poslat emailové adresy a telefonní čísla + Doporučení + Kontakty + Známí uživatelé + Poslední + QR kód + Přidat pomocí QR kódu + Hledat podle jména nebo ID + Udělte právo přístupu ke kontaktům. + Pro sken QR kódu je nutné povolit přístup k fotoaparátu. + Poslat historii požadavků na sdílení klíčů + Žádné další výsledky + Zahájit chat \ No newline at end of file diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 75ee23cb12..bcb33267a5 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -2248,4 +2248,21 @@ Direktnachricht Geschichte der Anfragen von Schlüsselfreigaben senden Keine weiteren Ergebnisse + Starte die Diskussion + Neue direkte Konversation mit Matrix-ID erstellen + E-Mails und Telefonnummern senden + Autorisieren + Meine Zustimmung widerrufen + Du hast zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzenden entdeckt zu werden. + Du hast nicht zugestimmt E-Mails und Telefonnummern an diesen Identitätsserver zu senden, um von anderen Nutzenden entdeckt zu werden. + E-Mails und Telefonnummern senden + Vorschläge + Kontakte + Bekannte Nutzer/innen + Kürzlich + QR-Code + Hinzufügen via QR-Code + Nach Name oder ID suchen + Gib die Erlaubnis, um auf die Kamera zu zugreifen. + Um den QR-Code zu scannen, muss der Zugriff auf die Kamera erlaubt werden. \ No newline at end of file diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index a26c4c91d6..b5356f3c68 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -1577,8 +1577,8 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Conferencia en progreso! Iniciar Videoconferencia Iniciar Audioconferencia - No puedes hacer llamarte a tí mismo - No puedes hacer llamarte a tí mismo, espera a que los participantes acepten la invitación + No puedes hacer llamarte a ti mismo + No puedes hacer llamarte a ti mismo, espera a que los participantes acepten la invitación Fallo al añadir Widget Fallo al eliminar Widget Confirmar llamada @@ -2211,7 +2211,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua Sólo mostrar el número de mensajes no leídos en una notificación sencilla. Mostrar detalles, como nombres de salas y contenido de mensajes. Mostrar contenido de notificaciones - Element sólo puede ser desbloqueado via Código PIN. + Element sólo puede ser desbloqueado vía Código PIN. Activar biometría de este dispositivo en particular, como huellas dactilares o reconocimiento facial. Activar biometría Configurar protecciones diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index 152f5f03e2..6bc5b3cd8c 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -863,7 +863,7 @@ Saadab antud emote vikerkaarevärvides Ajajoon Sõnumite kirjutamine - Võta läbiv krüptimine kasutusele + Võta läbiv krüptimine kasutusele… Kui krüptimine on juba kasutusele võetud, siis ei saa seda enam eemaldada. Kas võtame krüptimise kasutusele\? Kui kord juba kasutusele võetud, siis krüptimist enam hiljem ära lõpetada ei saa. Krüptitud sõnumeid ei saa lugeda ei vaheapealses veebiliikluses ega serveris ja vaid jututoa liikmed saavad neid lugeda. Krüptimise kasutusele võtmine võib takistada nii robotite kui sõnumisildade tööd. @@ -2191,4 +2191,59 @@ Otsevestlus Lisa kaasa võtmevahetusega seotud päringute ajalugu Rohkem otsingutulemusi pole + Kas selleks, et leida tuttavaid, oled sa nõus saatma oma kontaktteavet (telefoninumbreid ja/või e-posti aadresse) siin rakenduses seadistatud isikutuvastusserverile (%1$s)\? +\n +\nParema turvalisuse nimel me ei saada teavet mitte loetava tekstina, vaid räsina. + 🔐️ Liitu minuga vestlusrakenduses Element + Hei, palun suhtle minuga vestlusrakenduses Element: %s + Kutsu sõpru + Lisa inimesi + "Teema: " + lisa jututoa teema + Selleks et kõik teaks, millega siin jututoas tegeletakse, palun %s. + See on otsesõnumite algus kasutajaga %s. + See on vestluse algus. + Siit maalt algab %s jututuba. + Sul puuduvad õigused siin jututoas läbiva krüptimise kasutuselevõtmiseks. + Loon jututuba… + Mõned tähemärgid ei ole siin lubatud + Palun kirjuta jututoa aadress + See aadress on juba kasutusel + Jututoa aadress + Sa võid sellise võimaluse kasutusele võtta, kui seda jututuba kasutatakse vaid organisatsioonisiseste tiimide ühistööks oma koduserveri piires. Seda ei saa hiljem muuta. + Keela kõikide niisuguste kasutajate liitumine selle jututoaga, kelle kasutajakonto ei asu %s koduserveris + Peida lisaseadistused + Näita lisaseadistusi + %1$d / %2$d + Alusta QR-koodi lugemise abil uut vestlust + Alusta Matrix\'i kasutajatunnuse alusel uut vestlust + Matrix\'i link + QR-kood on lugemata! + Vigane QR-kood (vigane URI)! + Sa ei ole Muhv ega saa iseendale sõnumeid saata! + Jaga tekstina + Otsi Matrix\'i võrgust tuttavaid + Seadista tunnuspilt + Kasutaja nõusolek on puudu. + Selleks, et teised kasutajad saaks sind lisada oma kontaktiks ja alustada vestlust, jaga seda QR-koodi nendega. + Minu QR-kood + Jaga minu koodi + Loe QR-koodi + See ei ole korralik Matrix\'i QR-kood + Saada e-posti aadresse ja telefoninumbreid + Nõustu + Tühista minu nõusolek + Selleks, et leida Matrixikasutajaid oma kontaktide hulgast, sa ei ole andnud nõusolekut saata e-posti aadresse ja telefoninumbreid sellele isikutuvastusserverile. + Selleks, et leida Matrixikasutajaid oma kontaktide hulgast, oled sa andnud nõusoleku saata e-posti aadresse ja telefoninumbreid sellele isikutuvastusserverile. + Saada e-posti aadresse ja telefoninumbreid + Soovitused + Kontaktid + Tuttavad kasutajad + Hiljutised + QR-kood + Lisa QR-koodi abil + Otsi nime või Matrix\'i tunnuse alusel + Anna õigused oma kontakte lugeda. + QR-koodi lugemiseks pead selleks kaamerale õigused andma. + Alusta vestlust \ No newline at end of file diff --git a/vector/src/main/res/values-fa/strings.xml b/vector/src/main/res/values-fa/strings.xml index ae3292bb2c..0663dd1dca 100644 --- a/vector/src/main/res/values-fa/strings.xml +++ b/vector/src/main/res/values-fa/strings.xml @@ -299,7 +299,7 @@ رایانامه‌ای برای بازیابی تنظیم کرده تا بتوانید در صورت نیاز، از طریقش به دست افرادی که می‌شناسید، قابل کشف باشید. ثبت شماره تلفن (بعدا در صورت دلخواه می توانید از آن برای شناسایی دوستان خود استفاده کنید). نشانی رایانامهٔ پیوسته به حسابتان را برای بازنشانی گذواژه‌تان وارد کنید: - Latn + Arab شناسهٔ کاربری، نام یا رایانامه فرستادن پاسخی رمزشده… فرستادن یک پاسخ (رمزنشده)… @@ -321,7 +321,7 @@ تذکّرهای سوم‌شخص حق رونوشت سیاست محرمانگی - عکس نمایه + عکس پروفایل نام نمایشی رایانامه افزودن نشانی رایانامه @@ -506,7 +506,7 @@ فرستادن پرونده (%1$s / %2$s) بارگری پرونده %1$s… پرونده %1$s بارگیری شد! - (ویراسته) + (ویرایش شده) پالایش گفت‌وگوها… ایجاد اتاقی جدید فرستادن یک پیام مستقیم جدید @@ -999,7 +999,7 @@ افزایش کارایی با فقط بار کردن اعضای اتاق در نمای نخست. کارساز خانگیتان هنوز از بار کردن تنبلانهٔ اعضای اتاق پشتیبانی نمی‌کند. بعداً تلاش کنید. هرگز پیام‌های رمزشده را از دست ندهید - شروع با استفاده از پشتیبان کلید + شروع به استفاده از پشتیبانِ کلید برون‌ریزی دستی کلیدها امن کردن پشتیبانتان با یک عبارت عبور. تنظیم عبارت عبور @@ -1024,7 +1024,7 @@ لطفاً یک کلید بازیابی وارد کنید من بودم هرگز پیام‌های رمزشده را از دست ندهید - شروع با استفاده از پشتیبان کلید + شروع به استفاده از پشتیبانِ کلید پشتیبان امن سپرامنیتی در برابر از دست دادن دسترسی به داده‌ها و پیام‌های رمزشده هرگز پیام‌های رمزشده را از دست ندهید @@ -1062,7 +1062,7 @@ مخاطب دوربین صدا - جُنگ + گالری برچسب رسانه هیچ رسانه‌ای در این اتاق نیست @@ -1525,7 +1525,7 @@ [%1$s] \nاین خطا از کنترل المنت خارج است و به چند دلیل ممکن است رخ داده باشد. امکان دارد در صورت تلاش مجدد مشکل رفع شود، همچنین می‌توانید بررسی کنید که سرویس Google Play در استفاده از اینترنت در تنظیمات گوشی محدودیتی نداشته باشد یا ساعت دستگاه شما درست باشد. همچنین ممکن است به علت استفاده از ROM سفارشی‌شده این خطا رخ داده باشد. [%1$s] -\nاین خطا از کنترل هیوا خارج است و طبق گفته گوگل ، این خطا نشان می دهد که دستگاه بیش از حد مجاز، برنامه های ثبت شده در FCM دارد. این خطا فقط در مواردی رخ می دهد که تعداد زیادی برنامه وجود دارد، بنابراین نباید بر کاربر عادی رخ دهد. +\nاین خطا از کنترل المنت خارج است و طبق گفته گوگل ، این خطا نشان می دهد که دستگاه بیش از حد مجاز، برنامه های ثبت شده در FCM دارد. این خطا فقط در مواردی رخ می دهد که تعداد زیادی برنامه وجود دارد، بنابراین نباید بر کاربر عادی رخ دهد.
    بازیابی توکن FCM با مشکل مواجه شد: \n%1$s توکن FCM با موفقیت بازیابی شد: @@ -1588,21 +1588,21 @@ این دعوت به %s ارسال شده که ارتباطی با این حساب ندارد. \nممکن است بخواهید با حسابی دیگر وارد شده یا این رایانامه را به حسابتان بیفزایید. متاسفانه به دلیل عدم دسترسی، درخواست شما امکان پذیر نمی باشد - هیوا می‌تواند با دیدن دفترچه تلفن شما کاربرهای دیگر هیوا را بر اساس ایمبل و شماره تلفنشان پیدا کند. + المنت می‌تواند با دیدن دفترچه تلفن شما کاربرهای دیگر ماتریکس را بر اساس ایمبل و شماره تلفنشان پیدا کند. \n \nآیا موافق با اشتراک‌گذاری دفترچه تلفنتان به این منظور هستید؟ - هیوا می‌تواند با دیدن دفترچه تلفن شما کاربرهای دیگر هیوا را بر اساس ایمبل و شماره تلفنشان پیدا کند. اگر مایل به اشتراک گذاری دفترچه تلفنتان به این منظور هستید لطفا در پنجره‌ی بعد اجازه‌ی این کار را بدهید. - هیوا برای برقراری تماس تصویری نیازمند دسترسی به میکروفون و دوربین است. + المنت می‌تواند با دیدن دفترچه تلفن شما کاربرهای دیگر ماتریکس را بر اساس ایمبل و شماره تلفنشان پیدا کند. اگر مایل به اشتراک گذاری دفترچه تلفنتان به این منظور هستید لطفا در پنجره‌ی بعد اجازه‌ی این کار را بدهید. + المنت برای برقراری تماس تصویری نیازمند دسترسی به میکروفون و دوربین است. \n \nلطفا در پنجره های بعدی دسترسی های لازم را بدهید. " \n \nلطفا برای برقراری تماس در پنجره بعدی دسترسی لازم را بدهید." - هیوا برای برقراری تماس صوتی نیازمند دسترسی به میکروفون است. + المنت برای برقراری تماس صوتی نیازمند دسترسی به میکروفون است. " \n \nلطفا برای برقراری تماس در پنجره بعدی دسترسی لازم را بدهید." - هیوا برای ارسال و ذخیره‌ی فایل‌ها نیاز به دسترسی به گالری شما را دارد. + المنت برای ارسال و ذخیره‌ی فایل‌ها نیاز به دسترسی به گالری شما را دارد. \n \nلطفا در پنجره‌ای که باز می‌شود این دسترسی را بدهید. هنگامی که سرور شما قابلیت تماس ندارد، از سرور %s استفاده کن @@ -1699,7 +1699,7 @@ برای امنیت بیشتر، %s را با بررسی یک کد یکبارمصرف تایید کنید. پس از فعال‌شدن ، نمی‌توان رمزگذاری برای یک اتاق را غیرفعال کرد. پیام های ارسالی در یک اتاق رمزگذاری شده توسط سرور قابل مشاهده نیست و فقط توسط اعضای اتاق قابل مشاهده است. فعال کردن رمزنگاری ممکن است از کارکرد بسیاری از ربات ها و پل ها جلوگیری کند. رمزگذاری فعال شود؟ - فعال‌سازی رمزنگاری سرتاسر + به کار انداختن رمزنگاری سرتاسری… شکلک را با رنگ‌بندی رنگین گمان ارسال می کند پیام را با رنگ‌بندی رنگین کمان ارسال می کند این نشست نمی‌تواند تائید را با نشست‌های دیگر شما به اشتراک بگذارد. @@ -2044,7 +2044,7 @@ برای ادامه استفاده از سرور %1$s باید شرایط و ضوابط را خوانده و موافقت کنید. Markdown غیرفعال شده است. Markdown فعال شده است. - جهت رفع مشکلات مدیریت برنامه‌های هیوا + جهت رفع مشکلات مدیریت برنامه‌های ماتریکس فعال و غیرفعال کردن markdown نام مستعار شما را تغییر می‌دهد اخراج کاربر با شناسه داده شده @@ -2159,4 +2159,91 @@ پیام مستقیم ارسال تاریخچهٔ درخواست‌های هم‌رسانی کلید نتایج بیش‌تری نیست + کلید امنیتیتان را در جایی امن، مانند مدیر گذرواژه یا گاوصندوق نگه دارید. + عبارت امنیتی‌ای را که تنها خودتان می‌دانید، برای امن کردن رمزها روی کارسازتان وارد کنید. + کلید امنیتیتان را در جایی امن، مانند مدیر گذرواژه یا گاوصندوق نگه دارید. + عبارت رمزی که فقط خودتان می‌دانید را وارد کرده و کلیدی برای پشتیبان تولید کنید. + به هنوتم جایگزین، می‌توانید نشامی هر کارساز هویت دیگری را وارد کنید + کارساز خانگیتان (%1$s) پیشنهاد استفاده از %2$s برای کارساز هویتتان را می‌دهد + برای محرمانگیتان، المنت تنها از فرستادن شماره تلفن و رایانامه‌های کاربری در هم ریخته پشتیبانی می‌کند. + این عملیات ممکن نیست. کارساز خانگی منقضی شده است. + نتوانستیم کاربران را دعوت کنیم. لطفاً کاربرانی که می‌خواهید دعوت کنید را بررسی کرده و دوباره تلاش کنید. + نتوانستیم پیامتان را ایجاد کنیم. لطفاً کاربرانی که می‌خواهید دعوت کنید را بررسی کرده و دوباره تلاش کنید. + انسداد هرکسی خارج از %s از پیوستن به این اتاق + پیوند ماتریکس + دور ریختن تغییرات + تغییرات ذخیره‌نشده‌ای وجود دارد. دور ریختن تغییرات؟ + اتاق هنوز ایجاد نشده است. لغو ایجاد اتاق؟ + رمز QR پویش نشد! + رمز QR نامعتبر (نشانی نامعتبر)! + نمی‌توانید به خودتان پیام دهید! + هم‌رسانی با متن + جست‌وجوی مخاطبان در ماتریکس + تنظیم آواتار + ذخیرهٔ کلید امنیتیتان + برای تأیید عبارت امنیتیتان، دوباره واردش کنید. + عبارت امنیتی + تنظیم یک عبارت امنیتی + ذخیرهٔ کلید امنیتیتان + استفاده از یک عبارت امنیتی + نشانی کارساز هویتی را وارد کنید + رضایت کاربر فراهم نشده است. + لطفاً نخست شرایط کارساز هویت را در تنظیمات بپذیرید. + لطفاً نخست کارساز هویتی را پیکربندی کنید. + کارساز هویت منقضی شده اسن. المنت تنها نگارش ۲ از API را پشتیبانی می‌کند. + این رمز را افراد هم‌رسانی کرده تا بتوانند برای افزودنتان و شروع گپ، بپویندش. + رمزم + هم‌رسانی رمزم + پویش یک رمز QR + این یک رمز QR ماتریکس معتبر نیست + 🔐️ پیوستن به من روی المنت + سلام. روی المنت باهام حرف بزن: %s + دعوت دوستان + افزودن افراد + "موضوع: " + افزودن موضوع + %s برای دادن اجازهٔ دانستن موضوع این اتاق به افراد. + این، آغاز تاریخچهٔ پیام مستقیمتان با %s است. + این، آغاز %s است. + این، آغاز گفت‌وگوست. + اجازهٔ به کار انداختن رمزنگاری در این اتاق را ندارید. + ایجاد کردن اتاق… + برخی نویسه‌ها مجاز نیستند + لطفاً نام اتاقی را وارد کنید + این نشانی در حال استفاده است + نشانی اتاق + نهفتن پیشرفته + نمایش پیشرفته + %1$d از %2$d + ایجاد گفت‌وگوی مسنقیمی جدید با پویش یک رمز QR + ایجاد گفت‌وگوی مستقیمی جدید با شناسهٔ ماتریکس + فرستادن رایانامه‌ها و شماره تلفن‌ها + رضایت دادن + ابطال رضایتم + فرستادن رایانامه‌ها و شماره تلفن‌ها + پیشنهادها + مخاطبان + کاربران شناخته + اخیر + رمز QR + افزودن با رمز QR + جست‌وجو با نام یا شناسه + دسترسی به مخاطبانتان را مجاز کنید. + برای پویش یک رمز QR نیاز است دسترسی به دوربین را مجاز کنید. + آغاز به گپ + خروجی گرفتن + اگر اتاق فقط برای تعامل با افراد داخل سرور خانه شما می‌باشد، این قابلیت را فعال کنید. این تنظیم را بعدا نمی‌توانید تغییر دهید. + آیا می‌خواهید جهت کشف مخاطبینی که می شناسید، داده های مخاطب خود را (شماره تلفن و ایمیل) به سرور هویت‌سنجی(%1$s) ارسال کنید؟ +\n +\nبرای حفظ حریم خصوصی بیشتر، داده‌ها قبل از ارسال هش می‌شوند. + یک کلید امنیتی ایجاد کنید تا در مکانی امن مانند سامانه مدیریت رمز عبور یا گاوصندوق آن را ذخیره کنید. + در حال حاضر هیچ ارتباطی با این شناسه وجود ندارد. + هویت خود را تأیید کنید تا به پیام‌های رمز شده دسترسی پیدا کنید. + هویت خود را با تأیید این ورود به سیستم از یکی دیگر از نشست‌های خود، تأیید کنید تا به پیام‌های رمز شده دسترسی پیدا کنید. + + دستگاهی را که می‌توانید با استفاده از آن‌ها خود را تایید کنید نشان بده + %d دستگاهی را که می‌توانید با استفاده از آن‌ها خود را تایید کنید نشان بده + + شما عدم رضایت خود را برای ارسال ایمیل و شماره تلفن به این سرور اعلام کرده‌اید در نتیجه نمی‌توانید کاربران دیگر را با استفاده از مخاطبین خود پیدا کنید. + شما رضایت خود را برای ارسال ایمیل و شماره تلفن به این سرور اعلام کرده‌اید تا بتوانید کاربران دیگر را مخاطبین خود پیدا کنید. \ No newline at end of file diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 5a1426d644..adbbb22f42 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -13,7 +13,7 @@ OK Peruuta - Säästä + Tallenna Poistu Lähetä Lähetä uudelleen @@ -1231,7 +1231,7 @@ Yksityisviestit Uusi huone LUO - Huoneen nimi + Nimi Julkinen Kuka tahansa voi liittyä tähän huoneeseen Luottamustietoa haettaessa tapahtui virhe @@ -1711,7 +1711,7 @@ Viimeaikaiset huoneet Muut huoneet Lähettää annetun viestin väritettynä sateenkaaren väreillä - Ota käyttöön osapuolten välinen salaus + Ota käyttöön osapuolten välinen salaus… Salausta ei voi enää poistaa käytöstä sen jälkeen kun se on otettu käyttöön. Otetaanko salaus käyttöön\? Salausta ei voi ottaa pois käytöstä sen jälkeen kun se on otettu käyttöön. Salattuja viestejä ei pysty lukemaan edes palvelin, vain ainoastaan huoneessa olijat. Salauksen käyttöönotto voi estää bottien ja siltojen toiminnan huoneessa. @@ -1833,4 +1833,46 @@ Jätä huomiotta Tauko Toista + Käyttäjän huomiotta jättäminen piilottaa kyseisen käyttäjän viestit sinulta. +\n +\nVoit perua tämän milloin tahansa yleisissä asetuksissa. + Jätä käyttäjä huomiotta + Alenna + Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin. + Alenna itsesi\? + Peruuta kutsu + Anna lupa hakea yhteystiedot. + QR-koodin skannaaminen vaatii luvan kameran käyttöön. + Palaa puheluun + Aktiivinen puhelu (%s) + Kysy varmistusta ennen puhelun aloittamista + Estä vahinkopuhelut + SSL Virhe. + Takakamera + Etukamera + Vaihda kameraa + Langattomat kuulokkeet + Kuulokkeet + Kaiutin + Puhelin + Valitse äänilaite + Element-puhelu epäonnistui + Lähetä avaimen jakopyyntöjen historia + Ei enempää tuloksia + Ilmoitukset + Onnistui + Kopioi + Lopeta puhelu + Hylkää + Sovelman poistaminen epäonnistui + Sovelman lisääminen epäonnistui + Et voi aloittaa puhelua itsesi kanssa. Odota, että muut osallistujat hyväksyvät kutsun + Et voi aloittaa puhelua itsesi kanssa + Peruuta kutsu + Korkea laatu päälle + Korkea laatu pois + Reaaliaikaisen yhteyden muodostus epäonnistui. +\nPyydäthän kotipalvelimesi ylläpitäjää asentamaan TURN-palvelimen varmistaaksesi puhelujen luotettavan toiminnan. + Vastaa + Aloita keskustelu \ No newline at end of file diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index d6258d45bd..7eb188b070 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -102,8 +102,8 @@ Mot de passe Nouveau mot de passe Nom d’utilisateur - Adresse e-mail - Adresse e-mail (facultatif) + Adresse électronique + Adresse électronique (facultatif) Numéro de téléphone Numéro de téléphone (facultatif) Répéter le mot de passe @@ -704,9 +704,9 @@ Donner la permission Choisir une autre option Avatar d’avertissement - Envoyer un sticker - Envoyer un sticker - Vous n’avez aucun jeu d\'étiquettes activé pour le moment. + Envoyer un autocollant + Envoyer un autocollant + Vous n’avez aucun jeu d\'autocollants activé pour le moment. \n \nVoulez-vous en ajouter \? Désactiver le compte @@ -1269,7 +1269,7 @@ Conditions de service Examiner les conditions Être découvrable par les autres - Utiliser des robots, passerelles, widgets et packs de stickers + Utiliser des robots, passerelles, widgets et jeux d\'autocollants Lu à Aucun Révoquer @@ -1388,7 +1388,7 @@ Vous ne pouvez pas faire cela depuis Element mobile Une authentification est nécessaire Intégrations - Utilisez un gestionnaire d’intégrations pour gérer les bots, les passerelles, les widgets et les packs de stickers. + Utilisez un gestionnaire d’intégrations pour gérer les bots, les passerelles, les widgets et les jeux d\'autocollants. \nLes gestionnaires d’intégrations reçoivent des données de configuration et peuvent modifier des widgets, envoyer des invitations de salon et définir des rangs à votre place. Autoriser les intégrations Widget @@ -1654,7 +1654,7 @@ Envoie la réaction fournie colorée comme un arc-en-ciel Discussions Éditeur de messages - Activer le chiffrement de bout en bout + Active le chiffrement de bout en bout… Une fois qu’il est activé, le chiffrement ne peut pas être désactivé. Activer le chiffrement \? Une fois qu’il est activé, le chiffrement ne peut pas être désactivé. Les messages envoyés dans les salons chiffrés ne peuvent pas être vus par le serveur, uniquement par les participants du salon. Activer le chiffrement empêchera peut-être les robots et les passerelles de fonctionner correctement. @@ -2159,7 +2159,7 @@ Activer la HD Désactiver la HD Arrière - Frontale + Frontal Vous n’avez pas la permission de démarrer un appel Vous n’avez pas la permission de démarrer une téléconférence Réinitialiser @@ -2199,4 +2199,59 @@ Inclure l\'historique d\'échange de clés Plus aucun résultat Exporter le rapport d\'audit + %s pour permettre aux gens de connaître le sujet de ce salon. + Ceci est le début de l\'historique de vos messages directs avec %s. + Ceci est le début de cette conversation. + Ceci est le début de %s. + Vous n\'avez pas le droit d\'activer le chiffrement dans ce salon. + Création du salon… + Certains caractères sont interdits + Veuillez fournir une adresse de salon + Cette adresse est déjà utilisée + Adresse du salon + Créer une nouvelle conversation directe en scannant un QR Code + %1$d de %2$d + Créer une nouvelle conversation directe avec un identifiant Matrix + Dans le but de découvrir des contacts que vous connaîtriez, acceptez-vous d\'envoyer vos données de contact (numéros de téléphone et/ou e-mails) au serveur d\'identité configuré (%1$s) \? +\n +\nPour une meilleure protection de la vie privée, les données seront condensées (hash) avant l\'envoi. + Envoyer des e-mails et des numéros de téléphone + Autoriser + Révoquer mon autorisation + Vous n\'avez pas donné votre autorisation pour envoyer des e-mails et des numéros de téléphone à ce serveur d\'identité pour découvrir d\'autres utilisateurs à partir de vos contacts. + Vous avez donné votre autorisation pour envoyer des e-mails et des numéros de téléphone à ce serveur d\'identité pour découvrir d\'autres utilisateurs à partir de vos contacts. + Envoyer des e-mails et des numéros de téléphone + Suggestions + Contacts + Utilisateurs connus + Récent + QR code + Ajouter avec un QR Code + Chercher par nom ou identifiant + Autoriser l\'accès à vos contacts. + Pour scanner un QR code, vous devez autoriser l\'accès à votre appareil photo. + Débuter la discussion + Lien Matrix + Code QR non scanné ! + Code QR invalide (URI invalide) ! + Impossible avec vous-même ! + Partager par texto + Rechercher des contacts sur Matrix + Définir l\'avatar + Le consentement de l\'utilisateur n\'a pas été fourni. + Partagez ce code avec des gens pour qu\'ils puissent le scanner pour vous ajouter et commencer à discuter. + Mon code + Partager mon code + Scanner un code QR + Ce n\'est pas un code QR matrix valide + 🔐️ Rejoins-moi sur element + Salut, parle-moi sur Element : %s + Ajouter des amis + Ajouter des gens + "Sujet : " + Ajouter un sujet + Vous devriez l\'activer si le salon n\'est utilisé que pour collaborer avec des équipes internes sur votre serveur d\'accueil. Ceci ne peut pas être changé plus tard. + Bloquer les personnes qui ne sont pas membres de %s de rejoindre ce salon + Masquer les avancés + Afficher les avancés \ No newline at end of file diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index d7eb7a29d4..557aca192f 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1490,7 +1490,7 @@ Messaggi non letti È la tua conversazione. Tienitela. Chatta con persone direttamente o in gruppi - Tieni private le conversazioni con la cifratura + Tieni private le conversazioni con la crittografia Estendi e personalizza la tua esperienza Inizia Seleziona un server @@ -1712,7 +1712,7 @@ Invia l\'emoticon in questione colorata ad arcobaleno Cronologia Editor messaggi - Attiva cifratura end-to-end + Attiva crittografia end-to-end… Una volta attivata, la cifratura non può essere disattivata. Attivare la cifratura\? Una volta attivata, la cifratura di una stanza non può essere disattivata. I messaggi inviati in una stanza cifrata non possono essere visti dal server, solo dai partecipanti della stanza. L\'attivazione della cifratura può impedire il funzionamento di molti bot e bridge. @@ -2254,4 +2254,59 @@ Messaggio diretto Invia cronologia di richieste condivisione chiave Nessun altro risultato + Collegamento Matrix + Codice QR non scansionato! + Codice QR non valido (URI non valido)! + Non puoi messaggiare te stesso! + Condividi via testo + Cerca contatti su Matrix + Imposta avatar + Non è stato fornito il consenso dell\'utente. + Condividi questo codice con le persone consentendo la scansione per aggiungerti e iniziare a chattare. + Il mio codice + Condividi il mio codice + Scansiona un codice QR + Non è un codice QR Matrix valido + 🔐️ Unisciti a me su Element + Ehy, parliamo su Element: %s + Invita amici + Aggiungi persone + "Argomento: " + Aggiungi un argomento + %s per far sapere alle persone di cosa parla questa stanza. + Questo è l\'inizio della tua cronologia di messaggi diretti con %s. + Questo è l\'inizio di questa conversazione. + Questo è l\'inizio di %s. + Non hai il permesso di attivare la crittografia in questa stanza. + Creazione stanza… + Alcuni caratteri non sono permessi + Inserisci un indirizzo della stanza + Questo indirizzo è già in uso + Indirizzo stanza + Dovresti attivarlo se questa stanza verrà usata solo per collaborazioni tra squadre interne nel tuo homeserver. Non può essere cambiato in seguito. + Impedisci a chiunque non faccia parte di %s di entrare in questa stanza + Nascondi avanzate + Mostra avanzate + %1$d di %2$d + Crea una nuova conversazione diretta scansionando un codice QR + Crea una nuova conversazione diretta via ID Matrix + Per trovare i contatti esistenti che conosci, accetti di inviare i dati di contatto (numeri di telefono e/o email) al server d\'identità configurato (%1$s)\? +\n +\nPer privacy, sarà inviato solo l\'hash dei dati da inviare. + Invio di email e numeri di telefono + Accetta + Revoca il mio consenso + Non hai accettato l\'invio di email e numeri di telefono a questo server d\'identità per trovare altri utenti dai tuoi contatti. + Hai accettato l\'invio di email e numeri di telefono a questo server d\'identità per trovare altri utenti dai tuoi contatti. + Invia email e numeri di telefono + Suggerimenti + Contatti + Utenti conosciuti + Recenti + Codice QR + Aggiungi da codice QR + Cerca per nome o ID + Per scansionare un codice QR, devi permettere l\'accesso alla fotocamera. + Permetti l\'autorizzazione per l\'accesso ai contatti. + Inizia a chattare \ No newline at end of file diff --git a/vector/src/main/res/values-ja/strings.xml b/vector/src/main/res/values-ja/strings.xml index e764f5001b..e4c0ea4531 100644 --- a/vector/src/main/res/values-ja/strings.xml +++ b/vector/src/main/res/values-ja/strings.xml @@ -1098,4 +1098,23 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ グループ通話を開始する権限がありません リセット なし + トピック + 部屋名 + この部屋内のメッセージはエンドツーエンド暗号化されていません。 + ここでのメッセージはエンドツーエンド暗号化されていません。 + 設定 + あなたにはこの部屋の暗号化を有効にする権限がありません。 + 未読メッセージ + タイムラインでスワイプして返信を有効にする + タイムラインで非表示のイベントを表示する + QR コードをスキャン + QR コード画像 + QR コード + QR コードによる追加 + コードを共有 + Element で会話しましょう: %s + フレンドを招待 + 名前または ID で検索 + 既知のユーザー + 無効な QR コード (無効な URI)! \ No newline at end of file diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index f10458970e..b735c56452 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1,17 +1,15 @@ - + nl NL - Berichten Gesprek Instellingen Info over deelnemer Historisch - Oké Annuleren @@ -44,7 +42,6 @@ Toch sturen of Uitnodigen - Afmelden Spraakoproep @@ -57,27 +54,22 @@ Sluiten Gekopieerd naar klembord Uitschakelen - Bevestiging Waarschuwing - Thuis Favorieten Personen Gesprekken - Gespreksnamen filteren Favorieten filteren Personen filteren Gespreksnamen filteren - Uitnodigingen Lage prioriteit - Gesprekken Lokale contactenlijst @@ -85,7 +77,6 @@ Geen gesprekken U heeft Element geen toegang tot uw lokale contacten gegeven Geen resultaten - Gesprekken Gesprekscatalogus @@ -95,7 +86,6 @@ 1 gebruiker %d gebruikers - Logboek versturen Crash-logboek versturen Schermafdruk versturen @@ -108,10 +98,8 @@ Verzenden van foutmelding is mislukt (%s) Voortgang (%s%%) De toepassing is de vorige keer gecrasht. Wilt u dit melden\? - Versturen naar Gelezen - Gesprek toetreden Gebruikersnaam Account aanmaken @@ -120,14 +108,11 @@ Thuisserver-URL Identiteitsserver-URL Zoeken - Nieuw gesprek beginnen Spraakoproep beginnen Video-oproep beginnen - Bestanden versturen Foto of video maken - Aanmelden Account aanmaken @@ -176,7 +161,6 @@ Uw wachtwoord is opnieuw ingesteld. \n \nU bent op alle sessies afgemeld en zult niet langer pushmeldingen ontvangen. Om meldingen opnieuw in te schakelen, meldt u zich op elk apparaat opnieuw aan. - URL moet met http[s]:// beginnen Aanmelden mislukt: netwerkfout @@ -185,7 +169,6 @@ Registreren mislukt Registreren mislukt: e-mail-eigendomsfout Voer een geldige URL in - Ongeldige gebruikersnaam/wachtwoord Het opgegeven toegangsbewijs werd niet herkend Ongeldige JSON @@ -193,35 +176,27 @@ Er zijn te veel verzoeken verstuurd Deze gebruikersnaam is al in gebruik Er is nog niet op de koppeling in de e-mail geklikt - - Leesbevestigingslijst - - Versturen als Origineel Groot Medium Klein - "Download annuleren? Upload annuleren? %d s %1$dm %2$ds - Gisteren Vandaag - Gespreksnaam Gespreksonderwerp - Oproep verbonden Oproep is aan het verbinden… @@ -231,16 +206,13 @@ Inkomende video-oproep Inkomende spraakoproep Oproep gaande… - De andere kant heeft niet opgenomen. Mediaverbinding is mislukt Kan de camera niet initialiseren oproep elders opgenomen - Een afbeelding of video maken" Kan geen video opnemen" - Informatie Element heeft toegang nodig tot uw mediabestanden om bijlagen te verzenden en op te slaan. @@ -262,25 +234,20 @@ Element kan uw adresboek gebruiken om andere Matrix-gebruikers te vinden aan de hand van hun e-mailadressen en telefoonnummers. \n \nWilt u uw adresboek hiervoor delen\? - Sorry. De actie is niet toegepast vanwege ontbrekende rechten - Opgeslagen In downloads opslaan? JA NEE Verdergaan - Verwijderen Toetreden Voorvertoning Afwijzen - Ga naar het eerste ongelezen bericht. - %s heeft u uitgenodigd in dit gesprek Deze uitnodiging is naar %s verstuurd, maar die is niet geassocieerd met deze account. @@ -288,27 +255,22 @@ U probeert toegang te verkrijgen tot %s. Zou u het gesprek willen toetreden om eraan deel te nemen\? een gesprek Dit is een voorvertoning van dit gesprek. Gespreksinteracties zijn uitgeschakeld. - Nieuw gesprek Deelnemer toevoegen 1 deelnemer - Gesprek verlaten Weet u zeker dat u het gesprek wilt verlaten\? Weet u zeker dat u %s uit dit gesprek wilt verwijderen\? Aanmaken - Online Offline Afwezig - BEHEERDERSGEREEDSCHAPPEN BELLEN TWEEGESPREKKEN SESSIES - Uitnodigen Dit gesprek verlaten Verwijderen uit dit gesprek @@ -324,18 +286,14 @@ Sessielijst weergeven U kunt deze veranderingen niet ongedaan maken aangezien u de gebruiker tot hetzelfde niveau als uzelf promoveert. \nWeet u het zeker\? - Weet u zeker dat u %s in dit gesprek wilt uitnodigen\? - Uitnodigen met ID LOKALE CONTACTEN (%d) Enkel Matrix-gebruikers - Gebruiker uitnodigen met ID Voer één of meer e-mailadressen of Matrix-ID’s in E-mailadres of Matrix-ID - Zoeken %s is aan het typen… @@ -352,7 +310,6 @@ Onverstuurde berichten verwijderen Bestand niet gevonden U heeft geen toestemming om dit naar dit gesprek te sturen - Vertrouwen Niet vertrouwen @@ -365,7 +322,6 @@ Het certificaat is veranderd van één dat door uw telefoon werd vertrouwd naar een ander. Dit is HEEL ONGEBRUIKELIJK. Het wordt aangeraden om dit nieuwe certificaat NIET TE AANVAARDEN. Het certificaat is veranderd van een vertrouwd naar een onvertrouwd certificaat. De server heeft misschien zijn certificaat vernieuwd. Contacteer de serverbeheerder voor de verwachte vingerafdruk. Aanvaard het certificaat alleen als de serverbeheerder een vingerafdruk heeft gepubliceerd die overeenkomt met degene hierboven. - Info over gesprek Personen @@ -374,7 +330,6 @@ Ongeldige ID. Het zou een e-mailadres of een Matrix-ID zoals ‘@gebruikersnaam:domein’ moeten zijn UITGENODIGD TOEGETREDEN - Reden voor het melden van deze inhoud Wilt u alle berichten van deze gebruiker verbergen\? @@ -382,7 +337,6 @@ \nLet op: deze actie zal de app opnieuw opstarten; dit kan even duren. Upload annuleren Download annuleren - Zoeken Gespreksleden filteren @@ -391,7 +345,6 @@ BERICHTEN PERSONEN BESTANDEN - TOETREDEN CATALOGUS @@ -404,18 +357,15 @@ Gesprek toetreden Treed een gesprek toe Voer een gespreks(bij)naam in - Catalogus doorbladeren Catalogus wordt doorzocht… - Favoriet Lage prioriteit Tweegesprek Gesprek verlaten Vergeten - Berichten Instellingen @@ -424,9 +374,7 @@ Derdepartijvermeldingen Copyright Privacybeleid - - Profielfoto Naam E-mailadres @@ -435,22 +383,18 @@ Telefoonnummer toevoegen Toon informatie over de app in de systeeminstellingen. App-informatie - Meldingen voor deze account inschakelen Meldingen voor deze sessie inschakelen Het scherm voor 3 seconden aanzetten - Berichten in één-op-één-gesprekken Berichten in groepsgesprekken Wanneer ik in een gesprek word uitgenodigd Oproepuitnodigingen Door een robot verstuurde berichten - Synchronisatie in de achtergrond Achtergrondssynchronisatie inschakelen Synchronisatieverzoek is verlopen Pauze tussen elk synchronisatie - Versie olm-versie Algemene voorwaarden @@ -459,8 +403,6 @@ Privacybeleid Cache wissen - - Gebruikersinstellingen Meldingen Genegeerde gebruikers @@ -479,7 +421,6 @@ ID Publieke naam Publieke naam bijwerken - Laatst gezien %1$s @ %2$s Deze actie vereist bijkomende authenticatie. @@ -487,19 +428,15 @@ Authenticatie Wachtwoord: Indienen - Aangemeld als Thuisserver Identiteitsserver - Verificatie in afwachting Bekijk uw e-mail en tik op de koppeling erin. Tik zodra dit gedaan is op Verdergaan. Het verifiëren van het e-mailadres is mislukt. Bekijk uw e-mail en tik op de koppeling erin. Tik zodra dit gedaan is op Verdergaan. - Dit e-mailadres is al in gebruik. Dit e-mailadres is niet gevonden. Dit telefoonnummer is al in gebruik. - Wachtwoord veranderen Huidig wachtwoord Nieuw wachtwoord @@ -509,13 +446,9 @@ Alle berichten van %s tonen\? \n \nLet op: deze actie zal de app herstarten; dit kan even duren. - Weet u zeker dat u dit meldingsdoel wilt verwijderen\? - Weet u zeker dat u de %1$s %2$s wilt verwijderen\? - Kies een land - Land Kies een land Telefoonnummer @@ -525,21 +458,17 @@ Voer een activeringscode in Er is een fout opgetreden bij het valideren van uw telefoonnummer Code - - Gespreksafbeelding Gespreksnaam Onderwerp Gesprekslabel Gelabeld als: - Favoriet Lage prioriteit Geen - Toegankelijk- en zichtbaarheid Dit gesprek vermelden in de gesprekscatalogus @@ -547,22 +476,18 @@ Toegang tot de gespreksgeschiedenis Wie kan er de geschiedenis lezen\? Wie heeft er toegang tot dit gesprek\? - Iedereen Alleen deelnemers (vanaf het moment dat deze optie wordt geselecteerd) Alleen deelnemers (vanaf het moment dat ze worden uitgenodigd) Alleen deelnemers (vanaf het moment dat ze toetreden) - Om naar een gesprek te verwijzen moet dit een adres hebben. Alleen personen die uitgenodigd zijn Iedereen die de koppeling van het gesprek kent, met uitzondering van gasten Iedereen die de koppeling van het gesprek kent, inclusief gasten - Verbannen gebruikers - Geavanceerd Interne ID van dit gesprek @@ -574,35 +499,27 @@ Eind-tot-eind-versleuteling is actief Alleen naar geverifieerde sessies versleutelen Ongeverifieerde sessies in dit gesprek nooit berichten sturen vanaf deze sessie. - Dit gesprek heeft geen lokale adressen Nieuw adres (bv. #foo:matrix.org) - Ongeldig bijnaamformaat ‘%s’ is geen geldig bijnaamformaat U zult geen hoofdadres voor dit gesprek opgegeven hebben. Hoofdadreswaarschuwingen - Instellen als hoofdadres Niet instellen als hoofdadres Gespreks-ID kopiëren Gespreksadres kopiëren - Versleuteling is ingeschakeld in dit gesprek. Versleuteling is uitgeschakeld in dit gesprek. Versleuteling inschakelen \n(let op: dit kan niet meer uitgeschakeld worden!) - Catalogus - %s heeft geprobeerd een specifiek punt in de geschiedenis van dit gesprek te laden, maar kon het niet vinden. - Informatie over eind-tot-eind-versleuteling - Gebeurtenisinformatie Gebruikers-ID Curve25519-identiteitssleutel @@ -610,7 +527,6 @@ Algoritme Sessie-ID Ontsleutelingsfout - Informatie over sessie van afzender Publieke naam Publieke naam @@ -618,7 +534,6 @@ Sessiesleutel Verificatie Ed25519-vingerafdruk - E2E-gesprekssleutels exporteren Gesprekssleutels exporteren Exporteer de sleutels naar een lokaal bestand @@ -628,31 +543,25 @@ De E2E-gesprekssleutels zijn in ‘%s’ opgeslagen. \n \nLet op: dit bestand kan verwijderd worden als de app verwijderd is. - E2E-gesprekssleutels importeren Gesprekssleutels importeren Importeer de sleutels uit een lokaal bestand Importeren Enkel naar geverifieerde sessies versleutelen Versleutelde berichten nooit naar ongeverifieerde sessies sturen vanaf deze sessie. - NIET geverifieerd Geverifieerd Geblokkeerd - onbekende sessie geen - Verifiëren Ontverifiëren Blokkeringslijst Deblokkeringslijst - Sessie verifiëren Om te verifiëren dat deze sessie vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar Gebruikersinstellingen van deze sessie overeenkomt met de sleutel hieronder: Als het overeenkomt, drukt u op de knop ‘Verifiëren’ hieronder. Als het niet overeenkomt, dan onderschept iemand anders deze sessie en zou u het beter blokkeren. In de toekomst zal dit verificatieproces verbeterd worden. Ik verifieer dat de sleutels overeenkomen - Dit gesprek bevat onbekende sessies Dit gesprek bevat onbekende sessies die niet geverifieerd zijn. @@ -660,7 +569,6 @@ \nWe raden u aan om bij elke sessie door het verificatieprocces heen te gaan voordat u verdergaat, maar u kunt het bericht ook zonder te verifiëren opnieuw versturen. \n \nOnbekende sessies: - Kies een gesprekscatalogus Het kan zijn dat de server niet beschikbaar of overbelast is @@ -668,30 +576,23 @@ Thuisserver-URL Alle gesprekken op server %s Alle lokale gesprekken op %s - Zoeken in de historiek Offline - Gebruikerscatalogus GEBRUIKERSCATALOGUS (%s) Starten bij opstarten Mediacache wissen Media bewaren - Tijdsaanduidingen weergeven voor alle berichten Databesparingsmodus - Gebruikersinterface Taal Taal kiezen - 1 week 1 maand Altijd - Thema - Tekstgrootte Klein Normaal @@ -704,18 +605,14 @@ Licht thema Donker thema Zwart thema - Bezig met synchroniseren… Luisteren voor evenementen - Meldingsgeluid Tijdsaanduidingen in 12-uursformaat weergeven - U heeft toestemming nodig om widgets in dit gesprek te beheren Aanmaken van widget is mislukt Vergadergesprekken maken met jitsi Weet u zeker dat u deze widget uit dit gesprek wilt verwijderen\? - Kan widget niet aanmaken. Versturen van verzoek mislukt. @@ -728,67 +625,48 @@ Matrix-apps toevoegen Geluidsmeldingen Stille meldingen - Foutmelding - Foto maken Video maken - Bellen Berichten die mijn weergavenaam bevatten Berichten die mijn gebruikersnaam bevatten Statistische gegevens - Systeemcamera gebruiken - U heeft een nieuwe sessie ‘%s’ toegevoegd, die versleutelingssleutels aanvraagt. Uw ongeverifieerde sessie ‘%s’ vraagt versleutelingssleutels aan. Verificatie starten Delen zonder te verifiëren Verzoek negeren - Let op! Vergadergesprekken zijn in ontwikkeling en kunnen dus nog kuren vertonen. - Opdrachtfout Onbekende opdracht: %s - Uit Lawaaierig - Versleuteld bericht - Info over gemeenschap - Laden… - Afsluiten Acties Gemeenschappen - Gemeenschapsnamen filteren - Uitnodigen Gemeenschappen Geen groepen - Schudden om een probleem te melden - Weet u zeker dat u een nieuw gesprek met %s wilt beginnen\? Weet u zeker dat u een spraakoproep wilt beginnen\? Weet u zeker dat u een video-oproep wilt beginnen\? - Groepenlijst - 1 verandering in lidmaatschap %d veranderingen in lidmaatschap - Ledenlijst Opschrift openen Synchroniseren… @@ -801,12 +679,10 @@ %d deelnemers Weet u zeker dat u deze gebruiker uit dit gesprek wilt verbannen\? - 1 nieuw bericht %d nieuwe berichten - 1 gesprek %d gesprekken @@ -820,19 +696,14 @@ Alleen vermeldingen Dempen Snelkoppeling aan thuisscherm toevoegen - Inline URL-voorvertoning Trillen bij vermelden van een gebruiker - Badge - Meldingen Dit gesprek geeft geen badges voor gemeenschappen weer Nieuwe gemeenschaps-ID (bv. +foo:matrix.org) Ongeldige gemeenschaps-ID ‘%s’ is geen geldige gemeenschaps-ID - - 1 ongelezen bericht waarin u vermeld staat %d ongelezen berichten waarin u vermeld staat @@ -842,12 +713,10 @@ %d gesprekken %1$s in %2$s - 1 actieve widget %d actieve widgets - Aanmaken Gemeenschap aanmaken @@ -855,42 +724,34 @@ Voorbeeld Gemeenschaps-ID voorbeeld - Thuis Personen Gesprekken Geen gebruikers - Gesprekken Toegetreden Uitgenodigd Groepsleden filteren Groepsgesprekken filteren - De gemeenschapsbeheerder heeft geen lange beschrijving gegeven voor deze gemeenschap. - %2$s heeft u uit %1$s gezet %2$s heeft u uit %1$s verbannen Reden: %1$s Opnieuw toetreden Gesprek vergeten - Ontvangst-avatar Avatar - 1 ongelezen bericht waarin u vermeld bent %d ongelezen berichten waarin u vermeld bent Vermeldingsavatar Verstuur een sticker - Sticker versturen U heeft momenteel geen stickerpakketten ingeschakeld. \n \nWilt u er nu een paar toevoegen\? - Meldingsprivacy Normaal Gereduceerde privacy @@ -900,25 +761,20 @@ • De inhoud van de berichten in de melding is veilig overgebracht, rechtstreeks vanaf de Matrix-thuisserver • Meldingen bevatten meta- en berichtdata • Meldingen zullen berichtinhoud niet weergeven - Account deactiveren Mijn account deactiveren - Meldingsprivacy Element kan op de achtergrond werken om uw meldingen veilig en privé te beheren. Dit beïnvloedt mogelijk het accuverbruik. Toestemming verlenen Kies een andere optie - Statistische gegevens (analytics) versturen Element verzamelt anonieme statistische gegevens (analytics) om het voor ons mogelijk te maken om de app te verbeteren. Schakel statistische gegevens in om ons te helpen bij het verbeteren van Element. Ja, ik wil helpen! - Er ontbreekt een vereiste parameter. Er is een parameter ongeldig. Om de %1$s-thuisserver verder te blijven gebruiken, dient u de voorwaarden te lezen en ermee akkoord te gaan. Nu doorlezen - Account deactiveren Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal met dezelfde gebruikers-ID kunnen registreren. Dit zal er voor zorgen dat uw account alle gesprekken verlaat waar deze momenteel lid van is, en het verwijdert de accountgegevens van de identiteitsserver. Deze actie is onomkeerbaar. \n @@ -928,37 +784,25 @@ Vergeet alle berichten die ik heb verstuurd wanneer mijn account gedeactiveerd is (Let op: dit zal er voor zorgen dat toekomstige gebruikers een onvolledig beeld krijgen van gesprekken) Voer uw wachtwoord in om verder te gaan: Account deactiveren - Licenties van derde partijen - Downloaden Inspreken Beveiligingssleutels van uw sessies opnieuw aanvragen. - Sleutelaanvraag verstuurd. - Aanvraag verstuurd Start Element op een ander apparaat dat het bericht kan ontsleutelen, zodat het de sleutels naar deze sessie kan sturen. - Typ hier… - Wissen Spraakbericht versturen - doorgaan met… Sorry, er is geen externe toepassing gevonden om deze actie te voltooien. - Stemberichten versturen - Voer uw wachtwoord in. - Beschrijf het probleem in het Engels, indien mogelijk. Verstuur een versleuteld antwoord… Verstuur een antwoord (onversleuteld)… Media bekijken vóór het versturen - U bent momenteel geen lid van een gemeenschap. - Enter-knop van toetsenbord gebruiken om berichten te versturen Toont een actie Verbant gebruiker met gegeven ID @@ -976,10 +820,8 @@ Het gesprek wordt hier voortgezet Dit gesprek is een voortzetting van een ander gesprek Klik hier om oudere berichten te zien - Deze actie is niet mogelijk wegens ontbrekende rechten. Systeemmeldingen - 1s %ds @@ -996,60 +838,44 @@ 1d %dd - nu %1$s %2$s geleden %1$s - "%1$s, " %1$s en %2$s %1$s %2$s - 1 geselecteerd %d geselecteerd Om Matrix-appbeheer te herstellen - 1 deelnemer %d deelnemers - 1 gesprek %d gesprekken Bronlimiet overschreden Beheerder contacteren - contact op te nemen met uw dienstbeheerder - Deze thuisserver heeft een van zijn bronlimieten overschreden, dus sommige gebruikers zullen zich niet kunnen aanmelden. Deze thuisserver heeft een van zijn bronlimieten overschreden. - Deze thuisserver heeft zijn limiet voor maandelijks actieve gebruikers overschreden, dus sommige gebruikers zullen zich niet kunnen aanmelden. Deze thuisserver heeft zijn limiet voor maandelijks actieve gebruikers overschreden. - Gelieve %s om deze limiet te verhogen. Gelieve %s om deze dienst te blijven gebruiken. - Foutmelding - Status.im-thema - Toch bellen Aanvaarden - Gelieve het beleid van deze thuisserver te lezen en aanvaarden: - Oproepen Gebruik de standaardbeltoon van Element voor inkomende oproepen Beltoon voor inkomende oproepen Selecteer beltoon voor oproepen: - Eruit sturen Reden - Versie %s Voorvertoning van koppelingen in het gesprek tonen (als uw thuisserver deze functie ondersteunt). Typmeldingen versturen @@ -1065,7 +891,6 @@ Dienst wordt geïnitialiseerd Sleutelback-up Sleutelback-up gebruiken - Sleutelback-up is nog niet klaar, even geduld… Indien u zich nu afmeldt, zult u uw versleutelde berichten verliezen Sleutelback-up is bezig. Indien u zich nu afmeldt, zult u de toegang tot uw versleutelde berichten verliezen. @@ -1076,23 +901,19 @@ Weet u het zeker\? Back-up maken U zult de toegang tot uw versleutelde berichten verliezen, tenzij u eerst een back-up van uw sleutels maakt vooraleer u zich afmeldt. - Blijven Overslaan Klaar Afbreken Negeren - Weet u zeker dat u zich wilt afmelden\? Markeren als gelezen Aanmelden met unieke aanmelding Deze URL kan niet bereikt worden, gelieve deze na te kijken Uw apparaat gebruikt een verouderd TLS-beveiligingsprotocol, dat kwetsbaar is voor aanvallen. Uit veiligheidsoverwegingen zult u geen verbinding kunnen maken Video-oproep gaande… - Geavanceerde meldingsinstellingen Meldingsbelang op gebeurtenis - Problemen met meldingen oplossen Diagnostische probleemoplossingsinformatie Testen uitvoeren @@ -1100,37 +921,31 @@ Basisdiagnose is oké. Als u nog steeds geen meldingen ontvangt, gelieve dan een bugmelding in te dienen om ons te helpen onderzoeken. Er zijn één of meer tests mislukt, probeer de aanbevolen oplossing(en). Er zijn één of meer tests mislukt, gelieve een bugmelding in te dienen om ons te helpen onderzoeken. - Systeeminstellingen. Meldingen zijn ingeschakeld in de systeeminstellingen. Meldingen zijn uitgeschakeld in de systeeminstellingen. \nGelieve deze te controleren. Instellingen openen - Accountinstellingen. Meldingen zijn ingeschakeld voor uw account. Meldingen zijn uitgeschakeld voor uw account. \nGelieve de accountinstellingen te controleren. Inschakelen - Sessie-instellingen. Meldingen zijn ingeschakeld voor deze sessie. Meldingen zijn niet ingeschakeld voor deze sessie. \nGelieve de Element-instellingen te controleren. Inschakelen - Aangepaste instellingen. Sommige soorten berichten zijn stil (ze geven een geluidsloze melding). Sommige meldingen zijn uitgeschakeld in uw aangepaste instellingen. Laden van aangepaste regels is mislukt, probeer het opnieuw. Instellingen controleren - Play-diensten controleren De APK van Google Play Services is beschikbaar en up-to-date. Element maakt gebruikt van Google Play Services om pushberichten af te leveren, maar dit lijkt niet juist geconfigureerd te zijn: \n%1$s Play-diensten herstellen - Firebase-bewijs Het FCM-bewijs is opgehaald: \n%1$s @@ -1143,27 +958,22 @@ [%1$s] \nDeze fout is onafhankelijk van Element. Er is geen Google-account verbonden met de telefoon. Open het accountbeheer en voeg er een Google-account toe. Account toevoegen - Bewijsregistratie FCM-bewijs geregistreerd bij thuisserver. FCM-bewijs niet geregistreerd bij thuisserver: \n%1$s - Meldingsdienst Meldingsdienst is actief. Meldingsdienst is niet actief. \nProbeer de app te herstarten. Dienst starten - Meldingsdienst automatisch herstarten Dienst is afgesloten en automatisch herstart. Dienst is niet herstart - Starten bij opstarten van apparaat De dienst zal starten wanneer het apparaat wordt herstart. De dienst zal niet starten wanneer het apparaat wordt herstart en u zult geen meldingen ontvangen tot u Element hebt geopend. Starten bij opstarten inschakelen - Achtergrondbeperkingen controleren Achtergrondbeperkingen zijn uitgeschakeld voor Element. Deze test dient uitgevoerd te worden met een mobiele verbinding (geen wifi). \n%1$s @@ -1171,51 +981,39 @@ \nAl wat de app probeert te doen zal in de achtergrond hevig beperkt worden; dit kan het correct functioneren van meldingen beïnvloeden. \n%1$s Beperkingen uitschakelen - Accuoptimalisatie Element wordt niet beperkt door accuoptimalisatie. Als een gebruiker een apparaat los van de oplader een tijd laat stilliggen, met het scherm uitgeschakeld, gaat het apparaat in slaapmodus. Dit verhindert apps de toegang tot het netwerk, en stelt hun taken, synchronisaties en standaardalarmen uit. Optimalisatie negeren - De app heeft geen verbinding met de homeserver nodig in de achtergrond, dit zou het gebruik van de batterij moeten verlagen Lawaaiierige meldingen configureren Oproepmeldingen configureren Stille meldingen configureren Bepaal de LED-kleur, vibratie, geluid, … - - Beheer van cryptografische sleutels Berichten versturen met Enter De Enter-knop van het toetsenbord zal berichten versturen in plaats van een regeleinde in te voegen - Achtergrondverbinding Element heeft een achtergrondverbinding met lage impact nodig om betrouwbare meldingen te kunnen hebben. \nOp het volgende scherm zult u gevraagd worden om Element toestemming te verlenen om altijd in de achtergrond te kunnen draaien, gelieve deze toestemming te verlenen. Toestemming verlenen - Databesparingsmodus past een specifieke filter toe zodat aanwezigheidsupdates en typmeldingen weggefilterd worden. - Er is een fout opgetreden bij het verifiëren van uw e-mailadres. - Wachtwoord Wachtwoord bijwerken Het wachtwoord is ongeldig Wachtwoorden komen niet overeen - Er is een fout opgetreden bij het verifiëren van uw telefoonnummer. Bijkomende info: %s - Media Standaardcompressie Kiezen Standaardmediabron Kiezen Sluitergeluid afspelen - Maak een wachtwoord aan om de geëxporteerde sleutels mee te versleutelen. U heeft dit wachtwoord nodig om de sleutels te kunnen importeren. Herstel van versleutelde berichten Sleutelback-up beheren - %1$s: 1 bericht %1$s: %2$d berichten @@ -1224,51 +1022,40 @@ %d melding %d meldingen - Nieuwe gebeurtenis Gesprek Nieuwe berichten Nieuwe uitnodiging Ik ** Versturen mislukt - open het gesprek - Start de systeemcamera in plaats van het aangepaste camerascherm. Deze optie vereist een externe app om de berichten mee op te nemen. - De opdracht ‘%s’ heeft meer parameters nodig, of sommige parameters zijn onjuist. Markdown is ingeschakeld. Markdown is uitgeschakeld. - Stil Voer een gebruikersnaam in. Gespreksleden lui laden Verbeter de prestaties door gespreksleden enkel bij de eerste weergave te laden. Uw thuisserver ondersteunt het lui laden van gespreksleden nog niet. Probeer het later opnieuw. - Sorry, er is een fout opgetreden - uitvouwen invouwen - Infogebied weergeven Altijd Voor berichten en fouten Enkel voor fouten - %1$s: %1$s: %2$s +%d %d+ Er is geen geldige APK van Google Play Services gevonden. Meldingen zullen mogelijk niet correct functioneren. - Wachtwoord aanmaken Wachtwoorden komen niet overeen Voer een wachtwoord in Wachtwoord is te zwak - Verwijder het wachtwoord als u wilt dat Element een herstelsleutel genereert. Geen Matrix-sessie beschikbaar - Verlies nooit uw versleutelde berichten Berichten in versleutelde gesprekken worden beveiligd met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en u hebben de sleutels om deze berichten te lezen. \n @@ -1276,7 +1063,6 @@ Begin sleutelback-up te gebruiken (Geavanceerd) Sleutels handmatig exporteren - Beveilig uw back-up met een wachtwoord. We bewaren een versleutelde kopie van uw sleutels op onze thuisserver. Bescherm uw back-up met een wachtwoord om deze veilig te houden. \n @@ -1298,7 +1084,6 @@ De herstelsleutel is opgeslagen naar ‘%s’. \n \nLet op: dit bestand kan verwijderd worden als de app wordt verwijderd. - Gelieve er een kopie van te maken Herstelsleutel delen met… Herstelsleutel wordt gegenereerd met wachtwoord, dit proces kan enkele seconden duren. @@ -1306,25 +1091,18 @@ Onverwachte fout Back-up begonnen Uw versleutelingssleutels worden nu in de achtergrond naar uw thuisserver geback-upt. De initiële back-up kan enkele minuten duren. - - Weet u het zeker\? U kunt de toegang tot uw berichten verliezen indien u zich afmeldt of dit apparaat verliest. - Back-upversie wordt opgehaald… Gebruik uw herstelwachtwoord om uw versleutelde berichtgeschiedenis te ontgrendelen uw herstelsleutel gebruiken Als u uw herstelwachtwoord niet meer weet, kunt u %s. - Gebruik uw herstelsleutel om uw versleutelde berichtgeschiedenis te ontgrendelen Voer de herstelsleutel in - Berichtherstel - Herstelsleutel verloren\? U kunt er een nieuwe instellen in de instellingen. De back-up kan met dit wachtwoord niet ontsleuteld worden: controleer of u het juiste herstelwachtwoord heeft ingevoerd. Netwerkfout: controleer uw verbinding en probeer het opnieuw. - Back-up wordt hersteld: Herstelsleutel wordt berekend… Sleutels worden gedownload… @@ -1332,7 +1110,6 @@ Geschiedenis ontgrendelen Voer een herstelsleutel in De back-up kan met deze herstelsleutel niet ontsleuteld worden: controleer of u de juiste herstelsleutel heeft ingevoerd. - Back-up hersteld %s! Back-up met %d sleutel hersteld. @@ -1342,18 +1119,13 @@ Er is %d nieuwe sleutel toegevoegd aan deze sessie. Er zijn %d nieuwe sleutels toegevoegd aan deze sessie. - Verkrijgen van laatste herstelsleutelversie (%s) mislukt. Sessieversleuteling is niet actief - - Herstellen uit back-up Back-up verwijderen - Sleutelback-up is correct ingesteld voor deze sessie. Sleutelback-up is niet actief op deze sessie. Uw sleutels worden niet geback-upt vanaf deze sessie. - De back-up heeft een ondertekening van een onbekende sessie met ID %s. De back-up heeft een geldige ondertekening van deze sessie. De back-up heeft een geldige ondertekening van de geverifieerde sessie %s. @@ -1361,14 +1133,11 @@ De back-up heeft een ongeldige ondertekening van de geverifieerde sessie %s De back-up heeft een ongeldige ondertekening van de ongeverifieerde sessie %s Verkrijgen van vertrouwensinformatie voor back-up mislukt (%s). - Herstel nu met uw wachtwoord of herstelsleutel om sleutelback-up op deze sessie te gebruiken. Back-up wordt verwijderd… Verwijderen van back-up is mislukt (%s) - Back-up verwijderen Uw geback-upte versleutelingssleutels verwijderen van de server\? U zult uw herstelsleutel niet meer kunnen gebruiken om de versleutelde berichtgeschiedenis te lezen. - Nieuwe sleutelback-up Er is een nieuwe sleutelback-up voor versleutelde berichten gedetecteerd. \n @@ -1376,35 +1145,26 @@ Ik was het Verlies nooit uw versleutelde berichten Begin sleutelback-up te gebruiken - Verlies nooit uw versleutelde berichten Sleutelback-up gebruiken - Nieuwe sleutels voor versleutelde berichten Beheren in sleutelback-up - Back-up van sleutels wordt gemaakt… - Alle sleutels zijn geback-upt Back-up van %d sleutel wordt gemaakt… Back-up van %d sleutels wordt gemaakt… - Versie Algoritme Ondertekening - Ongeldig thuisserverontdekkingsantwoord Serveropties automatisch aanvullen Element heeft een aangepaste serverconfiguratie gedetecteerd voor uw gebruikers-ID-domein ‘%1$s’: \n%2$s Configuratie gebruiken - Sorry, vergadergesprekken met Jitsi worden nog niet ondersteund op oudere apparaten (met een Android-versie lager dan 5.0) - Sessie verifiëren - onbekend IP-adres Een nieuwe sessie vraagt versleutelingssleutels aan. \nSessienaam: %1$s @@ -1414,45 +1174,36 @@ \nSessienaam: %1$s \nLaatst gezien: %2$s \nAls u zich niet heeft aangemeld op een andere sessie, negeer dan dit verzoek. - Verifiëren Delen Sleuteldeelverzoek Negeren - Verifieer door een korte tekenreeks te vergelijken. Voor een maximale beveiliging bevelen we aan om dit onder vier ogen te doen, of via een ander vertrouwd communicatiekanaal. Verificatie beginnen Inkomend verificatieverzoek Verifieer de sessie door deze als vertrouwd te markeren. Door de sessie van uw gesprekspartners te vertrouwen, hoeft u zich nog minder zorgen te maken over het gebruik van eind-tot-eind-versleutelde berichten. De sessie verifiëren zal deze als vertrouwd markeren, en deze ook aan uw gesprekspartner als vertrouwd markeren. - Verifieer deze sessie door te bevestigen dat de volgende emoticons op het scherm van uw gesprekspartner verschijnen Verifieer deze sessie door te bevestigen dat de volgende cijfers op het scherm van uw gesprekspartner verschijnen - U heeft een inkomend verificatieverzoek ontvangen. Verzoek bekijken Wachten op bevestiging van gesprekspartner… - Geverifieerd! U heeft de sessie geverifieerd. Beveiligde berichten met deze gebruiker worden eind-tot-eind-versleuteld en kunnen niet door derde partijen gelezen worden. Ik snap het - Verschijnt er niets\? Nog niet alle cliënten bieden ondersteuning voor interactieve verificatie. Gebruik de traditionele verificatiemethode. Traditionele verificatie gebruiken. - Sleutelverificatie Verzoek geannuleerd De andere partij heeft de verificatie geannuleerd. \n%s De verificatie is geannuleerd. \nReden: %s - Interactieve sessieverificatie Verificatieverzoek %s wil uw sessie verifiëren - De gebruiker heeft de verificatie geannuleerd Het verificatieproces is verlopen De sessie heeft geen weet van die transactie @@ -1464,22 +1215,18 @@ Sleutels komen niet overeen Gebruikers komen niet overeen Onbekende fout - Geen Intrekken Verbinding verbreken Nakijken Weigeren - Geen identiteitsserver geconfigureerd. - Oproep mislukt door verkeerd geconfigureerde server Vraag de beheerder van uw thuisserver (%1$s) om een TURN-server te configureren om oproepen betrouwbaar te doen werken. \n \nAls alternatief kunt u de publieke server op %2$s gebruiken. Dit is minder betrouwbaar en zal tevens uw IP-addres delen met die server. U kunt dit ook configureren in de Instellingen. Probeer %s te gebruiken Vraag het niet opnieuw - Kies een e-mailadres om te gebruiken voor accountherstel. Later kunt u ervoor kiezen om mensen u te laten vinden via uw e-mailadres. Kies een telefoonnummer. Later kunt u ervoor kiezen om mensen u te laten vinden via dit nummer. Kies een e-mailadres om te gebruiken voor accountherstel. Later kunt u ervoor kiezen om mensen u te laten vinden via uw e-mailadres of telefoonnummer. @@ -1500,25 +1247,19 @@ Geen achtergrondssynchronisatie U zal geen melding van berichten ontvangen als de app zich in de achtergrond bevindt. Kon de instellingen niet bijwerken. - - Voorkeur voor synchronisatie-interval %s \nDe synchronisatie is mogelijk uitgesteld als gevolg van de batterij of staat van uw apparaat (slaapmodus). Integraties Latn - Terugvaloproepassistentieserver toestaan Authenticatie vereist - - Gebruik een integratiebeheerder om bots, bruggen, widgets en stickerpakketten te beheren. \nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, gespreksuitnodigingen versturen en bestuursniveaus instellen namens u. Ontdekken Beheer uw ontdekinstellingen. Integraties toestaan Integratiebeheerder - Publieke naam (zichtbaar voor mensen met wie u communiceert) De publieke naam van een sessie is zichtbaar voor mensen met wie u communiceert Widget @@ -1531,46 +1272,35 @@ Widget herladen Openen in browser Toegang intrekken voor mij - Uw weergavenaam Uw profielfoto-URL Uw gebruikers-ID Uw thema Widget-ID Gespreks-ID - - Deze widget wil gebruik maken van de volgende bronnen: Toestaan Alles blokkeren Camera gebruiken Microfoon gebruiken DRM-beschermde media lezen - Geen integratiebeheerder ingesteld. Om verder te gaan dient u de dienstvoorwaarden te aanvaarden. - Er bestaat al een back-up op uw thuisserver Het lijkt erop dat u al een back-up van uw herstelsleutel heeft uit een andere sessie. Wilt u deze vervangen door degene die u nu aanmaakt\? Vervangen Stoppen - Back-upstatus wordt gecontroleerd U bent afgemeld vanwege onjuiste of verlopen gebruikersreferenties. - U gebruikt geen identiteitsserver Er is geen identiteitsserver geconfigureerd. Dit is vereist om uw wachtwoord opnieuw in te stellen. - Het lijkt er op dat je probeert verbinding te maken met een andere thuisserver. Wil je uitloggen\? - Bewerken Beantwoorden - Opnieuw proberen Betreed een kamer om de applicatie te gebruiken. Heeft je een uitnodiging gestuurd Uitgenodigd door %s - Je bent helemaal bij! Je hebt geen ongelezen berichten meer Welkom thuis! @@ -1579,19 +1309,15 @@ Je directe gesprekken zullen hier worden weergegeven Kamers Je kamers zullen hier worden weergegeven - Reacties Bevestigen Leuk vinden Reactie Toevoegen Reacties Bekijken Reacties - Gebeurtenis verwijderd door gebruiker Gebeurtenis gemodereerd door gesprek beheerder Laatst bewerkt door %1$s op %2$s - - Niet correcte gebeurtenis, kan niet weergeven Maak een nieuw gesprek aan Geen netwerk. Controleer uw internet verbinding. @@ -1599,16 +1325,15 @@ Wijzig netwerk Even wachten… Alle Gemeenschappen - Dit gesprek kan niet worden voorvertoond De voorvertoning van wereld-leesbare gesprekken zijn nog niet ondersteund in Element - Gesprekken Directe Berichten - Nieuw Gesprek AANMAKEN Gespreksnaam Publiek Iedereen zal dit gesprek kunnen toetreden - + Pauzeren + Afspelen + \ No newline at end of file diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 6501b49aee..a7d29516b2 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -29,7 +29,7 @@ Wideo Nie można rozpocząć połączenia, spróbuj ponownie później Ze względu na brak pewnych uprawnień, niektóre funkcje mogą nie działać… - Musisz posiadać uprawninenia, aby rozpocząć połączenie grupowe + Musisz posiadać uprawnienia, aby rozpocząć połączenie grupowe Nie można rozpocząć połączenia Informacje o sesji Połączenia grupowe nie są obsługiwane w szyfrowanych pokojach @@ -1777,7 +1777,7 @@ Spróbuj uruchomić ponownie aplikację. Zamknij Wstrzymaj Odtwórz - Nie posiadasz wymaganych uprawnień do rozpoczęcia połaczenia w tym pokoju + Nie posiadasz wymaganych uprawnień do rozpoczęcia połączenia w tym pokoju Odbierz Usuwanie widżetu nie powiodło się Dodawanie widżetu nie powiodło się @@ -1787,6 +1787,6 @@ Spróbuj uruchomić ponownie aplikację. Rozpocznij połączenie wideo Połączenie grupowe już trwa! Nie posiadasz wymaganych uprawnień aby rozpocząć połączenie grupowe w tym pokoju - Nie posiadasz wymaganych uprawnień do rozpoczęcia połaczenia + Nie posiadasz wymaganych uprawnień do rozpoczęcia połączenia Nie posiadasz wymaganych uprawnień aby rozpocząć połączenie grupowe \ No newline at end of file diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index c040882e80..65db55c162 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -1808,7 +1808,7 @@ Envia o emoji colorido como um arco-íris Conversas Campo de texto - Ativar a criptografia de ponta a ponta + Ativar a criptografia de ponta a ponta… Uma vez ativada, a criptografia não poderá ser desativada. Ativar criptografia\? Uma vez ativada, a criptografia de uma sala não pode ser desativada. As mensagens enviadas em uma sala criptografada não podem ser lidas pelo servidor, apenas pelos participantes desta sala. A ativação da criptografia pode impedir que muitos bots e integrações funcionem corretamente. @@ -2254,4 +2254,59 @@ Não há mais resultados Exportar auditoria Enviar mensagem + Mostrar mais + Esconder mais + Link na Matrix + %s para que as pessoas saibam do que se trata esta sala. + Digite o endereço da sala + Recentes + Código QR não escaneado! + Código QR inválido (URL inválido)! + Não é possível enviar mensagens para si mesmo! + Compartilhar por texto + Pesquisar contatos na Matrix + Definir foto + A autorização do usuário não foi fornecida. + Compartilhe este código com as pessoas, para que possam escaneá-lo, de modo a adicionar seu contato e começar a conversar. + Meu código + Compartilhar meu código + Escanear um código QR + Não é um código QR da Matrix válido + 🔐️ Junte-se a mim no Element + Ei, fale comigo no Element: %s + Convidar amigos + Adicionar pessoas + "Descrição:· " + Adicionar uma descrição + Este é o começo do seu histórico de mensagens com %s. + Este é o começo desta conversa. + Este é o início de %s. + Você não tem permissão para ativar a criptografia nesta sala. + Criando sala… + Alguns caracteres não são permitidos + Este endereço já está em uso + Endereço da sala + Você pode habilitar essa opção se a sala for usada apenas para colaboração com equipes internas em seu servidor local. Essa opção não poderá ser alterada mais tarde. + Impedir que qualquer pessoa que não faça parte de %s jamais entre nesta sala + %1$d de %2$d + Começar uma nova conversa escaneando um código QR + Começar uma nova conversa com um ID na Matrix + De modo a descobrir contatos a partir de pessoas que você conhece, você aceita enviar seus dados de contato (números de telefone e/ou e-mails) para o servidor de identidade configurado (%1$s)\? +\n +\nPara obter mais privacidade, os dados enviados serão criptografados antes de serem enviados. + Enviar e-mails e números de telefone + Autorizar + Revogar minha autorização + Você não autorizou o envio de e-mails e números de telefone para este servidor de identidade, de modo a descobrir outras pessoas a partir dos seus contatos. + Você autorizou o envio de e-mails e números de telefone para este servidor de identidade, de modo a descobrir outras pessoas a partir dos seus contatos. + Enviar e-mails e números de telefone + Sugestões + Contatos + Pessoas conhecidas + Código QR + Adicionar por código QR + Pesquise por nome ou ID + Aceite a permissão para acessar seus contatos. + Para escanear um código QR, você precisa permitir o acesso à câmera. + Começar a conversar \ No newline at end of file diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 5b04407645..7716955d23 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -557,7 +557,7 @@ Разрешить Проверить сессию Чтобы убедиться, что этой сессии можно доверять, обратитесь к его владельцу, используя другие способы (например, лично или по телефону), и спросите, соответствует ли ключ, который он видит в настройках для этой сессии: - "Если совпадает, то нажмите кнопку подтвердить ниже. Если не совпадает, возможно кто-то пытается перехватить сессию и вы захотите добавить его в черный список. В будущем данный процесс будет улучшен." + Если совпадает, то нажмите кнопку подтвердить ниже. Если не совпадает, возможно кто-то пытается перехватить сессию и вы захотите добавить его в черный список. В будущем данный процесс будет улучшен. Я проверил, что ключи совпадают Комната содержит неизвестные сессии @@ -647,7 +647,7 @@ Зашифрованное сообщение Сведения о сообществе Загрузка… - "Закрыть приложение" + Закрыть приложение Сообщества Поиск сообществ Пригласить @@ -1053,7 +1053,7 @@ Удаление резервной копии… Чтобы использовать резервную копию ключа в этой сессии, восстановите его с помощью своей парольной фразы или ключа восстановления. Резервная копия имеет недействительную подпись из подтвержденной сессии %s - "Резервная копия имеет действительную подпись из неподтвержденной сессии %s" + Резервная копия имеет действительную подпись из неподтвержденной сессии %s Резервная копия имеет действительно подпись из подтверждённой сессии %s. Резервная копия имеет действительную подпись с этой сессии. Резервная копия подписана сессией с идентификатором %s. @@ -1192,7 +1192,7 @@ Использовать настройку Проверить сессию Ваше устройство использует устаревший TLS протокол, уязвимый для атак, для вашей же безопасности вам отказано в подключении - Приложениям не" нужно подключаться к HomeServer в фоновом режиме, это должно снизить расход заряда батареи" + Приложениям не нужно подключаться к домашнему серверу в фоновом режиме, это должно снизить расход заряда батареи Клавиша Ввод отправит сообщение вместо переноса строки Воспроизвести звук затвора неизвестный IP @@ -1252,7 +1252,7 @@ Несоответствие пользователя Неизвестная ошибка Резервная копия существует на домашнем сервере - "Похоже, у вас уже есть резервная копия ключа настройки из другой сессии. Хотите заменить его новым\?" + Похоже, у вас уже есть резервная копия ключа настройки из другой сессии. Хотите заменить его новым\? Заменить Стоп Проверка состояния резервного копирования @@ -1348,7 +1348,7 @@ Ссылка скопирована в буфер обмена Добавить по Matrix ID Создание комнаты… - "Результат не найден, используйте добавить matrix ID для поиска на сервере." + Результатов не найдено, используйте \"Добавить по matrix ID\" для поиска на сервере. Начните печатать, чтобы получить результат Фильтр по имени пользователя или ID… Присоединение к комнате… @@ -1406,7 +1406,7 @@ Настроить идентификационный сервер Изменить идентификационный сервер В настоящее время вы используете %1$s для обнаружения и быть найденным вашими контактами. - "Вы в настоящее время не используете идентификационный сервер. Чтобы обнаружить и быть найденным вашими существующими контактами, настройте один из них ниже." + Вы в настоящее время не используете идентификационный сервер. Чтобы обнаружить и быть найденным вашими существующими контактами, настройте один из них ниже. Видимые адреса электронной почты Доступные номера телефонов В ожидании @@ -1444,7 +1444,7 @@ Cyrl Используйте менеджер интеграций чтобы управлять ботами, мостами, виджетами и наборами стикеров. \nМенеджеры интеграций получают данные о конфигурации, могут изменять виджеты, отправлять приглашения в комнаты и устанавливать права от вашего имени. - "Использование может оставить cookie на вашем устройстве и отправить данные в %s:" + Использование может оставить cookie на вашем устройстве и отправить данные в %s: Использование может отправить данные в %s: Не удалось загрузить виджет. \n%s @@ -1880,7 +1880,7 @@ Посылает сообщение, окрашенное в цвет радуги Посылает данную эмоцию, окрашенную в цвет радуги Редактор сообщений - Включить сквозное шифрование + Включить сквозное шифрование… После включения шифрования оно не может быть отключено. Активировать шифрование\? После включения шифрование для комнаты не может быть отключено. Сообщения, отправленные в зашифрованном помещении, не могут быть замечены сервером, только участниками помещения. Включение шифрования может помешать правильной работе многих ботов и мостов. @@ -1929,7 +1929,7 @@ Сообщения, содержащие @room Отладка Настройки важности уведомлений для событий - Используйте последнюю версию Element на других ваших устройствах, веб-клиент Element, Element для ПК, Element для iOS, Element для Андроид или другой клиент Matrix, поддерживающий кросс-подпись + Используйте последнюю версию Element на других ваших устройствах, веб-клиент Element, Element для ПК, Element для iOS, Element для Android или другой клиент Matrix, поддерживающий кросс-подпись Используйте последнюю версию Element на других ваших устройствах: Подтвердите новую сессию вашей учетной записи: %1$s Настроить безопасное резервное копирование @@ -2301,4 +2301,63 @@ Добавить изображение из Тема Название Комнаты + Вы дали свое согласие на отправку электронных писем и телефонных номеров на этот сервер идентификации для обнаружения других пользователей из ваших контактов. + Добавить по QR-коду + Поиск по имени или идентификатору + Разрешить доступ к вашим контактам. + Чтобы отсканировать QR-код, вам нужно разрешить доступ к камере. + Ссылка Matrix + QR-код не отсканирован! + Недействительный QR-код (недопустимый URI)! + Нельзя отправлять сообщения самому себе! + Поделиться по тексту + Поиск контактов в Matrix + Установить аватар + Согласие пользователя не предоставлено. + Поделитесь этим кодом с людьми, чтобы они могли отсканировать его, добавить вас и начать общение. + Мой код + Поделиться моим кодом + Сканировать QR-код + Это недействительный QR-код matrix + 🔐️ Присоединяйтесь ко мне в Element + Привет, поговори со мной в Element: %s + Пригласить друзей + Добавить людей + "Тема: " + Добавьте тему + Это начало разговора. + Это начало %s. + У вас нет разрешения на включение шифрования в этой комнате. + Создание комнаты… + Некоторые символы не разрешены + Укажите адрес комнаты + Этот адрес уже используется + Адрес комнаты + Вы можете включить это, если комната будет использоваться только для совместной работы с внутренними командами на вашем домашнем сервере. Это не может быть изменено позже. + Запретить кому-либо, не входящему в %s, когда-либо присоединяться к этой комнате + Скрыть дополнительные настройки + Показать дополнительные настройки + %1$d из %2$d + Согласны ли вы на отправку своих контактных данных (номера телефонов и/или электронную почту) на настроенный сервер идентификации (%1$s) для обнаружения контактов\? +\n +\nДля большей конфиденциальности отправленные данные перед отправкой будут хешированы. + Дать согласие + Отозвать моё согласие + Вы не дали свое согласие на отправку электронной почты и номеров телефонов на этот сервер идентификации для обнаружения других пользователей из ваших контактов. + Больше никаких результатов + Предложения + Контакты + Известные пользователи + QR-код + Отправить историю запросов на обмен ключами + Начать беседу + Недавние + %s чтобы люди знали, о чём эта комната. + Это начало вашей истории диалога с %s. + Экспорт аудита + Диалог + Создать новый диалог через сканирование QR-кода + Создать новый диалог по Matrix ID + Отправить электронную почту и номера телефонов + Отправить электронную почту и номера телефонов \ No newline at end of file diff --git a/vector/src/main/res/values-sl/strings.xml b/vector/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000000..d491fb8b86 --- /dev/null +++ b/vector/src/main/res/values-sl/strings.xml @@ -0,0 +1,10 @@ + + + Ime + Prijava hrošča + Pošlji ekransko sliko + Ponvno pošlji + Pošlji + Sinhroniziram… + ZDA + \ No newline at end of file diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index ce76f7c387..9be2118399 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1099,7 +1099,8 @@ Na ndjeni, thirrjet konferencë me Jitsi-n nuk mbulohen në pajisje të vjetra (pajisje me Android OS nën 5.0) Verifiko sesion ip e panjohur - Një sesion i ri po kërkon emër keys.ession fshehtëzimi: %1$s + Një sesion i ri po kërkon emër kyçe fshehtëzimi. +\nEmër sesioni: %1$s \nParë së fundi më: %2$s \nNëse s’keni bërë hyrje në një tjetër sesion, shpërfilleni këtë kërkesë. Një sesion i paverifikuar po kërkon kyçe fshehtëzimi. @@ -1615,7 +1616,7 @@ E dërgon emote-n e dhënë të ngjyrosur si ylber Rrjedhë kohore Përpunues mesazhesh - Aktivizoni fshehtëzim skaj-më-skaj + Aktivizoni fshehtëzim skaj-më-skaj… Pasi të aktivizohet, fshehtëzimi s’mund të çaktivizohet më. Të aktivizohet fshehtëzimi\? Pasi të aktivizohet, fshehtëzimi për një dhomë nuk mund të çaktivizohet. Mesazhet e dërguar në një dhomë të fshehtëzuar s’mund të shihen nga shërbyesi, vetëm nga pjesëmarrësit te dhoma. Aktivizimi i fshehtëzimit mund të pengojë funksionimin si duhet të mjaft robotëve dhe urave. @@ -2181,4 +2182,58 @@ Mesazh i drejtpërdrejtë Dërgo historik kërkesash për dhënie kyçesh S’ka më përfundime + Lidhje Matrix + Kod QR jo i skanuar! + Kod QR i pavlefshëm (URI e pavlefshme)! + S’mund t’i dërgoni mesazh të drejtpërdrejtë vetes! + Jepuani si tekst + Kërkoni për kontakte në Matrix + Caktoni avatar + S’është dhënë pranimi nga përdoruesi. + Jepuani këtë kod njerëzve, që të mund ta skanojnë për t’ju shtuar dhe për të filluar të bisedoni. + Kodi im + Ndaje kodin tim me të tjerët + Skanoni një kod QR + S’është kod QR Matrix i vlefshëm + Takohuni me mua në Element + Hej, bisedoni me mua në Element: %s + Ftoni shokë + Shtoni persona + "Temë: " + Shtoni një temë + %s, që t’u bëni me dije njerëzve se për çfarë është kjo dhomë. + Ky është fillimi i historikut të mesazheve tuaj të drejtpërdrejtë me %s. + Ky është fillimi i kësaj bisede. + Ky është fillimi i %s. + S’keni leje të aktivizoni fshehtëzim në këtë dhomë. + Po krijohet dhoma… + Disa nga shenja nuk lejohen + Ju lutemi, jepni një adresë dhome + Kjo adresë është e përdorur tashmë + Adresë dhome + Mund ta aktivizoni këtë, nëse dhoma do të përdoret vetëm për bashkëpunim mes ekipesh të brendshëm në shërbyesin tuaj Home. Kjo s’mund të ndryshohet më vonë. + Blloko përgjithnjë pjesëmarrjen në këtë dhomë të kujtdo që s’është pjesë e %s + Fshihi të mëtejshmet + Shfaq të mëtejshme + %1$d nga %2$d + Krijoni një bisedë të re të drejtpërdrejtë duke skanuar një kod QR + Krijoni një bisedë të re të drejtpërdrejtë përmes ID-je Matrix + Që të mund të zbulohet kontakte ekzistuese që njihni, pranoni të dërgohen të dhënat tuaja të kontaktit (numra telefonash dhe/ose email-e) te Shërbyesi i formësuar për Identitete (%1$s)\? +\n +\nPër më tepër privatësi, të dhënat e dërguara do të kodohen, para se të dërgohen. + Dërgoni email-e dhe numra telefonash + Jepe pranimin + Shfuqizoje pranimin tim + S’keni dhënë pranimin tuaj për të dërguar email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. + Keni dhënë pranimin tuaj për të dërguar email-e dhe numra telefonash te ky shërbyes identitetesh që të zbulojë përdorues të tjerë prej kontakteve tuaj. + Dërgo email-e dhe numra telefonash + Sugjerime + Kontakte + Përdorues të Ditur + Kod QR + Shtoni përmes kodi QR + Kërkoni sipas emri ose ID-je + Që të skanoni një kod QR, lypset të lejoni përdorim kamere. + Filloni të Llafoseni + Jepni leje për hyrje te kontaktet tuaja. \ No newline at end of file diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index 4f71783b20..f5a848f819 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -749,7 +749,7 @@ Moderatorer Den här sessionen kan inte dela den här verifikationen med dina andra sessioner. \nVerifikationen kommer att sparas lokalt och delas i en framtida version av appen. - Aktivera totalsträckskryptering + Aktivera totalsträckskryptering… Efter aktivering så kan kryptering inte avaktiveras. Efter aktivering så kan kryptering inte avaktiveras. Meddelanden som skickas i ett krypterat rum kan inte läsas av servern, bara av deltagarna i rummet. Att aktivera kryptering kan göra att många bottar och bryggor inte funkar ordentligt. Meddelanden mellan dig och den här användaren är totalsträckskrypterade och kan inte läsas av tredje parter. diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index fdc69678fd..ff949f88b5 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -9,7 +9,7 @@ Темна Тема OLED Тема - Синхронізую + Синхронізація… Слухати події Повідомлення @@ -56,7 +56,7 @@ Голосовий виклик Відеовиклик Глообальний пошук - Позначити все як прочитане + Позначити все прочитаним Історичний Швидка відповідь Відкрити @@ -80,11 +80,11 @@ Запрошення Низький пріоритет - Діалоги + Бесіди Локальні контакти Каталог користувачів Лише Matrix-контакти - Немає діалогів + Немає бесід Ви не надали Element доступу до контактів Немає результатів @@ -127,7 +127,7 @@ Увійти Реєстрація - Вогонь! + Надіслати Пропустити Скинути Повернутися на екран входу @@ -136,7 +136,7 @@ Новий пароль Логін Email адреса - Email адреса (oдодаткова) + Email адреса (додаткова) Номер телефону Номер телефону (додатковий) Повторіть пароль @@ -212,7 +212,7 @@ Вхідний виклик Вхідний Відеовиклик Вхідний Голосовий Виклик - Виконується Виклик + Виконується виклик… Абонент не відповідає. Медіавиклик не вдався Неможливо ініціювати камеру @@ -228,10 +228,10 @@ Для здійснення аудіодзвінків потрібен доступ до мікрофону. \n\nБудь ласка, надайте його у наступному виринаючому вікні, щоб мати змогу здійснити дзвінок. Для здійснення відеодзвінків потрібен доступ до камери та мікрофону.\n\nБудь ласка, надайте його у наступних виринаючих вікнах, щоб мати змогу їх здійснити. - Element потребує доступу до ваших контактів, щоб знайти інших користувачів Matrix за їх email та номерами телефонів.\n\nБудь ласка, надайте його у наступному виринаючому діалозі, щоб знати, які з контактів теж використовують Element. - Element потребує доступу до ваших контактів, щоб знайти інших користувачів Matrix по email чи за номерами телефонів. - -Надати Element доступ до ваших контактів? + Element потребує доступу до ваших контактів, щоб знайти інших користувачів Matrix за їх електронними адресами та номерами телефонів. Якщо ви згодні, надайте його у наступному діалоговому вікні, щоб знати, які з контактів теж використовують Element. + Element потребує доступу до ваших контактів, щоб знайти інших користувачів Matrix за електронною поштою чи номерами телефонів. +\n +\nНадати Element доступ до ваших контактів\? Вибачте.. Дію не виконано через нестачу дозволів Збережено @@ -329,9 +329,9 @@ ПРИЄДНАЛИСЯ Причина скарги на цей вміст - Бажаєте сховати всі повідомлення цього користувача? - -Зауважте, що це перезавантажить додаток та може зайняти деякий час. + Бажаєте сховати всі повідомлення цього користувача\? +\n +\nЗауважте, що це перезавантажить застосунок та може тривати деякий час. Скасувати відвантаження Скасувати завантаження @@ -356,7 +356,7 @@ Введіть id кімнати чи аліас Огляд каталогу - Пошук у каталозі.. + Пошук у каталозі… Обране Не терміново @@ -433,7 +433,7 @@ Операція потребує додаткової аутентифікації.\nДля продовження введіть ваш пароль. Аутентифікація Пароль: - Вогонь! + Надіслати Залоговано як Cервер Сервер Аутентифікації @@ -452,9 +452,9 @@ Підтвердіть пароль Не вдалося оновити пароль Пароль успішно оновлено - Показувати всі повідомлення %s? - -Зауважте, що це перезавантажить додаток та може зайняти деякий час. + Показувати всі повідомлення %s\? +\n +\nЗауважте, що це перезавантажить застосунок та може тривати деякий час. Ви справді бажаєте видалити цю ціль сповіщень? Справді бажаєте видалити %1$s %2$s? Оберіть країну @@ -530,7 +530,7 @@ Каталог - %s намагався завантажити певну точку на шкалі часу цієї кімнати, але не зміг її знайти. + %s намагався завантажити певну точку зі стрічки подій цієї кімнати, але не зміг її знайти. Дані про наскрізне шифрування Інформація про подію @@ -553,9 +553,9 @@ Експорт Введіть парольну фразу Підтвердіть парольну фразу - E2E ключі кімнати збережено у \'%s\' - -Попередження: цей файл може бути видалений, якщо видалити додаток. + E2E ключі кімнати збережено до \'%s\'. +\n +\nПопередження: цей файл може бути видалено, якщо видалити застосунок. Імпортувати E2E ключі кімнати Імпортувати ключі кімнати Імпортувати ключі з локального файлу @@ -643,7 +643,7 @@ Запросити Спільноти Нема груп - "Струснути пристрій, щоб повідомити про помилку" + Струснути пристрій, щоб повідомити про помилку Ви дійсно хочете почати новий чат з %s? Ви впевнені, що бажаєте почати голосовий виклик? Ви впевнені, що бажаєте почати відео виклик? @@ -661,14 +661,14 @@ Всі повідомлення Тільки упоминания Без звука - Добавити ярлик на головний екран + Додати ярлик на головний екран Конфіденційність сповіщень Нормальний Відправити наліпку Відправити стікер - У вас зараз не має доступних стікерів. - -Добавити зараз? + У вас зараз не має стікерів. +\n +\nДодати зараз\? %d активний участник %d активних участника @@ -681,7 +681,7 @@ %d участників - Ви впевнені, що хочете забанити цього користувача в цьому чаті? + Користувачів з забороною буде вилучено з цієї кімнати й вони не зможуть приєднатися знову. %d нове повідомлення %d нових повідомленя @@ -720,22 +720,22 @@ Якщо можливо, будь ласка, напишіть опис англійською. продовжити з… - %d сек. - %d сек. - %d сек. - + %dс + %dsс + %dsс + %dsс - %d хв. - %d хв. - %d хв. - + %dхв + %dхв + %dхв + %dхв - %d год. - %d год. - %d год. - + %dгод + %dгод + %dгод + %dгод %d день @@ -751,10 +751,10 @@ Надіслати зашифровану відповідь… Надіслати відповідь (незашифровано)… - %d обрано - "%d обрано" - "%d обрано" - + %d вибрано + %d вибрано + %d вибрано + %d вибрано %d кімната @@ -867,11 +867,11 @@ Щоб продовжити користуватися домашнім сервером %1$s Вам слід ознайомитись та прийняти Умови використання. Ознайомитись зараз Деактивувати обліковий запис - Це зробить Ваш обліковий запис непридатним до подальшого використання. Ви не будете спроможні заходити у систему, та ніхто інший не зможе зареєструватися з таким самим ID. Це спричинить вихід Вашого облікового запису з усіх кімнат, в яких він зареєстрований, а також видалить усі данні облікового запису із Вашого серверу ідентифікації. Ця дія незворотня. - -Деактивація Вашого облікового запису за замовчанням не спричинить видалення надісланих Вами повідомлень. Якщо Ви бажаєте позбавитись Ваших повідомлень, будь ласка, поставте позначку під цим текстом. - -Відображення повідомлень у Matrix подібне до електронної пошти. Таке видалення означає, що Ваші надіслані повідомлення не будуть поширюватись серед нових або незареєстрованих користувачів, проте зареєстрованим користувачам, які мали доступ до цих повідомлень, все ще будуть доступні їх копії. + Це зробить ваш обліковий запис непридатним до подальшого використання. Ви не зможете заходити до системи, та ніхто інший не зможе зареєструватися з таким самим ID. Це спричинить вихід з вашого облікового запису з усіх кімнат, до яких ви долучилися, а також вилучить усі данні облікового запису із вашого сервера ідентифікації. Ця дія незворотня. +\n +\nДеактивація вашого облікового запису типово не спричинить видалення надісланих вами повідомлень. Якщо ви бажаєте позбавитись цих повідомлень, залиште позначку під цим текстом. +\n +\nПоказ повідомлень у Matrix подібне до електронної пошти. Таке видалення означає, що ваші надіслані повідомлення не будуть поширюватись серед нових або незареєстрованих користувачів, проте зареєстрованим користувачам, які мали доступ до цих повідомлень й надалі будуть доступні їхні копії. Будь ласка, після деактивації мого облікового запису видаліть усі надіслані мною повідомлення (Увага: це спричинить неповне відображення розмов для нових користувачів) Для продовження, будь ласка, введіть Ваш пароль: Деактивувати обліковий запис @@ -934,7 +934,7 @@ Запуск сервісу Резервна копія ключа Використати резервну копію ключа - Тут буде список Ваших прямих чатів + Тут з\'явиться список ваших прямих чатів Повідомлення видалено Прямі чати Прямі чати @@ -1012,10 +1012,120 @@ Створити нову кімнату Кімнати Пошук в зашифрованих кімнатах не підтримується на даний момент. - Запитувати підтвердження перед початком дзвінку - Запобігати випадковим дзвінкам + Запитувати підтвердження перед початком виклику + Запобігати випадковим викликам Мені не потрібні мої зашифровані повідомлення Скасувати зміни SSL помилка. Ви не можете здійснити дзвінок із самим собою, дочекайтесь, доки інші учасники приймуть ваше запрошення + ФАЙЛИ + В цій кімнаті немає медіа + МЕДІА + Завантаження + Підтримується тільки в зашифрованих кімнатах + Інші кімнати + Нова кімната + Створити нову кімнату + Запропонувати + Режим розробника + Додаткові налаштування + Розширені налаштування сповіщень + Голос та відео + Налаштування + Інші сеанси + Поточний сеанс + Налаштування + Сповіщення + Налаштування + Відкликати + немає + Ви втратите доступ до своїх зашифрованих повідомлень, якщо не зробите резервну копію ключів перед виходом з системи. + Безпечне резервне копіювання ключів має бути активним у всіх ваших сеансах, щоб не втратити доступу до ваших зашифрованих повідомлень. + Триває резервне копіювання ключів. Якщо вийти зараз, ви втратите доступ до своїх зашифрованих повідомлень. + Резервне копіювання ключів не завершено, зачекайте… + Увімкнути мікрофон + Вимкнути мікрофон + Відерити бесіду + Роль + Призначити роль + Надіслати + Номери телефонів + Email адреси + Скасувати запрошення + 🔐️ Приєднуйтесь до мене в Element + Привіт, поспілкуйся зі мною в Element: %s + Запросити друзів + Всі спільноти + Показувати заглушку на місці видалених повідомлень + Основні + Не вдалось надіслати пропозицію (%s) + Дякуємо, вашу пропозицію було успішно надіслано + Опишіть вашу пропозицію тут + Залиште вашу пропозицію внизу. + Показувати видалені повідомлення + Натискання Enter на програмній клавіатурі буде відправляти повідомлення замість додавання переносу рядку + Відправляти повідомлення натисканням Enter + Налаштування кімнати + Дізнатись більше + В цій кімнаті немає файлів + %1$s о %2$s + Жодного номера телефону не додано до вашого облікового запису + У ваш обліковий запис не додано жодної електронної адреси + Для вашої приватності Element підтримує лише надсилання хешованих електронних адрес користувачів та номера телефону. + Ви не погодилися надіслати електронні адреси та телефонні номери на цей сервер ідентифікації для виявлення інших користувачів із ваших контактів. + Ви погодилися надіслати електронні адреси та телефонні номери на цей сервер ідентифікації для виявлення інших користувачів із ваших контактів. + Надіслати електронні адреси та номери телефонів + Надіслати електронні адреси та номери телефонів + Керування електронними адресами та номерами телефонів, пов’язаними з вашим обліковим записом Matrix + Електорнні адреси та номери телефонів + Надіслати історію запитів спільного доступу до ключів + Стрічка подій + Не прчитані повідомлення + Провести пальцем, щоб відповісти у стрічці подій + Показувати приховані події у стрічці подій + Усунення несправностей + APK служб Google Play доступні та оновлені. + Перевірка служб Play + Перевірка налаштувань + Не вдалося завантажити власні правила, повторіть спробу. + Деякі сповіщення вимкнено у ваших власних налаштуваннях. + Зверніть увагу, що для деяких типів повідомлень встановлено беззвучність (беззвучні сповіщення). + Власні налаштування. + Увімкнути + Сповіщення не ввімкнено для цього сеансу. +\nПеревірте налаштування Element. + Сповіщення ввімкнено для цього сеансу. + Налаштування сеансу. + Увімкнути + Сповіщення для вашого облікового запису вимкнено. +\nПеревірте налаштування облікового запису. + Сповіщення ввімкнено для вашого облікового запису. + Налаштування облікового запису. + Сповіщення ввімкнено в налаштуваннях системи. + Налаштування системи. + Виявлення несправностей + Перевірка… (%1$d з %2$d) + Перевірити + Сповіщення усунення несправностей + Захистити приватність бесід шифруванням + Фільтрувати бесіди… + Бесіди + У вас більше немає непрочитаних повідомлень + Все прочитано! + Встановти важливість сповіщень за подіями + Сховати розширені + Показати розширені + Власні та розширені налаштування + (Розширені) + Довідка та опис + Налаштування + Виберіть колір світлодіода, вібрацію, звук… + Налаштування приглушених сповіщень + Налаштування сповіщень викликів + Налаштування гучних сповіщень + Початкова синхронізація… + Видимі електронні адреси + Недійсна відповідь виявлення домашнього сервера + Налаштуйте свою видимість. + Видимість \ No newline at end of file diff --git a/vector/src/main/res/values-v23/theme_status.xml b/vector/src/main/res/values-v23/theme_status.xml deleted file mode 100644 index 236864d4b8..0000000000 --- a/vector/src/main/res/values-v23/theme_status.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 18ced0a071..ab0ecbe4e9 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -18,6 +18,7 @@ @color/riotx_header_panel_text_secondary_black @color/riotx_text_primary_black @color/riotx_text_secondary_black + @color/riotx_text_tertiary_black @color/riotx_text_primary_body_contrast_black @color/riotx_android_secondary_black @color/riotx_search_placeholder_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index cdd5cde488..86fbb57608 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -16,6 +16,7 @@ @color/riotx_header_panel_text_secondary_dark @color/riotx_text_primary_dark @color/riotx_text_secondary_dark + @color/riotx_text_tertiary_dark @color/riotx_text_primary_body_contrast_dark @color/riotx_android_secondary_dark @color/riotx_search_placeholder_dark @@ -193,6 +194,15 @@ @transition/image_preview_transition @transition/image_preview_transition + + @style/WidgetButtonSocialLogin.Google.Dark + @style/WidgetButtonSocialLogin.Github.Dark + @style/WidgetButtonSocialLogin.Facebook.Dark + @style/WidgetButtonSocialLogin.Twitter.Dark + @style/WidgetButtonSocialLogin.Apple.Dark + + + @android:color/transparent - -