diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 1f93d1feee..93ac86f417 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -12,12 +12,15 @@ <w>fdroid</w> <w>gplay</w> <w>hmac</w> + <w>homeserver</w> <w>ktlint</w> <w>linkified</w> <w>linkify</w> <w>megolm</w> <w>msisdn</w> + <w>msisdns</w> <w>pbkdf</w> + <w>pids</w> <w>pkcs</w> <w>riotx</w> <w>signin</w> diff --git a/CHANGES.md b/CHANGES.md index 803b974413..a4767f8be9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,30 @@ -Changes in RiotX 0.20.0 (2020-XX-XX) +Changes in RiotX 0.21.0 (2020-XX-XX) +=================================================== + +Features ✨: + - Identity server support (#607) + - Switch language support (#41) + +Improvements 🙌: + - Better connectivity lost indicator when airplane mode is on + - Add a setting to hide redacted events (#951) + +Bugfix 🐛: + - Fix issues with FontScale switch (#69, #645) + +Translations 🗣: + - + +SDK API changes ⚠️: + - + +Build 🧱: + - + +Other changes: + - + +Changes in RiotX 0.20.0 (2020-05-15) =================================================== Features ✨: @@ -13,18 +39,10 @@ Bugfix 🐛: - Fix | Verify Manually by Text crashes if private SSK not known (#1337) - Sometimes the same device appears twice in the list of devices of a user (#1329) - Random Crashes while doing sth with cross signing keys (#1364) - -Translations 🗣: - - + - Crash | crash while restoring key backup (#1366) SDK API changes ⚠️: - - excludedUserIds parameter add to to UserService.getPagedUsersLive() function - -Build 🧱: - - - -Other changes: - - + - excludedUserIds parameter added to the UserService.getPagedUsersLive() function Changes in RiotX 0.19.0 (2020-05-04) =================================================== diff --git a/docs/identity_server.md b/docs/identity_server.md new file mode 100644 index 0000000000..04127c9ab0 --- /dev/null +++ b/docs/identity_server.md @@ -0,0 +1,92 @@ +# Identity server + +Issue: #607 +PR: #1354 + +## Introduction +Identity Servers support contact discovery on Matrix by letting people look up Third Party Identifiers to see if the owner has publicly linked them with their Matrix ID. + +## Implementation + +The current implementation was Inspired by the code from Riot-Android. + +Difference though (list not exhaustive): +- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest) +- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true) +- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client. +- The SDK supports incremental sendAttempt (this is not used by RiotX) +- The "Continue" button is now under the information, and not as the same place that the checkbox +- The app can cancel a binding. Current data are erased from DB. +- The API (IdentityService) is improved. +- A new DB to store data related to the identity server management. + +Missing features (list not exhaustive): +- Invite by 3Pid (will be in a dedicated PR) +- Add email or phone to account (not P1, can be done on Riot-Web) +- List email and phone of the account (could be done in a dedicated PR) +- Search contact (not P1) +- Logout from identity server when user sign out or deactivate his account. + +## Related MSCs +The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 + +## Steps and requirements + +- Only one identity server by account can be set. The user's choice is stored in account data with key `m.identity_server`. But every clients will managed its own token to log in to the identity server +```json +{ + "type": "m.identity_server", + "content": { + "base_url": "https://matrix.org" + } +} +``` +- The accepted terms are stored in the account data: +```json +{ + "type": "m.accepted_terms", + "content": { + "accepted": [ + "https://vector.im/identity-server-privacy-notice-1" + ] + } +} +``` + +- Default identity server URL, from Wellknown data is proposed to the user. +- Identity server can be set +- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) RiotX should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever) +- Registration to the identity server is managed with an openId token +- Terms of service can be accepted when configuring the identity server. +- Terms of service can be accepted after, if they change. +- Identity server can be modified +- Identity server can be disconnected with a warning dialog, with special content if there are current bound 3pid on this identity server. +- Email can be bound +- Email can be unbound +- Phone can be bound +- Phone can be unbound +- Look up can be performed, to get matrixIds from local contact book (phone and email): Android permission correctly handled (not done yet) +- Look up pepper can be updated if it is rotated on the identity server +- Invitation using 3PID can be done (See #548) (not done yet) +- Homeserver access-token will never be sent to an identity server +- When user sign-out: logout from the identity server if any. +- When user deactivate account: logout from the identity server if any. + +## Screens + +### Settings + +Identity server settings can be accessed from the internal setting of the application, both from "Discovery" section and from identity detail section. + +### Discovery screen + +This screen displays the identity server configuration and the binding of the user's ThreePid (email and msisdn). This is the main screen of the feature. + +### Set identity server screen + +This screen is a form to set a new identity server URL + +## Ref: +- https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 is a good summary of the role of an Identity server and the proper way to configure and use it in respect to the privacy and the consent of the user. +- API documentation: https://matrix.org/docs/spec/identity_service/latest +- vector.im TOS: https://vector.im/identity-server-privacy-notice diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index e92da1e424..a60e83ec96 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams import im.vector.matrix.android.api.session.group.model.GroupSummary +import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.model.RoomSummary @@ -94,6 +95,11 @@ class RxSession(private val session: Session) { return session.getPagedUsersLive(filter, excludedUserIds).asObservable() } + fun liveThreePIds(refreshData: Boolean): Observable<List<ThreePid>> { + return session.getThreePidsLive(refreshData).asObservable() + .startWithCallable { session.getThreePids() } + } + fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder { session.createRoom(roomParams, it) } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 514d1accae..abc860d1ff 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -158,6 +158,9 @@ dependencies { // Bus implementation 'org.greenrobot:eventbus:3.1.1' + // Phone number https://github.com/google/libphonenumber + implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' + debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0' diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 5bc8653f3d..3ca04a86d1 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -88,7 +88,8 @@ class CommonTestHelper(context: Context) { fun syncSession(session: Session) { val lock = CountDownLatch(1) - session.open() + GlobalScope.launch(Dispatchers.Main) { session.open() } + session.startSync(true) val syncLiveData = runBlocking(Dispatchers.Main) { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt index 9278bed918..f9aef3604a 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CryptoTestHelper.kt @@ -246,7 +246,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { assertNotNull(eventWireContent.get("session_id")) assertNotNull(eventWireContent.get("sender_key")) - assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id")) + assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id")) assertNotNull(event.eventId) assertEquals(roomId, event.roomId) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt index f8d30a2679..da3bbdc23c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/crosssigning/XSigningTest.kt @@ -122,7 +122,7 @@ class XSigningTest : InstrumentedTest { // We will want to test that in alice POV, this new device would be trusted by cross signing val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true)) - val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId!! + val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! // Check that bob first session sees the new login val data = mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index bb6e020d89..57ab4aaf33 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -148,7 +148,7 @@ class KeyShareTests : InstrumentedTest { // Mark the device as trusted aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, - aliceSession2.sessionParams.credentials.deviceId ?: "") + aliceSession2.sessionParams.deviceId ?: "") // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) @@ -253,12 +253,12 @@ class KeyShareTests : InstrumentedTest { }) val txId: String = "m.testVerif12" - aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId + aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId ?: "", txId) mTestHelper.waitWithLatch { latch -> mTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true + aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt index 59ef24beec..2e698a929e 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -835,7 +835,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(signature.valid) assertNotNull(signature.device) assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId) - assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId) stateObserver.stopAndCheckStates(null) cryptoTestData.cleanUp(mTestHelper) @@ -997,7 +997,7 @@ class KeysBackupTest : InstrumentedTest { keysBackup.backupAllGroupSessions(null, it) } - val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!! + val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!! val oldKeyBackupVersion = keysBackup.currentBackupVersion val aliceUserId = cryptoTestData.firstSession.myUserId diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt index 1ac70d7f2b..9bdd8f1131 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt @@ -579,7 +579,7 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, bobSession.myUserId, - bobSession.sessionParams.credentials.deviceId!!, + bobSession.sessionParams.deviceId!!, null) bobVerificationService.beginKeyVerificationInDMs( @@ -587,7 +587,7 @@ class SASTest : InstrumentedTest { requestID!!, cryptoTestData.roomId, aliceSession.myUserId, - aliceSession.sessionParams.credentials.deviceId!!, + aliceSession.sessionParams.deviceId!!, null) // we should reach SHOW SAS on both diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index 5150420de2..effeae596a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -20,7 +20,6 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.wellknown.WellknownResult @@ -37,6 +36,11 @@ interface AuthenticationService { */ fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable + /** + * Request the supported login flows for the corresponding sessionId. + */ + fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable + /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. */ @@ -74,15 +78,6 @@ interface AuthenticationService { */ fun getLastAuthenticatedSession(): Session? - /** - * Get an authenticated session. You should at least call authenticate one time before. - * If you logout, this session will no longer be valid. - * - * @param sessionParams the sessionParams to open with. - * @return the associated session if any, or null - */ - fun getSession(sessionParams: SessionParams): Session? - /** * Create a session after a SSO successful login */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt index 2d65cac43d..1cbba50af7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt @@ -21,7 +21,48 @@ package im.vector.matrix.android.api.auth.data * You don't have to manually instantiate it. */ data class SessionParams( + /** + * Please consider using shortcuts instead + */ val credentials: Credentials, + + /** + * Please consider using shortcuts instead + */ val homeServerConnectionConfig: HomeServerConnectionConfig, + + /** + * Set to false if the current token is not valid anymore. Application should not have to use this info. + */ val isTokenValid: Boolean -) +) { + /* + * Shortcuts. Usually the application should only need to use these shortcuts + */ + + /** + * The userId of the session (Ex: "@user:domain.org") + */ + val userId = credentials.userId + + /** + * The deviceId of the session (Ex: "ABCDEFGH") + */ + val deviceId = credentials.deviceId + + /** + * The current homeserver Url. It can be different that the homeserver url entered + * during login phase, because a redirection may have occurred + */ + val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString() + + /** + * The current homeserver host + */ + val homeServerHost = homeServerConnectionConfig.homeServerUri.host + + /** + * The default identity server url if any, returned by the homeserver during login phase + */ + val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt index d7a6954fd5..7c9ace5d82 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt @@ -39,7 +39,10 @@ data class MatrixError( // For M_LIMIT_EXCEEDED @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null, // For M_UNKNOWN_TOKEN - @Json(name = "soft_logout") val isSoftLogout: Boolean = false + @Json(name = "soft_logout") val isSoftLogout: Boolean = false, + // For M_INVALID_PEPPER + // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} + @Json(name = "lookup_pepper") val newLookupPepper: String? = null ) { companion object { @@ -129,6 +132,11 @@ data class MatrixError( /** (Not documented yet) */ const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" + const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" + + // For identity service + const val M_INVALID_PEPPER = "M_INVALID_PEPPER" + // Possible value for "limit_type" const val LIMIT_TYPE_MAU = "monthly_active_user" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 3ba275fd5f..1b9544bed6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.identity.IdentityService import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService @@ -39,6 +40,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.widgets.WidgetService @@ -55,6 +57,7 @@ interface Session : SignOutService, FilterService, FileService, + TermsService, ProfileService, PushRuleService, PushersService, @@ -79,7 +82,7 @@ interface Session : * Useful shortcut to get access to the userId */ val myUserId: String - get() = sessionParams.credentials.userId + get() = sessionParams.userId /** * The sessionId @@ -147,6 +150,11 @@ interface Session : */ fun cryptoService(): CryptoService + /** + * Returns the identity service associated with the session + */ + fun identityService(): IdentityService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt index c8526985e1..1c2b8de83b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt @@ -24,7 +24,15 @@ data class HomeServerCapabilities( /** * Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet */ - val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN + val maxUploadFileSize: Long = MAX_UPLOAD_FILE_SIZE_UNKNOWN, + /** + * Last version identity server and binding supported + */ + val lastVersionIdentityServerSupported: Boolean = false, + /** + * Default identity server url, provided in Wellknown + */ + val defaultIdentityServerUrl: String? = null ) { companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.kt new file mode 100644 index 0000000000..5817699636 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/FoundThreePid.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.matrix.android.api.session.identity + +data class FoundThreePid( + val threePid: ThreePid, + val matrixId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt new file mode 100644 index 0000000000..2f2821d7a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityService.kt @@ -0,0 +1,109 @@ +/* + * 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.matrix.android.api.session.identity + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * Provides access to the identity server configuration and services identity server can provide + */ +interface IdentityService { + /** + * Return the default identity server of the user, which may have been provided at login time by the homeserver, + * or by the Well-known setup of the homeserver + * It may be different from the current configured identity server + */ + fun getDefaultIdentityServer(): String? + + /** + * Return the current identity server URL used by this account. Returns null if no identity server is configured. + */ + fun getCurrentIdentityServerUrl(): String? + + /** + * Check if the identity server is valid + * See https://matrix.org/docs/spec/identity_service/latest#status-check + * RiotX SDK only supports identity server API v2 + */ + fun isValidIdentityServer(url: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * Update the identity server url. + * If successful, any previous identity server will be disconnected. + * In case of error, any previous identity server will remain configured. + * @param url the new url. + * @param callback will notify the user if change is successful. The String will be the final url of the identity server. + * The SDK can prepend "https://" for instance. + */ + fun setNewIdentityServer(url: String, callback: MatrixCallback<String>): Cancelable + + /** + * Disconnect (logout) from the current identity server + */ + fun disconnect(callback: MatrixCallback<Unit>): Cancelable + + /** + * This will ask the identity server to send an email or an SMS to let the user confirm he owns the ThreePid + */ + fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * This will cancel a pending binding of threePid. + */ + fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * This will ask the identity server to send an new email or a new SMS to let the user confirm he owns the ThreePid + */ + fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * Submit the code that the identity server has sent to the user (in email or SMS) + * Once successful, you will have to call [finalizeBindThreePid] + * @param code the code sent to the user + */ + fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable + + /** + * This will perform the actual association of ThreePid and Matrix account + */ + fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * Unbind a threePid + * The request will actually be done on the homeserver + */ + fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable + + /** + * Search MatrixId of users providing email and phone numbers + */ + fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable + + /** + * Get the status of the current user's threePid + * A lookup will be performed, but also pending binding state will be restored + * + * @param threePids the list of threePid the user owns (retrieved form the homeserver) + * @param callback onSuccess will be called with a map of ThreePid -> SharedState + */ + fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable + + fun addListener(listener: IdentityServiceListener) + fun removeListener(listener: IdentityServiceListener) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.kt new file mode 100644 index 0000000000..83fb949946 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceError.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.matrix.android.api.session.identity + +sealed class IdentityServiceError : Throwable() { + object OutdatedIdentityServer : IdentityServiceError() + object OutdatedHomeServer : IdentityServiceError() + object NoIdentityServerConfigured : IdentityServiceError() + object TermsNotSignedException : IdentityServiceError() + object BulkLookupSha256NotSupported : IdentityServiceError() + object BindingError : IdentityServiceError() + object NoCurrentBindingError : IdentityServiceError() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.kt new file mode 100644 index 0000000000..13f622fe77 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/IdentityServiceListener.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.matrix.android.api.session.identity + +interface IdentityServiceListener { + fun onIdentityServerChange() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/SharedState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/SharedState.kt new file mode 100644 index 0000000000..88cac776d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/SharedState.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.matrix.android.api.session.identity + +enum class SharedState { + SHARED, + NOT_SHARED, + BINDING_IN_PROGRESS +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt new file mode 100644 index 0000000000..2a453ca1a6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/identity/ThreePid.kt @@ -0,0 +1,40 @@ +/* + * 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.matrix.android.api.session.identity + +import com.google.i18n.phonenumbers.NumberParseException +import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.matrix.android.internal.session.profile.ThirdPartyIdentifier + +sealed class ThreePid(open val value: String) { + data class Email(val email: String) : ThreePid(email) + data class Msisdn(val msisdn: String) : ThreePid(msisdn) +} + +internal fun ThreePid.toMedium(): String { + return when (this) { + is ThreePid.Email -> ThirdPartyIdentifier.MEDIUM_EMAIL + is ThreePid.Msisdn -> ThirdPartyIdentifier.MEDIUM_MSISDN + } +} + +@Throws(NumberParseException::class) +internal fun ThreePid.Msisdn.getCountryCode(): String { + return with(PhoneNumberUtil.getInstance()) { + getRegionCodeForCountryCode(parse("+$msisdn", null).countryCode) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt index c1dc9a8afa..92f9359e34 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt @@ -17,7 +17,9 @@ package im.vector.matrix.android.api.session.profile +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional @@ -53,4 +55,15 @@ interface ProfileService { * */ fun getProfile(userId: String, matrixCallback: MatrixCallback<JsonDict>): Cancelable + + /** + * Get the current user 3Pids + */ + fun getThreePids(): List<ThreePid> + + /** + * Get the current user 3Pids Live + * @param refreshData set to true to fetch data from the homeserver + */ + fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt index 992cad41ca..154074b722 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineSettings.kt @@ -28,6 +28,10 @@ data class TimelineSettings( * A flag to filter edit events */ val filterEdits: Boolean = false, + /** + * A flag to filter redacted events + */ + val filterRedacted: Boolean = false, /** * A flag to filter by types. It should be used with [allowedTypes] field */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt new file mode 100644 index 0000000000..29c6a7a921 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/GetTermsResponse.kt @@ -0,0 +1,24 @@ +/* + * 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.matrix.android.api.session.terms + +import im.vector.matrix.android.internal.session.terms.TermsResponse + +data class GetTermsResponse( + val serverResponse: TermsResponse, + val alreadyAcceptedTermUrls: Set<String> +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt new file mode 100644 index 0000000000..36e6a411e3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/terms/TermsService.kt @@ -0,0 +1,37 @@ +/* + * 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.matrix.android.api.session.terms + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +interface TermsService { + enum class ServiceType { + IntegrationManager, + IdentityService + } + + fun getTerms(serviceType: ServiceType, + baseUrl: String, + callback: MatrixCallback<GetTermsResponse>): Cancelable + + fun agreeToTerms(serviceType: ServiceType, + baseUrl: String, + agreedUrls: List<String>, + token: String?, + callback: MatrixCallback<Unit>): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt index 753c9b609c..9f4f997b3b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/model/User.kt @@ -22,6 +22,9 @@ package im.vector.matrix.android.api.session.user.model */ data class User( val userId: String, + /** + * For usage in UI, consider using [getBestName] + */ val displayName: String? = null, val avatarUrl: String? = null ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 232cb3f541..68f404cb71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -25,16 +25,15 @@ import im.vector.matrix.android.internal.auth.db.AuthRealmMigration import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore -import im.vector.matrix.android.internal.auth.wellknown.DefaultDirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.DefaultGetWellknownTask -import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask +import im.vector.matrix.android.internal.auth.login.DefaultDirectLoginTask +import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase +import im.vector.matrix.android.internal.wellknown.WellknownModule import io.realm.RealmConfiguration import java.io.File -@Module +@Module(includes = [WellknownModule::class]) internal abstract class AuthModule { @Module @@ -74,9 +73,6 @@ internal abstract class AuthModule { @Binds abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator - @Binds - abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask - @Binds abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 997cf70e5a..b543fa7507 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult -import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk import im.vector.matrix.android.api.auth.data.isSupportedBySdk @@ -33,14 +32,14 @@ import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard +import im.vector.matrix.android.internal.auth.login.DirectLoginTask import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard -import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask -import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest @@ -50,7 +49,7 @@ import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.toCancelable -import kotlinx.coroutines.GlobalScope +import im.vector.matrix.android.internal.wellknown.GetWellknownTask import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -87,14 +86,21 @@ internal class DefaultAuthenticationService @Inject constructor( } } - override fun getSession(sessionParams: SessionParams): Session? { - return sessionManager.getOrCreateSession(sessionParams) + override fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable { + val homeServerConnectionConfig = sessionParamsStore.get(sessionId)?.homeServerConnectionConfig + + return if (homeServerConnectionConfig == null) { + callback.onFailure(IllegalStateException("Session not found")) + NoOpCancellable + } else { + getLoginFlow(homeServerConnectionConfig, callback) + } } override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable { pendingSessionData = null - return GlobalScope.launch(coroutineDispatchers.main) { + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() val result = runCatching { @@ -246,7 +252,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentRegistrationWizard = it } @@ -266,7 +273,8 @@ internal class DefaultAuthenticationService @Inject constructor( retrofitFactory, coroutineDispatchers, sessionCreator, - pendingSessionStore + pendingSessionStore, + taskExecutor.executorScope ).also { currentLoginWizard = it } @@ -283,7 +291,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = pendingSessionData?.homeServerConnectionConfig ?.let { PendingSessionData(it) } .also { - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { if (it == null) { // Should not happen pendingSessionStore.delete() @@ -300,7 +308,7 @@ internal class DefaultAuthenticationService @Inject constructor( pendingSessionData = null - GlobalScope.launch(coroutineDispatchers.main) { + taskExecutor.executorScope.launch(coroutineDispatchers.main) { pendingSessionStore.delete() } } @@ -308,7 +316,7 @@ internal class DefaultAuthenticationService @Inject constructor( override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback<Session>): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { createSessionFromSso(credentials, homeServerConnectionConfig) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index ebd50a6924..a4db0e84f7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -51,7 +51,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { } return SessionParamsEntity( sessionParams.credentials.sessionId(), - sessionParams.credentials.userId, + sessionParams.userId, credentialsJson, homeServerConnectionConfigJson, sessionParams.isTokenValid) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt index 4d98ddcf08..132073b340 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DefaultLoginWizard.kt @@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -47,7 +47,8 @@ internal class DefaultLoginWizard( retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope ) : LoginWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -59,7 +60,7 @@ internal class DefaultLoginWizard( password: String, deviceName: String, callback: MatrixCallback<Session>): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { loginInternal(login, password, deviceName) } } @@ -80,7 +81,7 @@ internal class DefaultLoginWizard( } override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { resetPasswordInternal(email, newPassword) } } @@ -108,7 +109,7 @@ internal class DefaultLoginWizard( callback.onFailure(IllegalStateException("developer error, no reset password in progress")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { resetPasswordMailConfirmedInternal(safeResetPasswordData) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt similarity index 97% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt index 01a3ab192d..90eddf2e14 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/login/DirectLoginTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.auth.login import dagger.Lazy import im.vector.matrix.android.api.auth.data.Credentials diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt index 29970b6c0c..5a39de72ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/DefaultRegistrationWizard.kt @@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import okhttp3.OkHttpClient @@ -45,7 +45,8 @@ internal class DefaultRegistrationWizard( private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val coroutineScope: CoroutineScope ) : RegistrationWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -72,7 +73,7 @@ internal class DefaultRegistrationWizard( override fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable { val params = RegistrationParams() - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } @@ -86,7 +87,7 @@ internal class DefaultRegistrationWizard( password = password, initialDeviceDisplayName = initialDeviceDisplayName ) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) .also { pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) @@ -101,7 +102,7 @@ internal class DefaultRegistrationWizard( return NoOpCancellable } val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } @@ -112,13 +113,13 @@ internal class DefaultRegistrationWizard( return NoOpCancellable } val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(params) } } override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { pendingSessionData = pendingSessionData.copy(currentThreePidData = null) .also { pendingSessionStore.savePendingSessionData(it) } @@ -131,7 +132,7 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { sendThreePid(safeCurrentThreePid) } } @@ -177,13 +178,13 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, no pending three pid")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { performRegistrationRequest(safeParam, delayMillis) } } override fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { validateThreePid(code) } } @@ -199,7 +200,7 @@ internal class DefaultRegistrationWizard( code = code ) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) - if (validationResponse.success == true) { + if (validationResponse.isSuccess()) { // The entered code is correct // Same than validate email return performRegistrationRequest(registrationParams, 3_000) @@ -214,7 +215,7 @@ internal class DefaultRegistrationWizard( callback.onFailure(IllegalStateException("developer error, call createAccount() method first")) return NoOpCancellable } - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return coroutineScope.launchToCallback(coroutineDispatchers.main, callback) { val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) performRegistrationRequest(params) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index 2cd52f702e..5bdc9579e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.androidsdk.rest.model.login +package im.vector.matrix.android.internal.auth.registration import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt index 8bfa3dda1d..1d19d1a5e5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/registration/SuccessResult.kt @@ -18,9 +18,12 @@ package im.vector.matrix.android.internal.auth.registration import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.extensions.orFalse @JsonClass(generateAdapter = true) data class SuccessResult( @Json(name = "success") val success: Boolean? -) +) { + fun isSuccess() = success.orFalse() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index e83895709e..19243f1a23 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -199,7 +199,7 @@ internal object MXEncryptedAttachments { .replace('_', '/') } - private fun base64ToBase64Url(base64: String): String { + internal fun base64ToBase64Url(base64: String): String { return base64.replace("\n".toRegex(), "") .replace("\\+".toRegex(), "-") .replace('/', '_') diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt index c3d2c30079..5e406fdc4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tools/Tools.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.tools import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkEncryption import org.matrix.olm.OlmPkSigning +import org.matrix.olm.OlmUtility fun <T> withOlmEncryption(block: (OlmPkEncryption) -> T): T { val olmPkEncryption = OlmPkEncryption() @@ -46,3 +47,12 @@ fun <T> withOlmSigning(block: (OlmPkSigning) -> T): T { olmPkSigning.releaseSigning() } } + +fun <T> withOlmUtility(block: (OlmUtility) -> T): T { + val olmUtility = OlmUtility() + try { + return block(olmUtility) + } finally { + olmUtility.releaseUtility() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 689829b8e3..1480029d6d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -121,7 +121,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( // } // // val requestMessage = KeyVerificationRequest( -// fromDevice = session.sessionParams.credentials.deviceId ?: "", +// fromDevice = session.sessionParams.deviceId ?: "", // methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), // timestamp = System.currentTimeMillis().toInt(), // transactionId = transactionId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 04a3560223..7479c55aa3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -81,9 +81,9 @@ import im.vector.matrix.android.internal.crypto.verification.qrcode.generateShar import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID @@ -104,7 +104,8 @@ internal class DefaultVerificationService @Inject constructor( private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory, private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory, private val crossSigningService: CrossSigningService, - private val cryptoCoroutineScope: CoroutineScope + private val cryptoCoroutineScope: CoroutineScope, + private val taskExecutor: TaskExecutor ) : DefaultVerificationTransaction.Listener, VerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -161,7 +162,7 @@ internal class DefaultVerificationService @Inject constructor( } fun onRoomEvent(event: Event) { - GlobalScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onRoomStartRequestReceived(event) @@ -301,7 +302,7 @@ internal class DefaultVerificationService @Inject constructor( // We don't want to block here val otherDeviceId = validRequestInfo.fromDevice - GlobalScope.launch { + cryptoCoroutineScope.launch { if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { Timber.e("## Verification device $otherDeviceId is not known") } @@ -340,7 +341,7 @@ internal class DefaultVerificationService @Inject constructor( } // We don't want to block here - GlobalScope.launch { + taskExecutor.executorScope.launch { if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) { Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index 77234e82f4..e0bbffc23b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -48,10 +48,11 @@ import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.StringProvider import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID @@ -66,7 +67,8 @@ internal class VerificationTransportRoomMessage( private val userDeviceId: String?, private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val tx: DefaultVerificationTransaction? + private val tx: DefaultVerificationTransaction?, + private val coroutineScope: CoroutineScope ) : VerificationTransport { override fun <T> sendToOther(type: String, @@ -131,7 +133,7 @@ internal class VerificationTransportRoomMessage( } // TODO listen to DB to get synced info - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { workLiveData.observeForever(observer) } } @@ -212,7 +214,7 @@ internal class VerificationTransportRoomMessage( } // TODO listen to DB to get synced info - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { workLiveData.observeForever(observer) } } @@ -265,7 +267,7 @@ internal class VerificationTransportRoomMessage( } // TODO listen to DB to get synced info - GlobalScope.launch(Dispatchers.Main) { + coroutineScope.launch(Dispatchers.Main) { workLiveData.observeForever(observer) } } @@ -384,9 +386,19 @@ internal class VerificationTransportRoomMessageFactory @Inject constructor( private val userId: String, @DeviceId private val deviceId: String?, - private val localEchoEventFactory: LocalEchoEventFactory) { + private val localEchoEventFactory: LocalEchoEventFactory, + private val taskExecutor: TaskExecutor +) { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { - return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, localEchoEventFactory, tx) + return VerificationTransportRoomMessage(workManagerProvider, + stringProvider, + sessionId, + userId, + deviceId, + roomId, + localEchoEventFactory, + tx, + taskExecutor.executorScope) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt index a0d3662e03..a66f587cec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -20,21 +20,16 @@ import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity /** - * HomeServerCapabilitiesEntity <-> HomeSeverCapabilities + * HomeServerCapabilitiesEntity -> HomeSeverCapabilities */ internal object HomeServerCapabilitiesMapper { fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities { return HomeServerCapabilities( canChangePassword = entity.canChangePassword, - maxUploadFileSize = entity.maxUploadFileSize - ) - } - - fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity { - return HomeServerCapabilitiesEntity( - canChangePassword = domain.canChangePassword, - maxUploadFileSize = domain.maxUploadFileSize + maxUploadFileSize = entity.maxUploadFileSize, + lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, + defaultIdentityServerUrl = entity.defaultIdentityServerUrl ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt index 5743597a61..a6b250b8fa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -22,6 +22,8 @@ import io.realm.RealmObject internal open class HomeServerCapabilitiesEntity( var canChangePassword: Boolean = true, var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, + var lastVersionIdentityServerSupported: Boolean = false, + var defaultIdentityServerUrl: String? = null, var lastUpdatedTimestamp: Long = 0L ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 47ae0199cf..03cdbfdf4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -37,6 +37,7 @@ import io.realm.annotations.RealmModule UserEntity::class, IgnoredUserEntity::class, BreadcrumbsEntity::class, + UserThreePidEntity::class, EventAnnotationsSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserThreePidEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserThreePidEntity.kt new file mode 100644 index 0000000000..f41ac1baa7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserThreePidEntity.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.matrix.android.internal.database.model + +import io.realm.RealmObject + +internal open class UserThreePidEntity( + var medium: String = "", + var address: String = "", + var validatedAt: Long = 0, + var addedAt: Long = 0 +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 5168d0728e..f798dbcf41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -62,8 +62,8 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) if (filterContentRelation) { liveEvents - ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) } val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { sendingTimelineEvents diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.kt new file mode 100644 index 0000000000..ea8122bc6d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventFilter.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.matrix.android.internal.database.query + +/** + * Query strings used to filter the timeline events regarding the Json raw string of the Event + */ +internal object TimelineEventFilter { + /** + * To apply to Event.content + */ + internal object Content { + internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" + internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" + } + + /** + * To apply to Event.unsigned + */ + internal object Unsigned { + internal const val REDACTED = """{*"redacted_because":*}""" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt index 8ee27b3375..0ceb94caa7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/AuthQualifiers.kt @@ -20,8 +20,12 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class Authenticated +internal annotation class Authenticated @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class Unauthenticated +internal annotation class AuthenticatedIdentity + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class Unauthenticated diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt index 3fdeb7eacc..4501ae5746 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/DbQualifiers.kt @@ -20,12 +20,16 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class AuthDatabase +internal annotation class AuthDatabase @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class SessionDatabase +internal annotation class SessionDatabase @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class CryptoDatabase +internal annotation class CryptoDatabase + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +internal annotation class IdentityDatabase diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt index aa39fc6fe8..5dfc04539a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -20,16 +20,16 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class SessionFilesDirectory +internal annotation class SessionFilesDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class SessionCacheDirectory +internal annotation class SessionCacheDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class CacheDirectory +internal annotation class CacheDirectory @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class ExternalFilesDirectory +internal annotation class ExternalFilesDirectory diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt index c802d4b63a..a15f660790 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt @@ -16,20 +16,16 @@ package im.vector.matrix.android.internal.network -import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.network.token.AccessTokenProvider import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject -internal class AccessTokenInterceptor @Inject constructor( - @SessionId private val sessionId: String, - private val sessionParamsStore: SessionParamsStore) : Interceptor { +internal class AccessTokenInterceptor(private val accessTokenProvider: AccessTokenProvider) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() - accessToken?.let { + accessTokenProvider.getToken()?.let { val newRequestBuilder = request.newBuilder() // Add the access token to all requests if it is set newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it") @@ -38,7 +34,4 @@ internal class AccessTokenInterceptor @Inject constructor( return chain.proceed(request) } - - private val accessToken - get() = sessionParamsStore.get(sessionId)?.credentials?.accessToken } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index ab6745148f..56e6ee0953 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -28,8 +28,9 @@ internal object NetworkConstants { const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" // Identity server - const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/" - const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/" + const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2" + const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/" - const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" + // TODO Ganfra, use correct value + const val URI_INTEGRATION_MANAGER_PATH = "TODO/" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt new file mode 100644 index 0000000000..8ffa0553e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/httpclient/OkHttpClientUtil.kt @@ -0,0 +1,39 @@ +/* + * 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.matrix.android.internal.network.httpclient + +import im.vector.matrix.android.internal.network.AccessTokenInterceptor +import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import okhttp3.OkHttpClient + +internal fun OkHttpClient.addAccessTokenInterceptor(accessTokenProvider: AccessTokenProvider): OkHttpClient { + return newBuilder() + .apply { + // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor + val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>() + interceptors().removeAll(existingCurlInterceptors) + + addInterceptor(AccessTokenInterceptor(accessTokenProvider)) + + // Re add eventually the curl logging interceptors + existingCurlInterceptors.forEach { + addInterceptor(it) + } + } + .build() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.kt new file mode 100644 index 0000000000..08176392fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/AccessTokenProvider.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.matrix.android.internal.network.token + +internal interface AccessTokenProvider { + fun getToken(): String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt new file mode 100644 index 0000000000..b570cb362e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/token/HomeserverAccessTokenProvider.kt @@ -0,0 +1,28 @@ +/* + * 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.matrix.android.internal.network.token + +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.SessionId +import javax.inject.Inject + +internal class HomeserverAccessTokenProvider @Inject constructor( + @SessionId private val sessionId: String, + private val sessionParamsStore: SessionParamsStore +) : AccessTokenProvider { + override fun getToken() = sessionParamsStore.get(sessionId)?.credentials?.accessToken +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index cfb0d23f2b..0cdd39f117 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -28,10 +28,10 @@ import im.vector.matrix.android.internal.di.ExternalFilesDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.extensions.foldToCallback +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable import im.vector.matrix.android.internal.util.writeToFile -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -51,7 +51,9 @@ internal class DefaultFileService @Inject constructor( private val contentUrlResolver: ContentUrlResolver, @Unauthenticated private val okHttpClient: OkHttpClient, - private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : FileService { /** * Download file in the cache folder, and eventually decrypt it @@ -63,7 +65,7 @@ internal class DefaultFileService @Inject constructor( url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback<File>): Cancelable { - return GlobalScope.launch(coroutineDispatchers.main) { + return taskExecutor.executorScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { val folder = File(sessionCacheDirectory, "MF") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index aa9df9c496..0052c1d7a2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState +import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.widgets.WidgetService import im.vector.matrix.android.internal.auth.SessionParamsStore @@ -51,14 +52,16 @@ import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.widgets.WidgetManager +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -85,6 +88,7 @@ internal class DefaultSession @Inject constructor( private val signOutService: Lazy<SignOutService>, private val pushRuleService: Lazy<PushRuleService>, private val pushersService: Lazy<PushersService>, + private val termsService: Lazy<TermsService>, private val cryptoService: Lazy<DefaultCryptoService>, private val fileService: Lazy<FileService>, private val secureStorageService: Lazy<SecureStorageService>, @@ -103,8 +107,11 @@ internal class DefaultSession @Inject constructor( private val timelineEventDecryptor: TimelineEventDecryptor, private val integrationManager: IntegrationManager, private val widgetManager: WidgetManager, - private val shieldTrustUpdater: ShieldTrustUpdater) - : Session, + private val shieldTrustUpdater: ShieldTrustUpdater, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val defaultIdentityService: DefaultIdentityService, + private val taskExecutor: TaskExecutor +) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), GroupService by groupService.get(), @@ -114,6 +121,7 @@ internal class DefaultSession @Inject constructor( PushRuleService by pushRuleService.get(), PushersService by pushersService.get(), FileService by fileService.get(), + TermsService by termsService.get(), InitialSyncProgressService by initialSyncProgressService.get(), SecureStorageService by secureStorageService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), @@ -142,6 +150,7 @@ internal class DefaultSession @Inject constructor( shieldTrustUpdater.start() integrationManager.start() widgetManager.start() + defaultIdentityService.start() } override fun requireBackgroundSync() { @@ -186,6 +195,10 @@ internal class DefaultSession @Inject constructor( shieldTrustUpdater.stop() integrationManager.stop() widgetManager.stop() + taskExecutor.executorScope.launch(coroutineDispatchers.main) { + // This has to be done on main thread + defaultIdentityService.stop() + } } override fun getSyncStateLive(): LiveData<SyncState> { @@ -215,7 +228,7 @@ internal class DefaultSession @Inject constructor( if (globalError is GlobalError.InvalidToken && globalError.softLogout) { // Mark the token has invalid - GlobalScope.launch(Dispatchers.IO) { + taskExecutor.executorScope.launch(Dispatchers.IO) { sessionParamsStore.setTokenInvalid(sessionId) } } @@ -229,6 +242,8 @@ internal class DefaultSession @Inject constructor( override fun cryptoService(): CryptoService = cryptoService.get() + override fun identityService() = defaultIdentityService + override fun addListener(listener: Session.Listener) { sessionListeners.addListener(listener) } @@ -239,6 +254,6 @@ internal class DefaultSession @Inject constructor( // For easy debugging override fun toString(): String { - return "$myUserId - ${sessionParams.credentials.deviceId}" + return "$myUserId - ${sessionParams.deviceId}" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index a99c2ba0aa..3fd8472edf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule +import im.vector.matrix.android.internal.session.identity.IdentityModule import im.vector.matrix.android.internal.session.openid.OpenIdModule import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker @@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.sync.SyncModule import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.job.SyncWorker +import im.vector.matrix.android.internal.session.terms.TermsModule import im.vector.matrix.android.internal.session.user.UserModule import im.vector.matrix.android.internal.session.user.accountdata.AccountDataModule import im.vector.matrix.android.internal.session.widgets.WidgetModule @@ -74,6 +76,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers PushersModule::class, OpenIdModule::class, WidgetModule::class, + IdentityModule::class, + TermsModule::class, AccountDataModule::class, ProfileModule::class, SessionAssistedInjectModule::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 8bdfff062f..9d5772b82a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -50,14 +50,15 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger -import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkCallbackStrategy import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy import im.vector.matrix.android.internal.network.RetrofitFactory -import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor +import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater @@ -175,21 +176,8 @@ internal abstract class SessionModule { @SessionScope @Authenticated fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, - accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient { - return okHttpClient.newBuilder() - .apply { - // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor - val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>() - interceptors().removeAll(existingCurlInterceptors) - - addInterceptor(accessTokenInterceptor) - - // Re add eventually the curl logging interceptors - existingCurlInterceptors.forEach { - addInterceptor(it) - } - } - .build() + @Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) } @JvmStatic @@ -233,6 +221,10 @@ internal abstract class SessionModule { } } + @Binds + @Authenticated + abstract fun bindAccessTokenProvider(provider: HomeserverAccessTokenProvider): AccessTokenProvider + @Binds abstract fun bindSession(session: DefaultSession): Session diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt index f5b105cfee..b993534ba1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt @@ -19,8 +19,10 @@ package im.vector.matrix.android.internal.session.account import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.cleanup.CleanupSession +import im.vector.matrix.android.internal.session.identity.IdentityDisconnectTask import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus +import timber.log.Timber import javax.inject.Inject internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> { @@ -34,6 +36,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor( private val accountAPI: AccountAPI, private val eventBus: EventBus, @UserId private val userId: String, + private val identityDisconnectTask: IdentityDisconnectTask, private val cleanupSession: CleanupSession ) : DeactivateAccountTask { @@ -44,6 +47,10 @@ internal class DefaultDeactivateAccountTask @Inject constructor( apiCall = accountAPI.deactivate(deactivateAccountParams) } + // Logout from identity server if any, ignoring errors + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + cleanupSession.handle() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt index f37bbfe798..880a8fbc31 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.homeserver +import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.GET @@ -38,5 +39,11 @@ internal interface CapabilitiesAPI { * Request the versions */ @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") - fun getVersions(): Call<Unit> + fun getVersions(): Call<Versions> + + /** + * Ping the homeserver. We do not care about the returned data, so there is no use to parse them + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions") + fun ping(): Call<Unit> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 9f068381f0..be5b0d3949 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -17,9 +17,14 @@ package im.vector.matrix.android.internal.session.homeserver import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Versions +import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities +import im.vector.matrix.android.internal.wellknown.GetWellknownTask import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.awaitTransaction @@ -32,7 +37,10 @@ internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit> internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val capabilitiesAPI: CapabilitiesAPI, private val monarchy: Monarchy, - private val eventBus: EventBus + private val eventBus: EventBus, + private val getWellknownTask: GetWellknownTask, + @UserId + private val userId: String ) : GetHomeServerCapabilitiesTask { override suspend fun execute(params: Unit) { @@ -47,29 +55,54 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( return } - val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult>(eventBus) { - apiCall = capabilitiesAPI.getUploadCapabilities() - } - val capabilities = runCatching { executeRequest<GetCapabilitiesResult>(eventBus) { apiCall = capabilitiesAPI.getCapabilities() } }.getOrNull() - // TODO Add other call here (get version, etc.) + val uploadCapabilities = runCatching { + executeRequest<GetUploadCapabilitiesResult>(eventBus) { + apiCall = capabilitiesAPI.getUploadCapabilities() + } + }.getOrNull() - insertInDb(capabilities, uploadCapabilities) + val versions = runCatching { + executeRequest<Versions>(null) { + apiCall = capabilitiesAPI.getVersions() + } + }.getOrNull() + + val wellknownResult = runCatching { + getWellknownTask.execute(GetWellknownTask.Params(userId)) + }.getOrNull() + + insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) } - private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { + private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, + getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getVersionResult: Versions?, + getWellknownResult: WellknownResult?) { monarchy.awaitTransaction { realm -> val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) - homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + if (getCapabilitiesResult != null) { + homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() + } - homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize - ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + if (getUploadCapabilitiesResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + } + + if (getVersionResult != null) { + homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() + } + + if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { + homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl + } homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt index ffaa998789..9a625571a8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt @@ -53,6 +53,6 @@ internal data class ChangePassword( ) // The spec says: If not present, the client should assume that password changes are possible via the API -internal fun GetCapabilitiesResult?.canChangePassword(): Boolean { - return this?.capabilities?.changePassword?.enabled.orTrue() +internal fun GetCapabilitiesResult.canChangePassword(): Boolean { + return capabilities?.changePassword?.enabled.orTrue() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt index f2b249ab0c..91ec3fe305 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -20,9 +20,10 @@ import dagger.Binds import dagger.Module import dagger.Provides import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.wellknown.WellknownModule import retrofit2.Retrofit -@Module +@Module(includes = [WellknownModule::class]) internal abstract class HomeServerCapabilitiesModule { @Module diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt index e220c0064d..705deb4e57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerPinger.kt @@ -35,7 +35,7 @@ internal class HomeServerPinger @Inject constructor(private val taskExecutor: Ta suspend fun canReachHomeServer(): Boolean { return try { executeRequest<Unit>(null) { - apiCall = capabilitiesAPI.getVersions() + apiCall = capabilitiesAPI.ping() } true } catch (throwable: Throwable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt new file mode 100644 index 0000000000..1a271e659e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/DefaultIdentityService.kt @@ -0,0 +1,341 @@ +/* + * 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.matrix.android.internal.session.identity + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityService +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.IdentityServiceListener +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource +import im.vector.matrix.android.internal.session.identity.todelete.observeNotNull +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.profile.BindThreePidsTask +import im.vector.matrix.android.internal.session.profile.UnbindThreePidsTask +import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.ensureProtocol +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +@SessionScope +internal class DefaultIdentityService @Inject constructor( + private val identityStore: IdentityStore, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityBulkLookupTask: IdentityBulkLookupTask, + private val identityRegisterTask: IdentityRegisterTask, + private val identityPingTask: IdentityPingTask, + private val identityDisconnectTask: IdentityDisconnectTask, + private val identityRequestTokenForBindingTask: IdentityRequestTokenForBindingTask, + @Unauthenticated + private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + @AuthenticatedIdentity + private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val bindThreePidsTask: BindThreePidsTask, + private val submitTokenForBindingTask: IdentitySubmitTokenForBindingTask, + private val unbindThreePidsTask: UnbindThreePidsTask, + private val identityApiProvider: IdentityApiProvider, + private val accountDataDataSource: AccountDataDataSource, + private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val sessionParams: SessionParams, + private val taskExecutor: TaskExecutor +) : IdentityService { + + private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry } + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + private val listeners = mutableSetOf<IdentityServiceListener>() + + fun start() { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + // Observe the account data change + accountDataDataSource + .getLiveAccountDataEvent(UserAccountData.TYPE_IDENTITY_SERVER) + .observeNotNull(lifecycleOwner) { + notifyIdentityServerUrlChange(it.getOrNull()?.content?.toModel<IdentityServerContent>()?.baseUrl) + } + + // Init identityApi + updateIdentityAPI(identityStore.getIdentityData()?.identityServerUrl) + } + + private fun notifyIdentityServerUrlChange(baseUrl: String?) { + // This is maybe not a real change (echo of account data we are just setting) + if (identityStore.getIdentityData()?.identityServerUrl == baseUrl) { + Timber.d("Echo of local identity server url change, or no change") + } else { + // Url has changed, we have to reset our store, update internal configuration and notify listeners + identityStore.setUrl(baseUrl) + updateIdentityAPI(baseUrl) + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + } + + fun stop() { + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + } + + /** + * First return the identity server provided during login phase. + * If null, provide the one in wellknown configuration of the homeserver + * Else return null + */ + override fun getDefaultIdentityServer(): String? { + return sessionParams.defaultIdentityServerUrl + ?.takeIf { it.isNotEmpty() } + ?: homeServerCapabilitiesService.getHomeServerCapabilities().defaultIdentityServerUrl + } + + override fun getCurrentIdentityServerUrl(): String? { + return identityStore.getIdentityData()?.identityServerUrl + } + + override fun startBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, false)) + } + } + + override fun cancelBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityStore.deletePendingBinding(threePid) + } + } + + override fun sendAgainValidationCode(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityRequestTokenForBindingTask.execute(IdentityRequestTokenForBindingTask.Params(threePid, true)) + } + } + + override fun finalizeBindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + bindThreePidsTask.execute(BindThreePidsTask.Params(threePid)) + } + } + + override fun submitValidationToken(threePid: ThreePid, code: String, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + submitTokenForBindingTask.execute(IdentitySubmitTokenForBindingTask.Params(threePid, code)) + } + } + + override fun unbindThreePid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable { + if (homeServerCapabilitiesService.getHomeServerCapabilities().lastVersionIdentityServerSupported.not()) { + callback.onFailure(IdentityServiceError.OutdatedHomeServer) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + unbindThreePidsTask.execute(UnbindThreePidsTask.Params(threePid)) + } + } + + override fun isValidIdentityServer(url: String, callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + identityPingTask.execute(IdentityPingTask.Params(api)) + } + } + + override fun disconnect(callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + identityDisconnectTask.execute(Unit) + + identityStore.setUrl(null) + updateIdentityAPI(null) + updateAccountData(null) + } + } + + override fun setNewIdentityServer(url: String, callback: MatrixCallback<String>): Cancelable { + val urlCandidate = url.ensureProtocol() + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val current = getCurrentIdentityServerUrl() + if (urlCandidate == current) { + // Nothing to do + Timber.d("Same URL, nothing to do") + } else { + // Disconnect previous one if any, first, because the token will change. + // In case of error when configuring the new identity server, this is not a big deal, + // we will ask for a new token on the previous Identity server + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + // Try to get a token + val token = getNewIdentityServerToken(urlCandidate) + + identityStore.setUrl(urlCandidate) + identityStore.setToken(token) + updateIdentityAPI(urlCandidate) + + updateAccountData(urlCandidate) + } + urlCandidate + } + } + + private suspend fun updateAccountData(url: String?) { + // Also notify the listener + withContext(coroutineDispatchers.main) { + listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + } + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( + identityContent = IdentityServerContent(baseUrl = url) + )) + } + + override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyList()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + lookUpInternal(true, threePids) + } + } + + override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable { + if (threePids.isEmpty()) { + callback.onSuccess(emptyMap()) + return NoOpCancellable + } + + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val lookupResult = lookUpInternal(true, threePids) + + threePids.associateWith { threePid -> + // If not in lookup result, check if there is a pending binding + if (lookupResult.firstOrNull { it.threePid == threePid } == null) { + if (identityStore.getPendingBinding(threePid) == null) { + SharedState.NOT_SHARED + } else { + SharedState.BINDING_IN_PROGRESS + } + } else { + SharedState.SHARED + } + } + } + } + + private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> { + ensureToken() + + return try { + identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids)) + } catch (throwable: Throwable) { + // Refresh token? + when { + throwable.isInvalidToken() && canRetry -> { + identityStore.setToken(null) + lookUpInternal(false, threePids) + } + throwable.isTermsNotSigned() -> throw IdentityServiceError.TermsNotSignedException + else -> throw throwable + } + } + } + + private suspend fun ensureToken() { + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured + + if (identityData.token == null) { + // Try to get a token + val token = getNewIdentityServerToken(url) + identityStore.setToken(token) + } + } + + private suspend fun getNewIdentityServerToken(url: String): String { + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + override fun addListener(listener: IdentityServiceListener) { + listeners.add(listener) + } + + override fun removeListener(listener: IdentityServiceListener) { + listeners.remove(listener) + } + + private fun updateIdentityAPI(url: String?) { + identityApiProvider.identityApi = url + ?.let { retrofitFactory.create(okHttpClient, it) } + ?.create(IdentityAPI::class.java) + } +} + +private fun Throwable.isInvalidToken(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ +} + +private fun Throwable.isTermsNotSigned(): Boolean { + return this is Failure.ServerError + && httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */ + && error.code == MatrixError.M_TERMS_NOT_SIGNED +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt new file mode 100644 index 0000000000..ca361b4265 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAPI.kt @@ -0,0 +1,98 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpParams +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which need an identity server token + */ +internal interface IdentityAPI { + /** + * Gets information about what user owns the access token used in the request. + * Will return a 403 for when terms are not signed + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-account + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "account") + fun getAccount(): Call<IdentityAccountResponse> + + /** + * Logs out the access token, preventing it from being used to authenticate future requests to the server. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/logout") + fun logout(): Call<Unit> + + /** + * Request the hash detail to request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2-hash-details + */ + @GET(NetworkConstants.URI_IDENTITY_PATH_V2 + "hash_details") + fun hashDetails(): Call<IdentityHashDetailResponse> + + /** + * Request a bunch of 3PIDs + * Ref: https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-lookup + * + * @param body the body request + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "lookup") + fun lookup(@Body body: IdentityLookUpParams): Call<IdentityLookUpResponse> + + /** + * Create a session to change the bind status of an email to an identity server + * The identity server will also send an email + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/email/requestToken") + fun requestTokenToBindEmail(@Body body: IdentityRequestTokenForEmailBody): Call<IdentityRequestTokenResponse> + + /** + * Create a session to change the bind status of an phone number to an identity server + * The identity server will also send an SMS on the ThreePid provided + * + * @param body + * @return the sid + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/msisdn/requestToken") + fun requestTokenToBindMsisdn(@Body body: IdentityRequestTokenForMsisdnBody): Call<IdentityRequestTokenResponse> + + /** + * Validate ownership of an email address, or a phone number. + * Ref: + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-msisdn-submittoken + * - https://matrix.org/docs/spec/identity_service/latest#post-matrix-identity-v2-validate-email-submittoken + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken") + fun submitToken(@Path("medium") medium: String, @Body body: IdentityRequestOwnershipParams): Call<SuccessResult> +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.kt new file mode 100644 index 0000000000..ee2f18c767 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAccessTokenProvider.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.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import javax.inject.Inject + +internal class IdentityAccessTokenProvider @Inject constructor( + private val identityStore: IdentityStore +) : AccessTokenProvider { + override fun getToken() = identityStore.getIdentityData()?.token +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt new file mode 100644 index 0000000000..3262a56398 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityApiProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class IdentityApiProvider @Inject constructor() { + + var identityApi: IdentityAPI? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.kt new file mode 100644 index 0000000000..04abf5fe6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityAuthAPI.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.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +/** + * Ref: https://matrix.org/docs/spec/identity_service/latest + * This contain the requests which do not need an identity server token + */ +internal interface IdentityAuthAPI { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server exists and is alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * https://matrix.org/docs/spec/identity_service/latest#get-matrix-identity-v2 + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_IDENTITY_PREFIX_PATH) + fun ping(): Call<Unit> + + /** + * Ping v1 will be used to check outdated Identity server + */ + @GET("_matrix/identity/api/v1") + fun pingV1(): Call<Unit> + + /** + * Exchanges an OpenID token from the homeserver for an access token to access the identity server. + * The request body is the same as the values returned by /openid/request_token in the Client-Server API. + */ + @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register") + fun register(@Body openIdToken: RequestOpenIdTokenResponse): Call<IdentityRegisterResponse> +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt new file mode 100644 index 0000000000..9f1579af60 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityBulkLookupTask.kt @@ -0,0 +1,133 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.identity.FoundThreePid +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url +import im.vector.matrix.android.internal.crypto.tools.withOlmUtility +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpParams +import im.vector.matrix.android.internal.session.identity.model.IdentityLookUpResponse +import im.vector.matrix.android.internal.task.Task +import java.util.Locale +import javax.inject.Inject + +internal interface IdentityBulkLookupTask : Task<IdentityBulkLookupTask.Params, List<FoundThreePid>> { + data class Params( + val threePids: List<ThreePid> + ) +} + +internal class DefaultIdentityBulkLookupTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityBulkLookupTask { + + override suspend fun execute(params: IdentityBulkLookupTask.Params): List<FoundThreePid> { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured + val pepper = identityData.hashLookupPepper + val hashDetailResponse = if (pepper == null) { + // We need to fetch the hash details first + fetchAndStoreHashDetails(identityAPI) + } else { + IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) + } + + if (hashDetailResponse.algorithms.contains("sha256").not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache could be outdated, the identity server maybe now supports sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + val hashedAddresses = withOlmUtility { olmUtility -> + params.threePids.map { threePid -> + base64ToBase64Url( + olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT) + + " " + threePid.toMedium() + " " + hashDetailResponse.pepper) + ) + } + } + + val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true) + + // Convert back to List<FoundThreePid> + return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + } + + private suspend fun lookUpInternal(identityAPI: IdentityAPI, + hashedAddresses: List<String>, + hashDetailResponse: IdentityHashDetailResponse, + canRetry: Boolean): IdentityLookUpResponse { + return try { + executeRequest(null) { + apiCall = identityAPI.lookup(IdentityLookUpParams( + hashedAddresses, + IdentityHashDetailResponse.ALGORITHM_SHA256, + hashDetailResponse.pepper + )) + } + } catch (failure: Throwable) { + // Catch invalid hash pepper and retry + if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { + // This is not documented, by the error can contain the new pepper! + if (!failure.error.newLookupPepper.isNullOrEmpty()) { + // Store it and use it right now + hashDetailResponse.copy(pepper = failure.error.newLookupPepper) + .also { identityStore.setHashDetails(it) } + .let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) } + } else { + // Retrieve the new hash details + val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) + + if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { + // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it + // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256 + throw IdentityServiceError.BulkLookupSha256NotSupported + } + + lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */) + } + } else { + // Other error + throw failure + } + } + } + + private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { + return executeRequest<IdentityHashDetailResponse>(null) { + apiCall = identityAPI.hashDetails() + } + .also { identityStore.setHashDetails(it) } + } + + private fun handleSuccess(threePids: List<ThreePid>, hashedAddresses: List<String>, identityLookUpResponse: IdentityLookUpResponse): List<FoundThreePid> { + return identityLookUpResponse.mappings.keys.map { hashedAddress -> + FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.kt new file mode 100644 index 0000000000..abed3962bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityDisconnectTask.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.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface IdentityDisconnectTask : Task<Unit, Unit> + +internal class DefaultIdentityDisconnectTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider +) : IdentityDisconnectTask { + + override suspend fun execute(params: Unit) { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Ensure we have a token. + // We can have an identity server configured, but no token yet. + if (accessTokenProvider.getToken() == null) { + Timber.d("No token to disconnect identity server.") + return + } + + executeRequest<Unit>(null) { + apiCall = identityAPI.logout() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt new file mode 100644 index 0000000000..7a5790788b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityModule.kt @@ -0,0 +1,95 @@ +/* + * 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.matrix.android.internal.session.identity + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.internal.database.RealmKeysUtils +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.di.SessionFilesDirectory +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.SessionModule +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.db.IdentityRealmModule +import im.vector.matrix.android.internal.session.identity.db.RealmIdentityStore +import io.realm.RealmConfiguration +import okhttp3.OkHttpClient +import java.io.File + +@Module +internal abstract class IdentityModule { + + @Module + companion object { + @JvmStatic + @Provides + @SessionScope + @AuthenticatedIdentity + fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient, + @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider): OkHttpClient { + return okHttpClient.addAccessTokenInterceptor(accessTokenProvider) + } + + @JvmStatic + @Provides + @IdentityDatabase + @SessionScope + fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils, + @SessionFilesDirectory directory: File, + @UserMd5 userMd5: String): RealmConfiguration { + return RealmConfiguration.Builder() + .directory(directory) + .name("matrix-sdk-identity.realm") + .apply { + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) + } + .modules(IdentityRealmModule()) + .build() + } + } + + @Binds + @AuthenticatedIdentity + abstract fun bindAccessTokenProvider(provider: IdentityAccessTokenProvider): AccessTokenProvider + + @Binds + abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore + + @Binds + abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask + + @Binds + abstract fun bindIdentityRegisterTask(task: DefaultIdentityRegisterTask): IdentityRegisterTask + + @Binds + abstract fun bindIdentityRequestTokenForBindingTask(task: DefaultIdentityRequestTokenForBindingTask): IdentityRequestTokenForBindingTask + + @Binds + abstract fun bindIdentitySubmitTokenForBindingTask(task: DefaultIdentitySubmitTokenForBindingTask): IdentitySubmitTokenForBindingTask + + @Binds + abstract fun bindIdentityBulkLookupTask(task: DefaultIdentityBulkLookupTask): IdentityBulkLookupTask + + @Binds + abstract fun bindIdentityDisconnectTask(task: DefaultIdentityDisconnectTask): IdentityDisconnectTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt new file mode 100644 index 0000000000..50a36097a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityPingTask.kt @@ -0,0 +1,52 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface IdentityPingTask : Task<IdentityPingTask.Params, Unit> { + data class Params( + val identityAuthAPI: IdentityAuthAPI + ) +} + +internal class DefaultIdentityPingTask @Inject constructor() : IdentityPingTask { + + override suspend fun execute(params: IdentityPingTask.Params) { + try { + executeRequest<Unit>(null) { + apiCall = params.identityAuthAPI.ping() + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Check if API v1 is available + executeRequest<Unit>(null) { + apiCall = params.identityAuthAPI.pingV1() + } + // API V1 is responding, but not V2 -> Outdated + throw IdentityServiceError.OutdatedIdentityServer + } else { + throw throwable + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt new file mode 100644 index 0000000000..c72e364ef8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRegisterTask.kt @@ -0,0 +1,39 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityRegisterResponse +import im.vector.matrix.android.internal.session.openid.RequestOpenIdTokenResponse +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentityRegisterTask : Task<IdentityRegisterTask.Params, IdentityRegisterResponse> { + data class Params( + val identityAuthAPI: IdentityAuthAPI, + val openIdTokenResponse: RequestOpenIdTokenResponse + ) +} + +internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegisterTask { + + override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse { + return executeRequest(null) { + apiCall = params.identityAuthAPI.register(params.openIdTokenResponse) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt new file mode 100644 index 0000000000..313f5f6662 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityRequestTokenForBindingTask.kt @@ -0,0 +1,87 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.getCountryCode +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForEmailBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenForMsisdnBody +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestTokenResponse +import im.vector.matrix.android.internal.task.Task +import java.util.UUID +import javax.inject.Inject + +internal interface IdentityRequestTokenForBindingTask : Task<IdentityRequestTokenForBindingTask.Params, Unit> { + data class Params( + val threePid: ThreePid, + // True to request the identity server to send again the email or the SMS + val sendAgain: Boolean + ) +} + +internal class DefaultIdentityRequestTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentityRequestTokenForBindingTask { + + override suspend fun execute(params: IdentityRequestTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) + + if (params.sendAgain && identityPendingBinding == null) { + throw IdentityServiceError.NoCurrentBindingError + } + + val clientSecret = identityPendingBinding?.clientSecret ?: UUID.randomUUID().toString() + val sendAttempt = identityPendingBinding?.sendAttempt?.inc() ?: 1 + + val tokenResponse = executeRequest<IdentityRequestTokenResponse>(null) { + apiCall = when (params.threePid) { + is ThreePid.Email -> identityAPI.requestTokenToBindEmail(IdentityRequestTokenForEmailBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + email = params.threePid.email + )) + is ThreePid.Msisdn -> { + identityAPI.requestTokenToBindMsisdn(IdentityRequestTokenForMsisdnBody( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + phoneNumber = params.threePid.msisdn, + countryCode = params.threePid.getCountryCode() + )) + } + } + } + + // Store client secret, send attempt and sid + identityStore.storePendingBinding( + params.threePid, + IdentityPendingBinding( + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = tokenResponse.sid + ) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.kt new file mode 100644 index 0000000000..fae1dd1eba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentitySubmitTokenForBindingTask.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.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.auth.registration.SuccessResult +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityRequestOwnershipParams +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface IdentitySubmitTokenForBindingTask : Task<IdentitySubmitTokenForBindingTask.Params, Unit> { + data class Params( + val threePid: ThreePid, + val token: String + ) +} + +internal class DefaultIdentitySubmitTokenForBindingTask @Inject constructor( + private val identityApiProvider: IdentityApiProvider, + private val identityStore: IdentityStore, + @UserId private val userId: String +) : IdentitySubmitTokenForBindingTask { + + override suspend fun execute(params: IdentitySubmitTokenForBindingTask.Params) { + val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + val tokenResponse = executeRequest<SuccessResult>(null) { + apiCall = identityAPI.submitToken( + params.threePid.toMedium(), + IdentityRequestOwnershipParams( + clientSecret = identityPendingBinding.clientSecret, + sid = identityPendingBinding.sid, + token = params.token + )) + } + + if (!tokenResponse.isSuccess()) { + throw IdentityServiceError.BindingError + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt new file mode 100644 index 0000000000..bd97a0af2b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/IdentityTaskHelper.kt @@ -0,0 +1,34 @@ +/* + * 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.matrix.android.internal.session.identity + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.model.IdentityAccountResponse + +internal suspend fun getIdentityApiAndEnsureTerms(identityApiProvider: IdentityApiProvider, userId: String): IdentityAPI { + val identityAPI = identityApiProvider.identityApi ?: throw IdentityServiceError.NoIdentityServerConfigured + + // Always check that we have access to the service (regarding terms) + val identityAccountResponse = executeRequest<IdentityAccountResponse>(null) { + apiCall = identityAPI.getAccount() + } + + assert(userId == identityAccountResponse.userId) + + return identityAPI +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt new file mode 100644 index 0000000000..f1e57e1ed5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityData.kt @@ -0,0 +1,24 @@ +/* + * 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.matrix.android.internal.session.identity.data + +internal data class IdentityData( + val identityServerUrl: String?, + val token: String?, + val hashLookupPepper: String?, + val hashLookupAlgorithm: List<String> +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt new file mode 100644 index 0000000000..b7f405cb0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityPendingBinding.kt @@ -0,0 +1,26 @@ +/* + * 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.matrix.android.internal.session.identity.data + +internal data class IdentityPendingBinding( + /* Managed by Riot */ + val clientSecret: String, + /* Managed by Riot */ + val sendAttempt: Int, + /* Provided by the identity server */ + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt new file mode 100644 index 0000000000..d5cd3277ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/data/IdentityStore.kt @@ -0,0 +1,44 @@ +/* + * 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.matrix.android.internal.session.identity.data + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse + +internal interface IdentityStore { + + fun getIdentityData(): IdentityData? + + fun setUrl(url: String?) + + fun setToken(token: String?) + + fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) + + /** + * Store details about a current binding + */ + fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) + + fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? + + fun deletePendingBinding(threePid: ThreePid) +} + +internal fun IdentityStore.getIdentityServerUrlWithoutProtocol(): String? { + return getIdentityData()?.identityServerUrl?.substringAfter("://") +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.kt new file mode 100644 index 0000000000..76e480bdc9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntity.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.matrix.android.internal.session.identity.db + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class IdentityDataEntity( + var identityServerUrl: String? = null, + var token: String? = null, + var hashLookupPepper: String? = null, + var hashLookupAlgorithm: RealmList<String> = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt new file mode 100644 index 0000000000..0a07359642 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityDataEntityQuery.kt @@ -0,0 +1,62 @@ +/* + * 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.matrix.android.internal.session.identity.db + +import io.realm.Realm +import io.realm.RealmList +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +/** + * Only one object can be stored at a time + */ +internal fun IdentityDataEntity.Companion.get(realm: Realm): IdentityDataEntity? { + return realm.where<IdentityDataEntity>().findFirst() +} + +private fun IdentityDataEntity.Companion.getOrCreate(realm: Realm): IdentityDataEntity { + return get(realm) ?: realm.createObject() +} + +internal fun IdentityDataEntity.Companion.setUrl(realm: Realm, + url: String?) { + realm.where<IdentityDataEntity>().findAll().deleteAllFromRealm() + // Delete all pending binding if any + IdentityPendingBindingEntity.deleteAll(realm) + + if (url != null) { + getOrCreate(realm).apply { + identityServerUrl = url + } + } +} + +internal fun IdentityDataEntity.Companion.setToken(realm: Realm, + newToken: String?) { + get(realm)?.apply { + token = newToken + } +} + +internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, + pepper: String, + algorithms: List<String>) { + get(realm)?.apply { + hashLookupPepper = pepper + hashLookupAlgorithm = RealmList<String>().apply { addAll(algorithms) } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt new file mode 100644 index 0000000000..1335e38565 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityMapper.kt @@ -0,0 +1,40 @@ +/* + * 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.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.internal.session.identity.data.IdentityData +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding + +internal object IdentityMapper { + + fun map(entity: IdentityDataEntity): IdentityData { + return IdentityData( + identityServerUrl = entity.identityServerUrl, + token = entity.token, + hashLookupPepper = entity.hashLookupPepper, + hashLookupAlgorithm = entity.hashLookupAlgorithm.toList() + ) + } + + fun map(entity: IdentityPendingBindingEntity): IdentityPendingBinding { + return IdentityPendingBinding( + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + sid = entity.sid + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt new file mode 100644 index 0000000000..51b195a7fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntity.kt @@ -0,0 +1,37 @@ +/* + * 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.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class IdentityPendingBindingEntity( + @PrimaryKey var threePid: String = "", + /* Managed by Riot */ + var clientSecret: String = "", + /* Managed by Riot */ + var sendAttempt: Int = 0, + /* Provided by the identity server */ + var sid: String = "" +) : RealmObject() { + + companion object { + fun ThreePid.toPrimaryKey() = "${toMedium()}_$value" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.kt new file mode 100644 index 0000000000..e358be6bbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityPendingBindingEntityQuery.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.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun IdentityPendingBindingEntity.Companion.get(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity? { + return realm.where<IdentityPendingBindingEntity>() + .equalTo(IdentityPendingBindingEntityFields.THREE_PID, threePid.toPrimaryKey()) + .findFirst() +} + +internal fun IdentityPendingBindingEntity.Companion.getOrCreate(realm: Realm, threePid: ThreePid): IdentityPendingBindingEntity { + return get(realm, threePid) ?: realm.createObject(threePid.toPrimaryKey()) +} + +internal fun IdentityPendingBindingEntity.Companion.delete(realm: Realm, threePid: ThreePid) { + get(realm, threePid)?.deleteFromRealm() +} + +internal fun IdentityPendingBindingEntity.Companion.deleteAll(realm: Realm) { + realm.where<IdentityPendingBindingEntity>() + .findAll() + .deleteAllFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt new file mode 100644 index 0000000000..19bd90ee1f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/IdentityRealmModule.kt @@ -0,0 +1,29 @@ +/* + * 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.matrix.android.internal.session.identity.db + +import io.realm.annotations.RealmModule + +/** + * Realm module for identity server classes + */ +@RealmModule(library = true, + classes = [ + IdentityDataEntity::class, + IdentityPendingBindingEntity::class + ]) +internal class IdentityRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt new file mode 100644 index 0000000000..c294fbbf4b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/db/RealmIdentityStore.kt @@ -0,0 +1,91 @@ +/* + * 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.matrix.android.internal.session.identity.db + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.di.IdentityDatabase +import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.identity.data.IdentityPendingBinding +import im.vector.matrix.android.internal.session.identity.data.IdentityData +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.model.IdentityHashDetailResponse +import io.realm.Realm +import io.realm.RealmConfiguration +import javax.inject.Inject + +@SessionScope +internal class RealmIdentityStore @Inject constructor( + @IdentityDatabase + private val realmConfiguration: RealmConfiguration +) : IdentityStore { + + override fun getIdentityData(): IdentityData? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityDataEntity.get(realm)?.let { IdentityMapper.map(it) } + } + } + + override fun setUrl(url: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setUrl(realm, url) + } + } + } + + override fun setToken(token: String?) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setToken(realm, token) + } + } + } + + override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityDataEntity.setHashDetails(realm, hashDetailResponse.pepper, hashDetailResponse.algorithms) + } + } + } + + override fun storePendingBinding(threePid: ThreePid, data: IdentityPendingBinding) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.getOrCreate(realm, threePid).let { entity -> + entity.clientSecret = data.clientSecret + entity.sendAttempt = data.sendAttempt + entity.sid = data.sid + } + } + } + } + + override fun getPendingBinding(threePid: ThreePid): IdentityPendingBinding? { + return Realm.getInstance(realmConfiguration).use { realm -> + IdentityPendingBindingEntity.get(realm, threePid)?.let { IdentityMapper.map(it) } + } + } + + override fun deletePendingBinding(threePid: ThreePid) { + Realm.getInstance(realmConfiguration).use { + it.executeTransaction { realm -> + IdentityPendingBindingEntity.delete(realm, threePid) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt new file mode 100644 index 0000000000..a72eb75537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityAccountResponse.kt @@ -0,0 +1,29 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityAccountResponse( + /** + * Required. The user ID which registered the token. + */ + @Json(name = "user_id") + val userId: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt new file mode 100644 index 0000000000..16a4e1fc71 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityHashDetailResponse.kt @@ -0,0 +1,45 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityHashDetailResponse( + /** + * Required. The pepper the client MUST use in hashing identifiers, and MUST supply to the /lookup endpoint when performing lookups. + * Servers SHOULD rotate this string often. + */ + @Json(name = "lookup_pepper") + val pepper: String, + + /** + * Required. The algorithms the server supports. Must contain at least "sha256". + * "none" can be another possible value. + */ + @Json(name = "algorithms") + val algorithms: List<String> +) { + companion object { + const val ALGORITHM_SHA256 = "sha256" + const val ALGORITHM_NONE = "none" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt new file mode 100644 index 0000000000..f87d14e1fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpParams.kt @@ -0,0 +1,45 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpParams( + /** + * Required. The addresses to look up. The format of the entries here depend on the algorithm used. + * Note that queries which have been incorrectly hashed or formatted will lead to no matches. + */ + @Json(name = "addresses") + val hashedAddresses: List<String>, + + /** + * Required. The algorithm the client is using to encode the addresses. This should be one of the available options from /hash_details. + */ + @Json(name = "algorithm") + val algorithm: String, + + /** + * Required. The pepper from /hash_details. This is required even when the algorithm does not make use of it. + */ + @Json(name = "pepper") + val pepper: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.kt new file mode 100644 index 0000000000..a71e2f7366 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityLookUpResponse.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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/blob/hs/hash-identity/proposals/2134-identity-hash-lookup.md + */ +@JsonClass(generateAdapter = true) +internal data class IdentityLookUpResponse( + /** + * Required. Any applicable mappings of addresses to Matrix User IDs. Addresses which do not have associations will + * not be included, which can make this property be an empty object. + */ + @Json(name = "mappings") + val mappings: Map<String, String> +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt new file mode 100644 index 0000000000..86999d570d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRegisterResponse.kt @@ -0,0 +1,29 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRegisterResponse( + /** + * Required. An opaque string representing the token to authenticate future requests to the identity server with. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt new file mode 100644 index 0000000000..9da86cbc48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestOwnershipParams.kt @@ -0,0 +1,40 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestOwnershipParams( + /** + * Required. The client secret that was supplied to the requestToken call. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The session ID, generated by the requestToken call. + */ + @Json(name = "sid") + val sid: String, + + /** + * Required. The token generated by the requestToken call and sent to the user. + */ + @Json(name = "token") + val token: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt new file mode 100644 index 0000000000..3e92ebb1d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenBody.kt @@ -0,0 +1,82 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// Just to consider common parameters +private interface IdentityRequestTokenBody { + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. + * Its length must not exceed 255 characters and it must not be empty. + */ + val clientSecret: String + + val sendAttempt: Int +} + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForEmailBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly + * sending the same email in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String +) : IdentityRequestTokenBody + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenForMsisdnBody( + @Json(name = "client_secret") + override val clientSecret: String, + + /** + * Required. The server will only send an SMS if the send_attempt is a number greater than the most recent one + * which it has seen, scoped to that country + phone_number + client_secret triple. This is to avoid repeatedly + * sending the same SMS in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new SMS (e.g. a reminder) to be sent. + */ + @Json(name = "send_attempt") + override val sendAttempt: Int, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val phoneNumber: String, + + /** + * Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in phone_number + * should be parsed as if it were dialled from. + */ + @Json(name = "country") + val countryCode: String +) : IdentityRequestTokenBody diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt new file mode 100644 index 0000000000..cb3c257ddb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/model/IdentityRequestTokenResponse.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.android.internal.session.identity.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class IdentityRequestTokenResponse( + /** + * Required. The session ID. Session IDs are opaque strings generated by the identity server. + * They must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt new file mode 100644 index 0000000000..37b0da9101 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataDataSource.kt @@ -0,0 +1,66 @@ +/* + * 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.matrix.android.internal.session.identity.todelete + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import io.realm.Realm +import io.realm.RealmQuery +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataDataSource @Inject constructor(private val monarchy: Monarchy, + private val accountDataMapper: AccountDataMapper) { + + fun getAccountDataEvent(type: String): UserAccountDataEvent? { + return getAccountDataEvents(setOf(type)).firstOrNull() + } + + fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>> { + return Transformations.map(getLiveAccountDataEvents(setOf(type))) { + it.firstOrNull()?.toOptional() + } + } + + fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> { + return monarchy.fetchAllMappedSync( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> { + return monarchy.findAllMappedWithChanges( + { accountDataEventsQuery(it, types) }, + accountDataMapper::map + ) + } + + private fun accountDataEventsQuery(realm: Realm, types: Set<String>): RealmQuery<UserAccountDataEntity> { + val query = realm.where(UserAccountDataEntity::class.java) + if (types.isNotEmpty()) { + query.`in`(UserAccountDataEntityFields.TYPE, types.toTypedArray()) + } + return query + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.kt new file mode 100644 index 0000000000..4627911b72 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/AccountDataMapper.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.matrix.android.internal.session.identity.todelete + +import com.squareup.moshi.Moshi +import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE +import im.vector.matrix.android.internal.database.model.UserAccountDataEntity +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent +import javax.inject.Inject + +// There will be a duplicated class when Integration manager will be merged, so delete this one +internal class AccountDataMapper @Inject constructor(moshi: Moshi) { + + private val adapter = moshi.adapter<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE) + + fun map(entity: UserAccountDataEntity): UserAccountDataEvent { + return UserAccountDataEvent( + type = entity.type ?: "", + content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() + ) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.kt new file mode 100644 index 0000000000..f84756fa86 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/identity/todelete/LiveData.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.matrix.android.internal.session.identity.todelete + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer + +// There will be a duplicated class when Integration manager will be merged, so delete this one +inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt index f3a06af929..931d1b9bb8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/integrationmanager/IntegrationManager.kt @@ -91,7 +91,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: fun start() { lifecycleRegistry.currentState = Lifecycle.State.STARTED accountDataDataSource - .getLiveAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + .getLiveAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) .observeNotNull(lifecycleOwner) { val allowedWidgetsContent = it.getOrNull()?.content?.toModel<AllowedWidgetsContent>() if (allowedWidgetsContent != null) { @@ -99,7 +99,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } } accountDataDataSource - .getLiveAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING) + .getLiveAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) .observeNotNull(lifecycleOwner) { val integrationProvisioningContent = it.getOrNull()?.content?.toModel<IntegrationProvisioningContent>() if (integrationProvisioningContent != null) { @@ -142,7 +142,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: * Returns false if the user as disabled integration manager feature */ fun isIntegrationEnabled(): Boolean { - val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING) + val integrationProvisioningData = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_INTEGRATION_PROVISIONING) val integrationProvisioningContent = integrationProvisioningData?.content?.toModel<IntegrationProvisioningContent>() return integrationProvisioningContent?.enabled ?: false } @@ -163,7 +163,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } fun setWidgetAllowed(stateEventId: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() val newContent = if (currentContent == null) { val allowedWidget = mapOf(stateEventId to allowed) @@ -183,13 +183,13 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } fun isWidgetAllowed(stateEventId: String): Boolean { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() return currentContent?.widgets?.get(stateEventId) ?: false } fun setNativeWidgetDomainAllowed(widgetType: String, domain: String, allowed: Boolean, callback: MatrixCallback<Unit>): Cancelable { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() val newContent = if (currentContent == null) { val nativeAllowedWidgets = mapOf(widgetType to mapOf(domain to allowed)) @@ -213,7 +213,7 @@ internal class IntegrationManager @Inject constructor(private val taskExecutor: } fun isNativeWidgetAllowed(widgetType: String, domain: String?): Boolean { - val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS) + val currentAllowedWidgets = accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ALLOWED_WIDGETS) val currentContent = currentAllowedWidgets?.content?.toModel<AllowedWidgetsContent>() return currentContent?.native?.get(widgetType)?.get(domain) ?: false } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/AccountThreePidsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/AccountThreePidsResponse.kt new file mode 100644 index 0000000000..17f12113dd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/AccountThreePidsResponse.kt @@ -0,0 +1,28 @@ +/* + * 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.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the ThreePids response + */ +@JsonClass(generateAdapter = true) +internal data class AccountThreePidsResponse( + @Json(name = "threepids") + val threePids: List<ThirdPartyIdentifier>? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidBody.kt new file mode 100644 index 0000000000..6ba3ddda4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidBody.kt @@ -0,0 +1,46 @@ +/* + * 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.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class BindThreePidBody( + /** + * Required. The client secret used in the session with the identity server. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The identity server to use. (without "https://") + */ + @Json(name = "id_server") + var identityServerUrlWithoutProtocol: String, + + /** + * Required. An access token previously registered with the identity server. + */ + @Json(name = "id_access_token") + var identityServerAccessToken: String, + + /** + * Required. The session identifier given by the identity server. + */ + @Json(name = "sid") + var sid: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidsTask.kt new file mode 100644 index 0000000000..0e1987dd5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/BindThreePidsTask.kt @@ -0,0 +1,59 @@ +/* + * 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.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.di.AuthenticatedIdentity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.network.token.AccessTokenProvider +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class BindThreePidsTask : Task<BindThreePidsTask.Params, Unit> { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultBindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + @AuthenticatedIdentity + private val accessTokenProvider: AccessTokenProvider, + private val eventBus: EventBus) : BindThreePidsTask() { + override suspend fun execute(params: Params) { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured + val identityPendingBinding = identityStore.getPendingBinding(params.threePid) ?: throw IdentityServiceError.NoCurrentBindingError + + executeRequest<Unit>(eventBus) { + apiCall = profileAPI.bindThreePid( + BindThreePidBody( + clientSecret = identityPendingBinding.clientSecret, + identityServerUrlWithoutProtocol = identityServerUrlWithoutProtocol, + identityServerAccessToken = identityServerAccessToken, + sid = identityPendingBinding.sid + )) + } + + // Binding is over, cleanup the store + identityStore.deletePendingBinding(params.threePid) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt index e2c18e41d6..a981e8e930 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -17,16 +17,23 @@ package im.vector.matrix.android.internal.session.profile +import androidx.lifecycle.LiveData +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.database.model.UserThreePidEntity import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith +import io.realm.kotlin.where import javax.inject.Inject internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, + private val monarchy: Monarchy, + private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val getProfileInfoTask: GetProfileInfoTask) : ProfileService { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { @@ -73,4 +80,33 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto } .executeBy(taskExecutor) } + + override fun getThreePids(): List<ThreePid> { + return monarchy.fetchAllMappedSync( + { it.where<UserThreePidEntity>() }, + { it.asDomain() } + ) + } + + override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> { + if (refreshData) { + // Force a refresh of the values + refreshUserThreePidsTask + .configureWith() + .executeBy(taskExecutor) + } + + return monarchy.findAllMappedWithChanges( + { it.where<UserThreePidEntity>() }, + { it.asDomain() } + ) + } +} + +private fun UserThreePidEntity.asDomain(): ThreePid { + return when (medium) { + ThirdPartyIdentifier.MEDIUM_EMAIL -> ThreePid.Email(address) + ThirdPartyIdentifier.MEDIUM_MSISDN -> ThreePid.Msisdn(address) + else -> error("Invalid medium type") + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt index 197d85f879..717497e582 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt @@ -20,10 +20,12 @@ package im.vector.matrix.android.internal.session.profile import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path -interface ProfileAPI { +internal interface ProfileAPI { /** * Get the combined profile information for this user. @@ -33,4 +35,24 @@ interface ProfileAPI { */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") fun getProfile(@Path("userId") userId: String): Call<JsonDict> + + /** + * List all 3PIDs linked to the Matrix user account. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid") + fun getThreePIDs(): Call<AccountThreePidsResponse> + + /** + * Bind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/bind") + fun bindThreePid(@Body body: BindThreePidBody): Call<Unit> + + /** + * Unbind a threePid + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-unbind + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind") + fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt index 7005a5341f..0d7ebe5b62 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt @@ -42,4 +42,13 @@ internal abstract class ProfileModule { @Binds abstract fun bindGetProfileTask(task: DefaultGetProfileInfoTask): GetProfileInfoTask + + @Binds + abstract fun bindRefreshUserThreePidsTask(task: DefaultRefreshUserThreePidsTask): RefreshUserThreePidsTask + + @Binds + abstract fun bindBindThreePidsTask(task: DefaultBindThreePidsTask): BindThreePidsTask + + @Binds + abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/RefreshUserThreePidsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/RefreshUserThreePidsTask.kt new file mode 100644 index 0000000000..9e4d683b8e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/RefreshUserThreePidsTask.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.matrix.android.internal.session.profile + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.UserThreePidEntity +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import timber.log.Timber +import javax.inject.Inject + +internal abstract class RefreshUserThreePidsTask : Task<Unit, Unit> + +internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val monarchy: Monarchy, + private val eventBus: EventBus) : RefreshUserThreePidsTask() { + + override suspend fun execute(params: Unit) { + val accountThreePidsResponse = executeRequest<AccountThreePidsResponse>(eventBus) { + apiCall = profileAPI.getThreePIDs() + } + + Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") + // Store the list in DB + monarchy.writeAsync { realm -> + realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() + accountThreePidsResponse.threePids?.forEach { + val entity = UserThreePidEntity( + it.medium?.takeIf { med -> med in ThirdPartyIdentifier.SUPPORTED_MEDIUM } ?: return@forEach, + it.address ?: return@forEach, + it.validatedAt.toLong(), + it.addedAt.toLong()) + realm.insertOrUpdate(entity) + } + } + } +} + +private fun Any?.toLong(): Long { + return when (this) { + null -> 0L + is Long -> this + is Double -> this.toLong() + else -> 0L + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ThirdPartyIdentifier.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ThirdPartyIdentifier.kt new file mode 100755 index 0000000000..76fa3bd80f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ThirdPartyIdentifier.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.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ThirdPartyIdentifier( + /** + * Required. The medium of the third party identifier. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String? = null, + + /** + * Required. The third party identifier address. + */ + @Json(name = "address") + val address: String? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been validated. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "validated_at") + val validatedAt: Any? = null, + + /** + * Required. The timestamp in milliseconds when this 3PID has been added to the user account. + * Define as Object because it should be Long and it is a Double. + * So, it might change. + */ + @Json(name = "added_at") + val addedAt: Any? = null +) { + companion object { + const val MEDIUM_EMAIL = "email" + const val MEDIUM_MSISDN = "msisdn" + + val SUPPORTED_MEDIUM = listOf(MEDIUM_EMAIL, MEDIUM_MSISDN) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidBody.kt new file mode 100644 index 0000000000..705569ba87 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidBody.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.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidBody( + /** + * The identity server to unbind from. If not provided, the homeserver MUST use the id_server the identifier was added through. + * If the homeserver does not know the original id_server, it MUST return a id_server_unbind_result of no-support. + */ + @Json(name = "id_server") + val identityServerUrlWithoutProtocol: String?, + + /** + * Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"] + */ + @Json(name = "medium") + val medium: String, + + /** + * Required. The third party address being removed. + */ + @Json(name = "address") + val address: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidResponse.kt new file mode 100644 index 0000000000..51467ad201 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidResponse.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.matrix.android.internal.session.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UnbindThreePidResponse( + @Json(name = "id_server_unbind_result") + val idServerUnbindResult: String? +) { + fun isSuccess() = idServerUnbindResult == "success" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidsTask.kt new file mode 100644 index 0000000000..5206ea9bda --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/UnbindThreePidsTask.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.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.identity.toMedium +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.data.IdentityStore +import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class UnbindThreePidsTask : Task<UnbindThreePidsTask.Params, Boolean> { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultUnbindThreePidsTask @Inject constructor(private val profileAPI: ProfileAPI, + private val identityStore: IdentityStore, + private val eventBus: EventBus) : UnbindThreePidsTask() { + override suspend fun execute(params: Params): Boolean { + val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() + ?: throw IdentityServiceError.NoIdentityServerConfigured + + return executeRequest<UnbindThreePidResponse>(eventBus) { + apiCall = profileAPI.unbindThreePid( + UnbindThreePidBody( + identityServerUrlWithoutProtocol, + params.threePid.toMedium(), + params.threePid.value + )) + }.isSuccess() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index f2bee734ce..bf6b81b57c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -35,7 +35,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields -import im.vector.matrix.android.internal.database.query.FilterContent +import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.whereInRoom @@ -724,8 +724,11 @@ internal class DefaultTimeline( `in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray()) } if (settings.filterEdits) { - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) - not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) + not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) + } + if (settings.filterRedacted) { + not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) } return this } @@ -737,13 +740,19 @@ internal class DefaultTimeline( } else { true } + if (!filterType) return@filter false + val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { val messageContent = it.root.content.toModel<MessageContent>() messageContent?.relatesTo?.type != RelationType.REPLACE } else { true } - filterType && filterEdits + if (!filterEdits) return@filter false + + val filterRedacted = settings.filterRedacted && it.root.isRedacted() + + filterRedacted } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 056f942211..72e99701cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntit import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields -import im.vector.matrix.android.internal.database.query.FilterContent +import im.vector.matrix.android.internal.database.query.TimelineEventFilter import im.vector.matrix.android.internal.database.query.whereInRoom import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm @@ -149,16 +149,21 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu */ private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> { beginGroup() + var needOr = false if (settings.filterTypes) { not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) - } - if (settings.filterTypes && settings.filterEdits) { - or() + needOr = true } if (settings.filterEdits) { - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE) + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) + needOr = true + } + if (settings.filterRedacted) { + if (needOr) or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) } endGroup() return this diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt index 68df456831..021b3ed066 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt @@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.GlobalScope import javax.inject.Inject internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, @@ -45,7 +44,7 @@ internal class DefaultSignOutService @Inject constructor(private val signOutTask override fun updateCredentials(credentials: Credentials, callback: MatrixCallback<Unit>): Cancelable { - return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { sessionParamsStore.updateCredentials(credentials) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt index 0b8902e71b..5763d397c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt @@ -43,13 +43,13 @@ internal class DefaultSignInAgainTask @Inject constructor( apiCall = signOutAPI.loginAgain( PasswordLoginParams.userIdentifier( // Reuse the same userId - sessionParams.credentials.userId, + sessionParams.userId, params.password, // The spec says the initial device name will be ignored // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login // but https://github.com/matrix-org/synapse/issues/6525 // Reuse the same deviceId - deviceId = sessionParams.credentials.deviceId + deviceId = sessionParams.deviceId ) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 610ade5744..cca0af7feb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.cleanup.CleanupSession +import im.vector.matrix.android.internal.session.identity.IdentityDisconnectTask import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus import timber.log.Timber @@ -35,6 +36,7 @@ internal interface SignOutTask : Task<SignOutTask.Params, Unit> { internal class DefaultSignOutTask @Inject constructor( private val signOutAPI: SignOutAPI, private val eventBus: EventBus, + private val identityDisconnectTask: IdentityDisconnectTask, private val cleanupSession: CleanupSession ) : SignOutTask { @@ -60,6 +62,10 @@ internal class DefaultSignOutTask @Inject constructor( } } + // Logout from identity server if any + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + Timber.d("SignOut: cleanup session...") cleanupSession.handle() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index 43a5d8a5cb..d758110e09 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -30,9 +30,9 @@ abstract class UserAccountData : AccountDataContent { const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" const val TYPE_PUSH_RULES = "m.push_rules" - - const val ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" - const val ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" - + const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning" + const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets" + const val TYPE_IDENTITY_SERVER = "m.identity_server" + const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt new file mode 100644 index 0000000000..ef34503463 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAcceptedTerms.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataAcceptedTerms( + @Json(name = "type") override val type: String = TYPE_ACCEPTED_TERMS, + @Json(name = "content") val content: AcceptedTermsContent +) : UserAccountData() + +@JsonClass(generateAdapter = true) +internal data class AcceptedTermsContent( + @Json(name = "accepted") val acceptedTerms: List<String> = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt index ae6ca686ed..43f3b3b32a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataAllowedWidgets.kt @@ -22,6 +22,6 @@ import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidge @JsonClass(generateAdapter = true) internal data class UserAccountDataAllowedWidgets( - @Json(name = "type") override val type: String = ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS, + @Json(name = "type") override val type: String = TYPE_ALLOWED_WIDGETS, @Json(name = "content") val content: AllowedWidgetsContent ) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt new file mode 100644 index 0000000000..4af2034d64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIdentityServer.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataIdentityServer( + @Json(name = "type") override val type: String = TYPE_IDENTITY_SERVER, + @Json(name = "content") val content: IdentityServerContent? = null +) : UserAccountData() + +@JsonClass(generateAdapter = true) +internal data class IdentityServerContent( + @Json(name = "base_url") val baseUrl: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt index c65bbd82c1..a47bb761cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataIntegrationProvisioning.kt @@ -22,6 +22,6 @@ import im.vector.matrix.android.internal.session.integrationmanager.IntegrationP @JsonClass(generateAdapter = true) internal data class UserAccountDataIntegrationProvisioning( - @Json(name = "type") override val type: String = ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING, + @Json(name = "type") override val type: String = TYPE_INTEGRATION_PROVISIONING, @Json(name = "content") val content: IntegrationProvisioningContent ) : UserAccountData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt new file mode 100644 index 0000000000..c5827b822f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/AcceptTermsBody.kt @@ -0,0 +1,29 @@ +/* + * 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.matrix.android.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represent a list of urls of terms the user wants to accept + */ +@JsonClass(generateAdapter = true) +internal data class AcceptTermsBody( + @Json(name = "user_accepts") + val acceptedTermUrls: List<String> +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt new file mode 100644 index 0000000000..6d5e597da8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/DefaultTermsService.kt @@ -0,0 +1,123 @@ +/* + * 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.matrix.android.internal.session.terms + +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.terms.GetTermsResponse +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.NetworkConstants +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI +import im.vector.matrix.android.internal.session.identity.IdentityRegisterTask +import im.vector.matrix.android.internal.session.identity.todelete.AccountDataDataSource +import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask +import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal class DefaultTermsService @Inject constructor( + @Unauthenticated + private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + private val accountDataDataSource: AccountDataDataSource, + private val termsAPI: TermsAPI, + private val retrofitFactory: RetrofitFactory, + private val getOpenIdTokenTask: GetOpenIdTokenTask, + private val identityRegisterTask: IdentityRegisterTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val taskExecutor: TaskExecutor +) : TermsService { + override fun getTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + callback: MatrixCallback<GetTermsResponse>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val sep = if (baseUrl.endsWith("/")) "" else "/" + + val url = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" + TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" + } + + val termsResponse = executeRequest<TermsResponse>(null) { + apiCall = termsAPI.getTerms("${url}terms") + } + + GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) + } + } + + override fun agreeToTerms(serviceType: TermsService.ServiceType, + baseUrl: String, + agreedUrls: List<String>, + token: String?, + callback: MatrixCallback<Unit>): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + val sep = if (baseUrl.endsWith("/")) "" else "/" + + val url = when (serviceType) { + TermsService.ServiceType.IntegrationManager -> "$baseUrl$sep${NetworkConstants.URI_INTEGRATION_MANAGER_PATH}" + TermsService.ServiceType.IdentityService -> "$baseUrl$sep${NetworkConstants.URI_IDENTITY_PATH_V2}" + } + + val tokenToUse = token?.takeIf { it.isNotEmpty() } ?: getToken(baseUrl) + + executeRequest<Unit>(null) { + apiCall = termsAPI.agreeToTerms("${url}terms", AcceptTermsBody(agreedUrls), "Bearer $tokenToUse") + } + + // client SHOULD update this account data section adding any the URLs + // of any additional documents that the user agreed to this list. + // Get current m.accepted_terms append new ones and update account data + val listOfAcceptedTerms = getAlreadyAcceptedTermUrlsFromAccountData() + + val newList = listOfAcceptedTerms.toMutableSet().apply { addAll(agreedUrls) }.toList() + + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.AcceptedTermsParams( + acceptedTermsContent = AcceptedTermsContent(newList) + )) + } + } + + private suspend fun getToken(url: String): String { + // TODO This is duplicated code see DefaultIdentityService + val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java) + + val openIdToken = getOpenIdTokenTask.execute(Unit) + val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken)) + + return token.token + } + + private fun getAlreadyAcceptedTermUrlsFromAccountData(): Set<String> { + return accountDataDataSource.getAccountDataEvent(UserAccountData.TYPE_ACCEPTED_TERMS) + ?.content + ?.toModel<AcceptedTermsContent>() + ?.acceptedTerms + ?.toSet() + .orEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt similarity index 53% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt index 2a0e00704c..03b745f8d7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsAPI.kt @@ -13,22 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.identity -import im.vector.matrix.android.internal.network.NetworkConstants +package im.vector.matrix.android.internal.session.terms + +import im.vector.matrix.android.internal.network.HttpHeaders import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url -internal interface IdentityPingApi { +internal interface TermsAPI { + /** + * This request does not require authentication + */ + @GET + fun getTerms(@Url url: String): Call<TermsResponse> /** - * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery - * Simple ping call to check if server alive - * - * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check - * - * @return 200 in case of success + * This request requires authentication */ - @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) - fun ping(): Call<Unit> + @POST + fun agreeToTerms(@Url url: String, @Body params: AcceptTermsBody, @Header(HttpHeaders.Authorization) token: String): Call<Unit> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt new file mode 100644 index 0000000000..eee7e22134 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsModule.kt @@ -0,0 +1,46 @@ +/* + * 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.matrix.android.internal.session.terms + +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.session.SessionScope +import okhttp3.OkHttpClient + +@Module +internal abstract class TermsModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesTermsAPI(@Unauthenticated unauthenticatedOkHttpClient: Lazy<OkHttpClient>, + retrofitFactory: RetrofitFactory): TermsAPI { + val retrofit = retrofitFactory.create(unauthenticatedOkHttpClient, "https://foo.bar") + return retrofit.create(TermsAPI::class.java) + } + } + + @Binds + abstract fun bindTermsService(service: DefaultTermsService): TermsService +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt new file mode 100644 index 0000000000..7c6451e3a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/terms/TermsResponse.kt @@ -0,0 +1,55 @@ +/* + * 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.matrix.android.internal.session.terms + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms + +/** + * This class represent a localized privacy policy for registration Flow. + */ +@JsonClass(generateAdapter = true) +data class TermsResponse( + @Json(name = "policies") + val policies: JsonDict? = null +) { + + fun getLocalizedTerms(userLanguage: String, + defaultLanguage: String = "en"): List<LocalizedFlowDataLoginTerms> { + return policies?.map { + val tos = policies[it.key] as? Map<*, *> ?: return@map null + ((tos[userLanguage] ?: tos[defaultLanguage]) as? Map<*, *>)?.let { termsMap -> + val name = termsMap[NAME] as? String + val url = termsMap[URL] as? String + LocalizedFlowDataLoginTerms( + policyName = it.key, + localizedUrl = url, + localizedName = name, + version = tos[VERSION] as? String + ) + } + }?.filterNotNull() ?: emptyList() + } + + private companion object { + const val VERSION = "version" + const val NAME = "name" + const val URL = "url" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 22ab61c93a..8db2ad9781 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -20,7 +20,9 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidgetsContent import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.AcceptedTermsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent +import im.vector.matrix.android.internal.session.sync.model.accountdata.IdentityServerContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.task.Task import org.greenrobot.eventbus.EventBus @@ -33,6 +35,24 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa fun getData(): Any } + data class IdentityParams(override val type: String = UserAccountData.TYPE_IDENTITY_SERVER, + private val identityContent: IdentityServerContent + ) : Params { + + override fun getData(): Any { + return identityContent + } + } + + data class AcceptedTermsParams(override val type: String = UserAccountData.TYPE_ACCEPTED_TERMS, + private val acceptedTermsContent: AcceptedTermsContent + ) : Params { + + override fun getData(): Any { + return acceptedTermsContent + } + } + // TODO Use [UserAccountDataDirectMessages] class? data class DirectChatParams(override val type: String = UserAccountData.TYPE_DIRECT_MESSAGES, private val directMessages: Map<String, List<String>> @@ -52,7 +72,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa } } - data class AllowedWidgets(override val type: String = UserAccountData.ACCOUNT_DATA_TYPE_ALLOWED_WIDGETS, + data class AllowedWidgets(override val type: String = UserAccountData.TYPE_ALLOWED_WIDGETS, private val allowedWidgetsContent: AllowedWidgetsContent) : Params { override fun getData(): Any { @@ -60,7 +80,7 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa } } - data class IntegrationProvisioning(override val type: String = UserAccountData.ACCOUNT_DATA_TYPE_INTEGRATION_PROVISIONING, + data class IntegrationProvisioning(override val type: String = UserAccountData.TYPE_INTEGRATION_PROVISIONING, private val integrationProvisioningContent: IntegrationProvisioningContent) : Params { override fun getData(): Any { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt index 8ad5e89605..7c81a03223 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -26,3 +26,14 @@ internal fun String.isValidUrl(): Boolean { false } } + +/** + * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + */ +internal fun String.ensureProtocol(): String { + return when { + isEmpty() -> this + !startsWith("http") -> "https://$this" + else -> this + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt index 8ed2cb3b0f..c6f6b8752d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/GetWellknownTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.wellknown import android.util.MalformedJsonException import dagger.Lazy @@ -23,10 +23,10 @@ import im.vector.matrix.android.api.auth.data.WellKnown import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.internal.di.Unauthenticated -import im.vector.matrix.android.internal.identity.IdentityPingApi import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI +import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.isValidUrl import okhttp3.OkHttpClient @@ -119,7 +119,7 @@ internal class DefaultGetWellknownTask @Inject constructor( try { executeRequest<Unit>(null) { - apiCall = capabilitiesAPI.getVersions() + apiCall = capabilitiesAPI.ping() } } catch (throwable: Throwable) { return WellknownResult.FailError @@ -153,7 +153,7 @@ internal class DefaultGetWellknownTask @Inject constructor( */ private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) - .create(IdentityPingApi::class.java) + .create(IdentityAuthAPI::class.java) return try { executeRequest<Unit>(null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt index 71928123bf..ec62707db7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellKnownAPI.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package im.vector.matrix.android.internal.auth.wellknown +package im.vector.matrix.android.internal.wellknown import im.vector.matrix.android.api.auth.data.WellKnown import retrofit2.Call diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt similarity index 60% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt index 6e89a28b7d..2705803fec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/wellknown/WellknownModule.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * 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 + * 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,10 +14,14 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.database.query +package im.vector.matrix.android.internal.wellknown -internal object FilterContent { +import dagger.Binds +import dagger.Module - internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" - internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" +@Module +internal abstract class WellknownModule { + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask } 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 dc874c2b94..871d01175e 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -179,8 +179,8 @@ <string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string> <string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string> <string name="notice_room_invite_you_with_reason">%1$s hat dich eingeladen. Grund: %2$s</string> - <string name="notice_room_join_with_reason">%1$s beigetreten. Grund: %2$s</string> - <string name="notice_room_leave_with_reason">%1$s ging. Grund: %2$s</string> + <string name="notice_room_join_with_reason">%1$s ist dem Raum beigetreten. Grund: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s hat den Raum verlassen. Grund: %2$s</string> <string name="notice_room_reject_with_reason">%1$s hat die Einladung abgelehnt. Grund: %2$s</string> <string name="notice_room_kick_with_reason">%1$s hat %2$s gekickt. Grund: %3$s</string> <string name="notice_room_unban_with_reason">%1$s hat Sperre von %2$s aufgehoben. Grund: %3$s</string> diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml index c88d96d610..69600394ac 100644 --- a/matrix-sdk-android/src/main/res/values-eo/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -3,20 +3,210 @@ <string name="summary_user_sent_image">%1$s sendis bildon.</string> <string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string> - <string name="notice_room_invite_no_invitee">invito de %s</string> - <string name="notice_room_invite">%1$s invitis %2$s</string> + <string name="notice_room_invite_no_invitee">Invito de %s</string> + <string name="notice_room_invite">%1$s invitis uzanton %2$s</string> <string name="notice_room_invite_you">%1$s invitis vin</string> <string name="notice_room_join">%1$s alvenis</string> <string name="notice_room_leave">%1$s foriris</string> <string name="notice_room_reject">%1$s malakceptis la inviton</string> - <string name="notice_room_kick">%1$s forpelis %2$s</string> - <string name="notice_room_unban">%1$s malforbaris %2$s</string> - <string name="notice_room_ban">%1$s forbaris %2$s</string> - <string name="notice_room_withdraw">%1$s malinvitis %2$s</string> + <string name="notice_room_kick">%1$s forpelis uzanton %2$s</string> + <string name="notice_room_unban">%1$s malforbaris uzanton %2$s</string> + <string name="notice_room_ban">%1$s forbaris uzanton %2$s</string> + <string name="notice_room_withdraw">%1$s nuligis inviton por %2$s</string> <string name="notice_avatar_url_changed">%1$s ŝanĝis sian profilbildon</string> <string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string> <string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string> - <string name="message_reply_to_prefix">Respondanta al</string> + <string name="message_reply_to_prefix">Responde al</string> + + <string name="summary_message">%1$s: %2$s</string> + <string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string> + <string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string> + <string name="notice_display_name_removed">%1$s forigis sian vidigan nomon (%2$s)</string> + <string name="notice_room_topic_changed">%1$s ŝanĝis la temon al: %2$s</string> + <string name="notice_room_name_changed">%1$s ŝanĝis nomon de la ĉambro al: %2$s</string> + <string name="notice_placed_video_call">%s vidvokis.</string> + <string name="notice_placed_voice_call">%s voĉvokis.</string> + <string name="notice_answered_call">%s respondis la vokon.</string> + <string name="notice_ended_call">%s finis la vokon.</string> + <string name="notice_made_future_room_visibility">%1$s videbligis estontan historion de ĉambro al %2$s</string> + <string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde iliaj invitoj.</string> + <string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde iliaj aliĝoj.</string> + <string name="notice_room_visibility_shared">ĉiuj ĉambranoj.</string> + <string name="notice_room_visibility_world_readable">ĉiu ajn.</string> + <string name="notice_room_visibility_unknown">nekonata (%s).</string> + <string name="notice_end_to_end">%1$s ŝaltis tutvojan ĉifradon (%2$s)</string> + <string name="notice_room_update">%s gradaltigis la ĉambron.</string> + + <string name="notice_event_redacted">Mesaĝo foriĝis</string> + <string name="notice_event_redacted_by">Mesaĝo foriĝis de %1$s</string> + <string name="notice_event_redacted_with_reason">Mesaĝo foriĝis [kialo: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Mesaĝo foriĝis de %1$s [kialo: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s ĝisdatigis sian profilon %2$s</string> + <string name="notice_room_third_party_invite">%1$s sendis aliĝan inviton al %2$s</string> + <string name="notice_room_third_party_revoked_invite">%1$s nuligis la aliĝan inviton por %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s akceptis la inviton por %2$s</string> + + <string name="could_not_redact">Ne povis redakti</string> + <string name="unable_to_send_message">Ne povas sendi mesaĝon</string> + + <string name="message_failed_to_upload">Malsukcesis alŝuti bildon</string> + + <string name="network_error">Reta eraro</string> + <string name="matrix_error">Matrix-eraro</string> + + <string name="room_error_join_failed_empty_room">Nun ne eblas re-aliĝi al malplena ĉambro</string> + + <string name="encrypted_message">Ĉifrita mesaĝo</string> + + <string name="medium_email">Retpoŝtadreso</string> + <string name="medium_phone_number">Telefonnumero</string> + + <string name="reply_to_an_image">sendis bildon.</string> + <string name="reply_to_a_video">sendis filmon.</string> + <string name="reply_to_an_audio_file">sendis sondosieron.</string> + <string name="reply_to_a_file">sendis dosieron.</string> + + <string name="room_displayname_invite_from">Invito de %s</string> + <string name="room_displayname_room_invite">Ĉambra invito</string> + + <string name="room_displayname_two_members">%1$s kaj %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s kaj 1 alia</item> + <item quantity="other">%1$s kaj %2$d aliaj</item> + </plurals> + + <string name="room_displayname_empty_room">Malplena ĉambro</string> + + + <string name="verification_emoji_dog">Hundo</string> + <string name="verification_emoji_cat">Kato</string> + <string name="verification_emoji_lion">Leono</string> + <string name="verification_emoji_horse">Ĉevalo</string> + <string name="verification_emoji_unicorn">Unukorno</string> + <string name="verification_emoji_pig">Porko</string> + <string name="verification_emoji_elephant">Elefanto</string> + <string name="verification_emoji_rabbit">Kuniklo</string> + <string name="verification_emoji_panda">Pando</string> + <string name="verification_emoji_rooster">Koko</string> + <string name="verification_emoji_penguin">Pingveno</string> + <string name="verification_emoji_turtle">Testudo</string> + <string name="verification_emoji_fish">Fiŝo</string> + <string name="verification_emoji_octopus">Polpo</string> + <string name="verification_emoji_butterfly">Papilio</string> + <string name="verification_emoji_flower">Floro</string> + <string name="verification_emoji_tree">Arbo</string> + <string name="verification_emoji_cactus">Kakto</string> + <string name="verification_emoji_mushroom">Fungo</string> + <string name="verification_emoji_globe">Globo</string> + <string name="verification_emoji_moon">Luno</string> + <string name="verification_emoji_cloud">Nubo</string> + <string name="verification_emoji_fire">Fajro</string> + <string name="verification_emoji_banana">Banano</string> + <string name="verification_emoji_apple">Pomo</string> + <string name="verification_emoji_strawberry">Frago</string> + <string name="verification_emoji_corn">Maizo</string> + <string name="verification_emoji_pizza">Pico</string> + <string name="verification_emoji_cake">Kuko</string> + <string name="verification_emoji_heart">Koro</string> + <string name="verification_emoji_smiley">Mieneto</string> + <string name="verification_emoji_robot">Roboto</string> + <string name="verification_emoji_hat">Ĉapelo</string> + <string name="verification_emoji_glasses">Okulvitroj</string> + <string name="verification_emoji_wrench">Boltilo</string> + <string name="verification_emoji_santa">Kristnaska viro</string> + <string name="verification_emoji_thumbsup">Dikfingro supren</string> + <string name="verification_emoji_umbrella">Ombrelo</string> + <string name="verification_emoji_hourglass">Sablohorloĝo</string> + <string name="verification_emoji_clock">Horloĝo</string> + <string name="verification_emoji_gift">Donaco</string> + <string name="verification_emoji_lightbulb">Lampo</string> + <string name="verification_emoji_book">Libro</string> + <string name="verification_emoji_pencil">Grifelo</string> + <string name="verification_emoji_paperclip">Paperkuntenilo</string> + <string name="verification_emoji_scissors">Tondilo</string> + <string name="verification_emoji_lock">Seruro</string> + <string name="verification_emoji_key">Ŝlosilo</string> + <string name="verification_emoji_hammer">Martelo</string> + <string name="verification_emoji_telephone">Telefono</string> + <string name="verification_emoji_flag">Flago</string> + <string name="verification_emoji_train">Vagonaro</string> + <string name="verification_emoji_bicycle">Biciklo</string> + <string name="verification_emoji_airplane">Aviadilo</string> + <string name="verification_emoji_rocket">Raketo</string> + <string name="verification_emoji_trophy">Trofeo</string> + <string name="verification_emoji_ball">Pilko</string> + <string name="verification_emoji_guitar">Gitaro</string> + <string name="verification_emoji_trumpet">Trumpeto</string> + <string name="verification_emoji_bell">Sonorilo</string> + <string name="verification_emoji_anchor">Ankro</string> + <string name="verification_emoji_headphone">Kapaŭdilo</string> + <string name="verification_emoji_folder">Dosierujo</string> + <string name="verification_emoji_pin">Pinglo</string> + + <string name="initial_sync_start_importing_account">Komenca spegulado: +\nEnportante konton…</string> + <string name="initial_sync_start_importing_account_crypto">Komenca spegulado: +\nEnportante ĉifrilojn</string> + <string name="initial_sync_start_importing_account_rooms">Komenca spegulado: +\nEnportante ĉambrojn</string> + <string name="initial_sync_start_importing_account_joined_rooms">Komenca spegulado: +\nEnportante aliĝitajn ĉambrojn</string> + <string name="initial_sync_start_importing_account_invited_rooms">Komenca spegulado: +\nEnportante ĉambrojn de invitoj</string> + <string name="initial_sync_start_importing_account_left_rooms">Komenca spegulado: +\nEnportante forlasitajn ĉambrojn</string> + <string name="initial_sync_start_importing_account_groups">Komenca spegulado: +\nEnportante komunumojn</string> + <string name="initial_sync_start_importing_account_data">Komenca spegulado: +\nEnportante datumojn de konto</string> + + <string name="event_status_sending_message">Sendante mesaĝon…</string> + <string name="clear_timeline_send_queue">Vakigi sendan atendovicon</string> + + <string name="notice_requested_voip_conference">%1$s petis grupan vokon</string> + <string name="notice_voip_started">Grupa voko komenciĝis</string> + <string name="notice_voip_finished">Grupa voko finiĝis</string> + + <string name="notice_avatar_changed_too">(ankaŭ profilbildo ŝanĝiĝis)</string> + <string name="notice_room_name_removed">%1$s forigis nomon de la ĉambro</string> + <string name="notice_room_topic_removed">%1$s forigis temon de la ĉambro</string> + <string name="notice_room_invite_no_invitee_with_reason">Invito de %1$s. Kialo: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s invitis uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s invitis vin. Kialo: %2$s</string> + <string name="notice_room_join_with_reason">%1$s aliĝis al la ĉambro. Kialo: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s foriris de la ĉambro. Kialo: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s rifuzis la inviton. Kialo: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s forpelis uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_unban_with_reason">%1$s malforbaris uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_ban_with_reason">%1$s forbaris uzanton %2$s. Kialo: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s akceptis la inviton por %2$s. Kialo: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton al %2$s. Kialo: %3$s</string> + + <plurals name="notice_room_aliases_added"> + <item quantity="one">%1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.</item> + <item quantity="other">%1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.</item> + </plurals> + + <plurals name="notice_room_aliases_removed"> + <item quantity="one">%1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.</item> + <item quantity="other">%1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.</item> + </plurals> + + <string name="notice_room_aliases_added_and_removed">%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.</string> + + <string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.</string> + <string name="notice_room_canonical_alias_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</string> + + <string name="notice_room_guest_access_can_join">%1$s permesis al gastoj aliĝi al la ĉambro.</string> + <string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj aliĝi al la ĉambro.</string> + + <string name="notice_end_to_end_ok">%1$s ŝaltis tutvojan ĉifradon.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).</string> + + <string name="key_verification_request_fallback_message">%s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj.</string> </resources> diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..1d52c2a7a1 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -0,0 +1,188 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="summary_message">%1$s: %2$s</string> + <string name="summary_user_sent_image">%1$s saatis pildi.</string> + <string name="summary_user_sent_sticker">%1$s saatis kleepsu.</string> + + <string name="notice_room_invite_no_invitee">Kasutaja %s kutse</string> + <string name="notice_room_invite">%1$s kutsus kasutajat %2$s</string> + <string name="notice_room_invite_you">%1$s kutsus sind</string> + <string name="notice_room_join">%1$s liitus jututoaga</string> + <string name="notice_room_leave">%1$s lahkus jututoast</string> + <string name="notice_room_reject">%1$s lükkas tagasi kutse</string> + <string name="notice_room_kick">%1$s müksas kasutajat %2$s</string> + <string name="notice_room_withdraw">%1$s võttis tagasi kutse kasutajale %2$s</string> + <string name="notice_avatar_url_changed">%1$s muutis oma avatari</string> + <string name="notice_display_name_set">%1$s määras oma kuvatavaks nimeks %2$s</string> + <string name="notice_display_name_changed_from">%1$s muutis senise kuvatava nime %2$s uueks nimeks %3$s</string> + <string name="notice_display_name_removed">%1$s eemaldas oma kuvatava nime (%2$s)</string> + <string name="notice_room_topic_changed">%1$s muutis uueks teemaks %2$s</string> + <string name="notice_room_name_changed">%1$s muutis jututoa uueks nimeks %2$s</string> + <string name="notice_placed_video_call">%s alustas videokõnet.</string> + <string name="notice_placed_voice_call">%s alustas häälkõnet.</string> + <string name="notice_answered_call">%s vastas kõnele.</string> + <string name="notice_ended_call">%s lõpetas kõne.</string> + <string name="notice_made_future_room_visibility">%1$s seadistas, et tulevane jututoa ajalugu on nähtav kasutajale %2$s</string> + <string name="notice_room_visibility_invited">kõikidele jututoa liikmetele alates kutsumise hetkest.</string> + <string name="notice_room_visibility_joined">kõikidele jututoa liikmetele alates liitumise hetkest.</string> + <string name="notice_room_visibility_shared">kõikidele jututoa liikmetele.</string> + <string name="notice_room_visibility_world_readable">kõikidele.</string> + <string name="notice_room_visibility_unknown">teadmata (%s).</string> + <string name="notice_end_to_end">%1$s lülitas sisse läbiva krüptimise (%2$s)</string> + <string name="notice_room_update">%s uuendas seda jututuba.</string> + + <string name="notice_requested_voip_conference">%1$s saatis VoIP konverentsi kutse</string> + <string name="notice_voip_started">VoIP-konverents algas</string> + <string name="notice_voip_finished">VoIP-konverents lõppes</string> + + <string name="notice_avatar_changed_too">(samuti sai avatar muudetud)</string> + <string name="notice_room_name_removed">%1$s eemaldas jututoa nime</string> + <string name="notice_room_topic_removed">%1$s eemaldas jututoa teema</string> + <string name="notice_event_redacted">Sõnum on eemaldatud</string> + <string name="notice_event_redacted_by">Sõnum on eemaldatud %1$s poolt</string> + <string name="notice_event_redacted_with_reason">Sõnum on eemaldatud [põhjus: %1$s]</string> + <string name="notice_event_redacted_by_with_reason">Sõnum on eemaldatud %1$s poolt [põhjus: %2$s]</string> + <string name="notice_profile_change_redacted">%1$s uuendas oma profiili %2$s</string> + <string name="notice_room_third_party_invite">%1$s saatis jututoaga liitumiseks kutse kasutajale %2$s</string> + <string name="notice_room_third_party_revoked_invite">%1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s</string> + <string name="notice_room_third_party_registered_invite">%1$s võttis vastu kutse %2$s nimel</string> + + <string name="notice_crypto_unable_to_decrypt">** Ei õnnestu dekrüptida: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string> + + <string name="message_reply_to_prefix">Vastuseks kasutajale</string> + + <string name="could_not_redact">Ei saanud muuta sõnumit</string> + <string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string> + + <string name="message_failed_to_upload">Faili üles laadimine ei õnnestunud</string> + + <string name="network_error">Võrguühenduse viga</string> + <string name="matrix_error">Matrix\'i viga</string> + + <string name="room_error_join_failed_empty_room">Hetkel ei ole võimalik uuesti liituda tühja jututoaga.</string> + + <string name="encrypted_message">Krüptitud sõnum</string> + + <string name="medium_email">E-posti aadress</string> + <string name="medium_phone_number">Telefoninumber</string> + + <string name="reply_to_an_image">saatis pildi.</string> + <string name="reply_to_a_video">saatis video.</string> + <string name="reply_to_an_audio_file">saatis helifaili.</string> + <string name="reply_to_a_file">saatis faili.</string> + + <string name="room_displayname_invite_from">Kutse kasutajalt %s</string> + <string name="room_displayname_room_invite">Kutse jututuppa</string> + + <string name="room_displayname_two_members">%1$s ja %2$s</string> + + <plurals name="room_displayname_three_and_more_members"> + <item quantity="one">%1$s ja üks muu</item> + <item quantity="other">%1$s ja %2$d muud</item> + </plurals> + + <string name="room_displayname_empty_room">Tühi jututuba</string> + + + <string name="verification_emoji_dog">Koer</string> + <string name="verification_emoji_cat">Kass</string> + <string name="verification_emoji_lion">Lõvi</string> + <string name="verification_emoji_horse">Hobune</string> + <string name="verification_emoji_unicorn">Ükssarvik</string> + <string name="verification_emoji_pig">Siga</string> + <string name="verification_emoji_elephant">Elevant</string> + <string name="verification_emoji_rabbit">Jänes</string> + <string name="verification_emoji_panda">Panda</string> + <string name="verification_emoji_rooster">Kukk</string> + <string name="verification_emoji_penguin">Pingviin</string> + <string name="verification_emoji_turtle">Kilpkonn</string> + <string name="verification_emoji_fish">Kala</string> + <string name="verification_emoji_octopus">Kaheksajalg</string> + <string name="verification_emoji_butterfly">Liblikas</string> + <string name="verification_emoji_flower">Lill</string> + <string name="verification_emoji_tree">Puu</string> + <string name="verification_emoji_cactus">Kaktus</string> + <string name="verification_emoji_mushroom">Seen</string> + <string name="verification_emoji_globe">Maakera</string> + <string name="verification_emoji_moon">Kuu</string> + <string name="verification_emoji_cloud">Pilv</string> + <string name="verification_emoji_fire">Tuli</string> + <string name="verification_emoji_banana">Banaan</string> + <string name="verification_emoji_apple">Õun</string> + <string name="verification_emoji_strawberry">Maasikas</string> + <string name="verification_emoji_corn">Mais</string> + <string name="verification_emoji_pizza">Pitsa</string> + <string name="verification_emoji_cake">Kook</string> + <string name="verification_emoji_heart">Süda</string> + <string name="verification_emoji_smiley">Smaili</string> + <string name="verification_emoji_robot">Robot</string> + <string name="verification_emoji_hat">Müts</string> + <string name="verification_emoji_glasses">Prillid</string> + <string name="verification_emoji_wrench">Mutrivõti</string> + <string name="verification_emoji_santa">Jõuluvana</string> + <string name="verification_emoji_thumbsup">Pöidlad püsti</string> + <string name="verification_emoji_umbrella">Vihmavari</string> + <string name="verification_emoji_hourglass">Liivakell</string> + <string name="verification_emoji_clock">Kell</string> + <string name="verification_emoji_gift">Kingitus</string> + <string name="verification_emoji_lightbulb">Lambipirn</string> + <string name="verification_emoji_book">Raamat</string> + <string name="verification_emoji_pencil">Pliiats</string> + <string name="verification_emoji_paperclip">Kirjaklamber</string> + <string name="verification_emoji_scissors">Käärid</string> + <string name="verification_emoji_lock">Lukk</string> + <string name="verification_emoji_key">Võti</string> + <string name="verification_emoji_hammer">Haamer</string> + <string name="verification_emoji_telephone">Telefon</string> + <string name="verification_emoji_flag">Lipp</string> + <string name="verification_emoji_train">Rong</string> + <string name="verification_emoji_bicycle">Jalgratas</string> + <string name="verification_emoji_airplane">Lennuk</string> + <string name="verification_emoji_rocket">Rakett</string> + <string name="verification_emoji_trophy">Auhind</string> + <string name="verification_emoji_ball">Pall</string> + <string name="verification_emoji_guitar">Kitarr</string> + <string name="verification_emoji_trumpet">Trompet</string> + <string name="verification_emoji_bell">Kelluke</string> + <string name="verification_emoji_anchor">Ankur</string> + <string name="verification_emoji_headphone">Kõrvaklapid</string> + <string name="verification_emoji_folder">Kaust</string> + <string name="verification_emoji_pin">Knopka</string> + + <string name="initial_sync_start_importing_account">Alglaadimine: +\nImpordin kontot…</string> + <string name="initial_sync_start_importing_account_crypto">Alglaadimine: +\nImpordin krüptoseadistusi</string> + <string name="initial_sync_start_importing_account_rooms">Alglaadimine: +\nImpordin jututubasid</string> + <string name="initial_sync_start_importing_account_joined_rooms">Alglaadimine: +\nImpordin liitutud jututubasid</string> + <string name="initial_sync_start_importing_account_invited_rooms">Alglaadimine: +\nImpordin kutsutud jututubasid</string> + <string name="initial_sync_start_importing_account_left_rooms">Alglaadimine: +\nImpordin lahkutud jututubasid</string> + <string name="initial_sync_start_importing_account_groups">Alglaadimine: +\nImpordin kogukondi</string> + <string name="initial_sync_start_importing_account_data">Alglaadimine: +\nImpordin kontoandmeid</string> + + <string name="event_status_sending_message">Saadan sõnumit…</string> + <string name="clear_timeline_send_queue">Tühjenda saatmisjärjekord</string> + + <string name="notice_room_invite_no_invitee_with_reason">Kasutaja %1$s kutse. Põhjus: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s kutsus kasutajat %2$s. Põhjus: %3$s</string> + <string name="notice_room_invite_you_with_reason">%1$s kutsus sind. Põhjus: %2$s</string> + <string name="notice_room_join_with_reason">%1$s liitus jututoaga. Põhjus: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s lahkus jututoast. Põhjus: %2$s</string> + <string name="notice_room_reject_with_reason">%1$s lükkas kutse tagasi. Põhjus: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s müksas välja kasutaja %2$s. Põhjus: %3$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s saatis kasutajale %2$s kutse jututoaga liitumiseks. Põhjus: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s tühistas kasutajale %2$s saadetud kutse jututoaga liitumiseks. Põhjus: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s võttis vastu kutse %2$s jututoaga liitumiseks. Põhjus: %3$s</string> + <string name="notice_room_withdraw_with_reason">%1$s võttis tagasi kasutajale %2$s saadetud kutse. Põhjus: %3$s</string> + + <string name="notice_end_to_end_ok">%1$s lülitas sisse läbiva krüptimise.</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s).</string> + +</resources> 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 9487aa7db4..8dd87b6b6a 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -209,4 +209,6 @@ <string name="notice_end_to_end_ok">%1$s laittoi päälle osapuolten välisen salauksen.</string> <string name="notice_end_to_end_unknown_algorithm">%1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s).</string> + <string name="key_verification_request_fallback_message">%s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa.</string> + </resources> 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 e966f22064..521c805be8 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -4,8 +4,8 @@ <string name="summary_user_sent_image">%1$s dërgoi një figurë.</string> <string name="notice_room_invite">%1$s ftoi %2$s</string> <string name="notice_room_invite_you">%1$s ju ftoi</string> - <string name="notice_room_join">%1$s u bë pjesë</string> - <string name="notice_room_leave">%1$s iku</string> + <string name="notice_room_join">%1$s hyri në dhomë</string> + <string name="notice_room_leave">%1$s doli nga dhoma</string> <string name="notice_room_reject">%1$s hodhi tej ftesën</string> <string name="notice_room_kick">%1$s përzuri %2$s</string> <string name="notice_room_ban">%1$s dëboi %2$s</string> @@ -172,8 +172,8 @@ <string name="notice_room_invite_no_invitee_with_reason">Ftesë e %1$s. Arsye: %2$s</string> <string name="notice_room_invite_with_reason">%1$s ftoi %2$s. Arsye: %3$s</string> <string name="notice_room_invite_you_with_reason">%1$s ju ftoi. Arsye: %2$s</string> - <string name="notice_room_join_with_reason">%1$s erdhi. Arsye: %2$s</string> - <string name="notice_room_leave_with_reason">%1$s iku. Arsye: %2$s</string> + <string name="notice_room_join_with_reason">%1$s erdhi në dhomë. Arsye: %2$s</string> + <string name="notice_room_leave_with_reason">%1$s doli nga dhoma. Arsye: %2$s</string> <string name="notice_room_reject_with_reason">%1$s hodhi poshtë ftesën. Arsye: %2$s</string> <string name="notice_room_kick_with_reason">%1$s përzuri %2$s. Arsye: %3$s</string> <string name="notice_room_unban_with_reason">%1$s hoqi dëbimin për %2$s. Arsye: %3$s</string> diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index 38affc0599..cdd9c5eb8d 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -5,8 +5,8 @@ <string name="notice_room_invite_no_invitee">%s 的邀请</string> <string name="notice_room_invite">%1$s 邀请了 %2$s</string> <string name="notice_room_invite_you">%1$s 邀请了您</string> - <string name="notice_room_join">%1$s 加入了</string> - <string name="notice_room_leave">%1$s 离开了</string> + <string name="notice_room_join">%1$s 加入了聊天室</string> + <string name="notice_room_leave">%1$s 离开了聊天室</string> <string name="notice_room_reject">%1$s 拒绝了邀请</string> <string name="notice_room_kick">%1$s 移除了 %2$s</string> <string name="notice_room_unban">%1$s 解封了 %2$s</string> @@ -173,8 +173,8 @@ <string name="notice_room_invite_no_invitee_with_reason">%1$s 的邀请。理由:%2$s</string> <string name="notice_room_invite_with_reason">%1$s 邀请了 %2$s。理由:%3$s</string> <string name="notice_room_invite_you_with_reason">%1$s 邀请了您。理由:%2$s</string> - <string name="notice_room_join_with_reason">%1$s 已加入。理由:%2$s</string> - <string name="notice_room_leave_with_reason">%1$s 已离开。理由:%2$s</string> + <string name="notice_room_join_with_reason">%1$s 加入了聊天室。理由:%2$s</string> + <string name="notice_room_leave_with_reason">%1$s 离开了聊天室。理由:%2$s</string> <string name="notice_room_reject_with_reason">%1$s 已拒绝邀请。理由:%2$s</string> <string name="notice_room_kick_with_reason">%1$s 踢走了 %2$s。理由:%3$s</string> <string name="notice_room_unban_with_reason">%1$s 取消封锁了 %2$s。理由:%3$s</string> diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml deleted file mode 100644 index 6eb46fd7df..0000000000 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - - <!-- - - - - - - - - - - - - - - PLEASE DO NOT ADD NEW STRINGS HERE, THE FILE WILL BE DELETED, ONCE ALL PR WILL BE MERGED - - - - - - - - - - - - --> - -</resources> diff --git a/tools/import_from_riot.sh b/tools/import_from_riot.sh deleted file mode 100755 index a2b68a347c..0000000000 --- a/tools/import_from_riot.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash - -# -# Copyright 2018 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. -# - -# Exit on any error -set -e - -echo -echo "Copy strings to SDK" - -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values/strings.xml ./matrix-sdk-android/src/main/res/values/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ar/strings.xml ./matrix-sdk-android/src/main/res/values-ar/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-az/strings.xml ./matrix-sdk-android/src/main/res/values-az/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-bg/strings.xml ./matrix-sdk-android/src/main/res/values-bg/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-bs/strings.xml ./matrix-sdk-android/src/main/res/values-bs/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ca/strings.xml ./matrix-sdk-android/src/main/res/values-ca/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-cs/strings.xml ./matrix-sdk-android/src/main/res/values-cs/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./matrix-sdk-android/src/main/res/values-da/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rGB/strings.xml ./matrix-sdk-android/src/main/res/values-en-rGB/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fa/strings.xml ./matrix-sdk-android/src/main/res/values-fa/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fi/strings.xml ./matrix-sdk-android/src/main/res/values-fi/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-fr/strings.xml ./matrix-sdk-android/src/main/res/values-fr/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-gl/strings.xml ./matrix-sdk-android/src/main/res/values-gl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-hu/strings.xml ./matrix-sdk-android/src/main/res/values-hu/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-id/strings.xml ./matrix-sdk-android/src/main/res/values-id/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-in/strings.xml ./matrix-sdk-android/src/main/res/values-in/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-is/strings.xml ./matrix-sdk-android/src/main/res/values-is/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-it/strings.xml ./matrix-sdk-android/src/main/res/values-it/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ja/strings.xml ./matrix-sdk-android/src/main/res/values-ja/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ko/strings.xml ./matrix-sdk-android/src/main/res/values-ko/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-lv/strings.xml ./matrix-sdk-android/src/main/res/values-lv/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-nl/strings.xml ./matrix-sdk-android/src/main/res/values-nl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-nn/strings.xml ./matrix-sdk-android/src/main/res/values-nn/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pl/strings.xml ./matrix-sdk-android/src/main/res/values-pl/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pt/strings.xml ./matrix-sdk-android/src/main/res/values-pt/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-pt-rBR/strings.xml ./matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ru/strings.xml ./matrix-sdk-android/src/main/res/values-ru/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-sk/strings.xml ./matrix-sdk-android/src/main/res/values-sk/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-sq/strings.xml ./matrix-sdk-android/src/main/res/values-sq/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-te/strings.xml ./matrix-sdk-android/src/main/res/values-te/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-th/strings.xml ./matrix-sdk-android/src/main/res/values-th/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-uk/strings.xml ./matrix-sdk-android/src/main/res/values-uk/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-vls/strings.xml ./matrix-sdk-android/src/main/res/values-vls/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rCN/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml -cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rTW/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml - -echo -echo "Copy strings to RiotX" - -cp ../riot-android/vector/src/main/res/values/strings.xml ./vector/src/main/res/values/strings.xml -cp ../riot-android/vector/src/main/res/values-ar/strings.xml ./vector/src/main/res/values-ar/strings.xml -cp ../riot-android/vector/src/main/res/values-az/strings.xml ./vector/src/main/res/values-az/strings.xml -cp ../riot-android/vector/src/main/res/values-b+sr+Latn/strings.xml ./vector/src/main/res/values-b+sr+Latn/strings.xml -cp ../riot-android/vector/src/main/res/values-bg/strings.xml ./vector/src/main/res/values-bg/strings.xml -cp ../riot-android/vector/src/main/res/values-bn-rIN/strings.xml ./vector/src/main/res/values-bn-rIN/strings.xml -cp ../riot-android/vector/src/main/res/values-bs/strings.xml ./vector/src/main/res/values-bs/strings.xml -cp ../riot-android/vector/src/main/res/values-ca/strings.xml ./vector/src/main/res/values-ca/strings.xml -cp ../riot-android/vector/src/main/res/values-cs/strings.xml ./vector/src/main/res/values-cs/strings.xml -cp ../riot-android/vector/src/main/res/values-cy/strings.xml ./vector/src/main/res/values-cy/strings.xml -cp ../riot-android/vector/src/main/res/values-da/strings.xml ./vector/src/main/res/values-da/strings.xml -cp ../riot-android/vector/src/main/res/values-de/strings.xml ./vector/src/main/res/values-de/strings.xml -cp ../riot-android/vector/src/main/res/values-el/strings.xml ./vector/src/main/res/values-el/strings.xml -cp ../riot-android/vector/src/main/res/values-eo/strings.xml ./vector/src/main/res/values-eo/strings.xml -cp ../riot-android/vector/src/main/res/values-es/strings.xml ./vector/src/main/res/values-es/strings.xml -cp ../riot-android/vector/src/main/res/values-es-rMX/strings.xml ./vector/src/main/res/values-es-rMX/strings.xml -cp ../riot-android/vector/src/main/res/values-eu/strings.xml ./vector/src/main/res/values-eu/strings.xml -cp ../riot-android/vector/src/main/res/values-fa/strings.xml ./vector/src/main/res/values-fa/strings.xml -cp ../riot-android/vector/src/main/res/values-fi/strings.xml ./vector/src/main/res/values-fi/strings.xml -cp ../riot-android/vector/src/main/res/values-fy/strings.xml ./vector/src/main/res/values-fy/strings.xml -cp ../riot-android/vector/src/main/res/values-fr/strings.xml ./vector/src/main/res/values-fr/strings.xml -cp ../riot-android/vector/src/main/res/values-fr-rCA/strings.xml ./vector/src/main/res/values-fr-rCA/strings.xml -cp ../riot-android/vector/src/main/res/values-gl/strings.xml ./vector/src/main/res/values-gl/strings.xml -cp ../riot-android/vector/src/main/res/values-hu/strings.xml ./vector/src/main/res/values-hu/strings.xml -cp ../riot-android/vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-id/strings.xml -cp ../riot-android/vector/src/main/res/values-in/strings.xml ./vector/src/main/res/values-in/strings.xml -cp ../riot-android/vector/src/main/res/values-is/strings.xml ./vector/src/main/res/values-is/strings.xml -cp ../riot-android/vector/src/main/res/values-it/strings.xml ./vector/src/main/res/values-it/strings.xml -cp ../riot-android/vector/src/main/res/values-ja/strings.xml ./vector/src/main/res/values-ja/strings.xml -cp ../riot-android/vector/src/main/res/values-ko/strings.xml ./vector/src/main/res/values-ko/strings.xml -cp ../riot-android/vector/src/main/res/values-lv/strings.xml ./vector/src/main/res/values-lv/strings.xml -cp ../riot-android/vector/src/main/res/values-nb-rNO/strings.xml ./vector/src/main/res/values-nb-rNO/strings.xml -cp ../riot-android/vector/src/main/res/values-nl/strings.xml ./vector/src/main/res/values-nl/strings.xml -cp ../riot-android/vector/src/main/res/values-nn/strings.xml ./vector/src/main/res/values-nn/strings.xml -cp ../riot-android/vector/src/main/res/values-pl/strings.xml ./vector/src/main/res/values-pl/strings.xml -cp ../riot-android/vector/src/main/res/values-pt/strings.xml ./vector/src/main/res/values-pt/strings.xml -cp ../riot-android/vector/src/main/res/values-pt-rBR/strings.xml ./vector/src/main/res/values-pt-rBR/strings.xml -cp ../riot-android/vector/src/main/res/values-ro/strings.xml ./vector/src/main/res/values-ro/strings.xml -cp ../riot-android/vector/src/main/res/values-ru/strings.xml ./vector/src/main/res/values-ru/strings.xml -cp ../riot-android/vector/src/main/res/values-sk/strings.xml ./vector/src/main/res/values-sk/strings.xml -cp ../riot-android/vector/src/main/res/values-sq/strings.xml ./vector/src/main/res/values-sq/strings.xml -cp ../riot-android/vector/src/main/res/values-sr/strings.xml ./vector/src/main/res/values-sr/strings.xml -cp ../riot-android/vector/src/main/res/values-te/strings.xml ./vector/src/main/res/values-te/strings.xml -cp ../riot-android/vector/src/main/res/values-th/strings.xml ./vector/src/main/res/values-th/strings.xml -cp ../riot-android/vector/src/main/res/values-tlh/strings.xml ./vector/src/main/res/values-tlh/strings.xml -cp ../riot-android/vector/src/main/res/values-tr/strings.xml ./vector/src/main/res/values-tr/strings.xml -cp ../riot-android/vector/src/main/res/values-uk/strings.xml ./vector/src/main/res/values-uk/strings.xml -cp ../riot-android/vector/src/main/res/values-vls/strings.xml ./vector/src/main/res/values-vls/strings.xml -cp ../riot-android/vector/src/main/res/values-zh-rCN/strings.xml ./vector/src/main/res/values-zh-rCN/strings.xml -cp ../riot-android/vector/src/main/res/values-zh-rTW/strings.xml ./vector/src/main/res/values-zh-rTW/strings.xml - -echo -echo "Success!" diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl index f4090b40e6..1d2ec0a069 100644 --- a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl +++ b/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl @@ -6,6 +6,7 @@ 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.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel <#if createViewEvents> @@ -38,7 +39,8 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi } override fun handle(action: ${actionClass}) { - //TODO - } + when (action) { + }.exhaustive + } } diff --git a/vector/build.gradle b/vector/build.gradle index 459b297fd6..74fc96a425 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 20 +ext.versionMinor = 21 ext.versionPatch = 0 static def getGitTimestamp() { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index ae0ffa1f91..7c2939707f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -161,6 +161,8 @@ android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> + <activity android:name=".features.terms.ReviewTermsActivity" /> + <!-- Services --> <service diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index b7dd06bca4..b776a450a6 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -41,6 +41,8 @@ import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeF import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQRWaitingFragment import im.vector.riotx.features.crypto.verification.qrconfirmation.VerificationQrScannedByOtherFragment import im.vector.riotx.features.crypto.verification.request.VerificationRequestFragment +import im.vector.riotx.features.discovery.DiscoverySettingsFragment +import im.vector.riotx.features.discovery.change.SetIdentityServerFragment import im.vector.riotx.features.grouplist.GroupListFragment import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment @@ -61,8 +63,6 @@ import im.vector.riotx.features.login.LoginSplashFragment import im.vector.riotx.features.login.LoginWaitForEmailFragment import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.terms.LoginTermsFragment -import im.vector.riotx.features.userdirectory.KnownUsersFragment -import im.vector.riotx.features.userdirectory.UserDirectoryFragment import im.vector.riotx.features.qrcode.QrCodeScannerFragment import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment @@ -92,9 +92,13 @@ import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment import im.vector.riotx.features.settings.devtools.KeyRequestsFragment import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment +import im.vector.riotx.features.settings.locale.LocalePickerFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareFragment import im.vector.riotx.features.signout.soft.SoftLogoutFragment +import im.vector.riotx.features.terms.ReviewTermsFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.UserDirectoryFragment import im.vector.riotx.features.widgets.RoomWidgetFragment @Module @@ -110,6 +114,11 @@ interface FragmentModule { @FragmentKey(RoomListFragment::class) fun bindRoomListFragment(fragment: RoomListFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LocalePickerFragment::class) + fun bindLocalePickerFragment(fragment: LocalePickerFragment): Fragment + @Binds @IntoMap @FragmentKey(GroupListFragment::class) @@ -474,4 +483,19 @@ interface FragmentModule { @IntoMap @FragmentKey(RoomWidgetFragment::class) fun bindWidgetFragment(fragment: RoomWidgetFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SetIdentityServerFragment::class) + fun bindSetIdentityServerFragment(fragment: SetIdentityServerFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(DiscoverySettingsFragment::class) + fun bindDiscoverySettingsFragment(fragment: DiscoverySettingsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(ReviewTermsFragment::class) + fun bindReviewTermsFragment(fragment: ReviewTermsFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index c38c0c99e6..7d61c13b83 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -61,6 +61,7 @@ import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity +import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.ui.UiStateRepository @Component( @@ -118,6 +119,7 @@ interface ScreenComponent { fun inject(activity: SharedSecureStorageActivity) fun inject(activity: BigImageViewerActivity) fun inject(activity: InviteUsersToRoomActivity) + fun inject(activity: ReviewTermsActivity) /* ========================================================================================== * BottomSheets diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 6f864c7f5b..5c5052cf2b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -130,9 +130,9 @@ interface VectorComponent { fun emojiDataSource(): EmojiDataSource - fun alertManager() : PopupAlertManager + fun alertManager(): PopupAlertManager - fun reAuthHelper() : ReAuthHelper + fun reAuthHelper(): ReAuthHelper @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 8046f67668..a214073104 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -26,6 +26,7 @@ import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromK import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel +import im.vector.riotx.features.discovery.DiscoverySharedViewModel import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel @@ -118,4 +119,9 @@ interface ViewModelModule { @IntoMap @ViewModelKey(RoomProfileSharedActionViewModel::class) fun bindRoomProfileSharedActionViewModel(viewModel: RoomProfileSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(DiscoverySharedViewModel::class) + fun bindDiscoverySharedViewModel(viewModel: DiscoverySharedViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonStyle.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonStyle.kt new file mode 100644 index 0000000000..d6cc90c0e9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonStyle.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.riotx.core.epoxy.attributes + +enum class ButtonStyle { + POSITIVE, + DESTRUCTIVE +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonType.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonType.kt new file mode 100644 index 0000000000..b3cbe9cd84 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/ButtonType.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.riotx.core.epoxy.attributes + +enum class ButtonType { + NO_BUTTON, + NORMAL, + SWITCH +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/IconMode.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/IconMode.kt new file mode 100644 index 0000000000..c6b1ebf505 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/attributes/IconMode.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.riotx.core.epoxy.attributes + +enum class IconMode { + NONE, + INFO, + ERROR +} diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 557bc93bb1..57e5c6381f 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -19,6 +19,7 @@ package im.vector.riotx.core.error import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isInvalidPassword +import im.vector.matrix.android.api.session.identity.IdentityServiceError import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import java.net.HttpURLConnection @@ -37,6 +38,7 @@ class DefaultErrorFormatter @Inject constructor( override fun toHumanReadable(throwable: Throwable?): String { return when (throwable) { null -> null + is IdentityServiceError -> identityServerError(throwable) is Failure.NetworkConnection -> { when { throwable.ioException is SocketTimeoutException -> @@ -107,4 +109,16 @@ class DefaultErrorFormatter @Inject constructor( stringProvider.getQuantityString(R.plurals.login_error_limit_exceeded_retry_after, delaySeconds, delaySeconds) } } + + private fun identityServerError(identityServiceError: IdentityServiceError): String { + return stringProvider.getString(when (identityServiceError) { + IdentityServiceError.OutdatedIdentityServer -> R.string.identity_server_error_outdated_identity_server + IdentityServiceError.OutdatedHomeServer -> R.string.identity_server_error_outdated_home_server + IdentityServiceError.NoIdentityServerConfigured -> R.string.identity_server_error_no_identity_server_configured + IdentityServiceError.TermsNotSignedException -> R.string.identity_server_error_terms_not_signed + IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported + IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error + IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error + }) + } } diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt index f9f5d3b3d2..b74f143e17 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Activity.kt @@ -16,6 +16,7 @@ package im.vector.riotx.core.extensions +import android.app.Activity import android.os.Parcelable import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction @@ -59,3 +60,8 @@ fun <T : Fragment> VectorBaseActivity.addFragmentToBackstack(frameId: Int, fun VectorBaseActivity.hideKeyboard() { currentFocus?.hideKeyboard() } + +fun Activity.restart() { + startActivity(intent) + finish() +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index e8e8f21259..58ec4b22c6 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity.* import javax.inject.Inject @@ -108,15 +107,4 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { } super.onBackPressed() } - - protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { - viewEvents - .observe() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - hideWaitingView() - observer(it) - } - .disposeOnDestroy() - } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 08cf8e57e1..770a63a3fa 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -70,6 +70,7 @@ import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.receivers.DebugReceiver +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import timber.log.Timber @@ -94,6 +95,18 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { protected val viewModelProvider get() = ViewModelProvider(this, viewModelFactory) + // TODO Other Activity should use this also + protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + hideWaitingView() + observer(it) + } + .disposeOnDestroy() + } + /* ========================================================================================== * DATA * ========================================================================================== */ @@ -179,7 +192,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } }) - sessionListener = getVectorComponent().sessionListener() + sessionListener = vectorComponent.sessionListener() sessionListener.globalErrorLiveData.observeEvent(this) { handleGlobalError(it) } @@ -217,8 +230,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { handleInvalidToken(globalError) is GlobalError.ConsentNotGivenError -> consentNotGivenHelper.displayDialog(globalError.consentUri, - activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host - ?: "") + activeSessionHolder.getActiveSession().sessionParams.homeServerHost ?: "") } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index 6eb316456a..c4dcb0d996 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -39,6 +39,7 @@ import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.MvRx import com.bumptech.glide.util.Util.assertMainThread import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasScreenInjector @@ -49,6 +50,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import timber.log.Timber +import java.util.concurrent.TimeUnit abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { @@ -249,6 +251,18 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { .disposeOnDestroyView() } + /* ========================================================================================== + * Views + * ========================================================================================== */ + + protected fun View.debouncedClicks(onClicked: () -> Unit) { + clicks() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { onClicked() } + .disposeOnDestroyView() + } + /* ========================================================================================== * MENU MANAGEMENT * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index e82e8b3856..11cd9c485e 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -30,7 +30,7 @@ import io.reactivex.Single abstract class VectorViewModel<S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents>(initialState: S) : BaseMvRxViewModel<S>(initialState, false) { - interface Factory<S: MvRxState> { + interface Factory<S : MvRxState> { fun create(state: S): BaseMvRxViewModel<S> } diff --git a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt index aa1fbaca54..69367e529c 100644 --- a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt @@ -45,7 +45,7 @@ class PushersManager @Inject constructor( profileTag, localeProvider.current().language, appNameProvider.getAppName(), - currentSession.sessionParams.credentials.deviceId ?: "MOBILE", + currentSession.sessionParams.deviceId ?: "MOBILE", stringProvider.getString(R.string.pusher_http_url), append = false, withEventIdOnly = true diff --git a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt index ac379a8f98..fa4b09ed4c 100644 --- a/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/riotx/core/resources/UserPreferencesProvider.kt @@ -29,6 +29,10 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: return vectorPreferences.showReadReceipts() } + fun shouldShowRedactedMessages(): Boolean { + return vectorPreferences.showRedactedMessages() + } + fun shouldShowLongClickOnRoomHelp(): Boolean { return vectorPreferences.shouldShowLongClickOnRoomHelp() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index d82134caf5..9e5af038ef 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -33,9 +33,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import im.vector.riotx.R import im.vector.riotx.features.notifications.NotificationUtils -import im.vector.riotx.features.settings.VectorLocale -import timber.log.Timber -import java.util.Locale /** * Tells if the application ignores battery optimizations. @@ -53,6 +50,10 @@ fun isIgnoringBatteryOptimizations(context: Context): Boolean { || (context.getSystemService(Context.POWER_SERVICE) as PowerManager?)?.isIgnoringBatteryOptimizations(context.packageName) == true } +fun isAirplaneModeOn(context: Context): Boolean { + return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 +} + /** * display the system dialog for granting this permission. If previously granted, the * system will not show it (so you should call this method). @@ -90,24 +91,6 @@ fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = t } } -/** - * Provides the device locale - * - * @return the device locale - */ -fun getDeviceLocale(context: Context): Locale { - return try { - val packageManager = context.packageManager - val resources = packageManager.getResourcesForApplication("android") - @Suppress("DEPRECATION") - resources.configuration.locale - } catch (e: Exception) { - Timber.e(e, "## getDeviceLocale() failed") - // Fallback to application locale - VectorLocale.applicationLocale - } -} - /** * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings diff --git a/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt index 3de555f66e..0361fc9d71 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/UrlUtils.kt @@ -26,3 +26,14 @@ fun String.isValidUrl(): Boolean { false } } + +/** + * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + */ +internal fun String.ensureProtocol(): String { + return when { + isEmpty() -> this + !startsWith("http") -> "https://$this" + else -> this + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt index a4b7ca263d..2ef69890ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt +++ b/vector/src/main/java/im/vector/riotx/features/configuration/VectorConfiguration.kt @@ -30,62 +30,30 @@ import javax.inject.Inject /** * Handle locale configuration change, such as theme, font size and locale chosen by the user */ - class VectorConfiguration @Inject constructor(private val context: Context) { - // TODO Import mLanguageReceiver From Riot? fun onConfigurationChanged() { if (Locale.getDefault().toString() != VectorLocale.applicationLocale.toString()) { Timber.v("## onConfigurationChanged(): the locale has been updated to ${Locale.getDefault()}") Timber.v("## onConfigurationChanged(): restore the expected value ${VectorLocale.applicationLocale}") - updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScalePrefValue(context), - ThemeUtils.getApplicationTheme(context)) + Locale.setDefault(VectorLocale.applicationLocale) } } - private fun updateApplicationSettings(locale: Locale, textSize: String, theme: String) { - VectorLocale.saveApplicationLocale(context, locale) - FontScale.saveFontScale(context, textSize) - Locale.setDefault(locale) - - val config = Configuration(context.resources.configuration) - @Suppress("DEPRECATION") - config.locale = locale - config.fontScale = FontScale.getFontScale(context) - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - - ThemeUtils.setApplicationTheme(context, theme) - // TODO PhoneNumberUtils.onLocaleUpdate() - } - - /** - * Update the application theme - * - * @param theme the new theme - */ - fun updateApplicationTheme(theme: String) { - ThemeUtils.setApplicationTheme(context, theme) - updateApplicationSettings(VectorLocale.applicationLocale, - FontScale.getFontScalePrefValue(context), - theme) - } - /** * Init the configuration from the saved one */ fun initConfiguration() { VectorLocale.init(context) val locale = VectorLocale.applicationLocale - val fontScale = FontScale.getFontScale(context) + val fontScale = FontScale.getFontScaleValue(context) val theme = ThemeUtils.getApplicationTheme(context) Locale.setDefault(locale) val config = Configuration(context.resources.configuration) @Suppress("DEPRECATION") config.locale = locale - config.fontScale = fontScale + config.fontScale = fontScale.scale @Suppress("DEPRECATION") context.resources.updateConfiguration(config, context.resources.displayMetrics) @@ -93,16 +61,6 @@ class VectorConfiguration @Inject constructor(private val context: Context) { ThemeUtils.setApplicationTheme(context, theme) } - /** - * Update the application locale - * - * @param locale - */ - // TODO Call from LanguagePickerActivity - fun updateApplicationLocale(locale: Locale) { - updateApplicationSettings(locale, FontScale.getFontScalePrefValue(context), ThemeUtils.getApplicationTheme(context)) - } - /** * Compute a localised context * @@ -115,7 +73,7 @@ class VectorConfiguration @Inject constructor(private val context: Context) { val resources = context.resources val locale = VectorLocale.applicationLocale val configuration = resources.configuration - configuration.fontScale = FontScale.getFontScale(context) + configuration.fontScale = FontScale.getFontScaleValue(context).scale if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { configuration.setLocale(locale) @@ -142,10 +100,9 @@ class VectorConfiguration @Inject constructor(private val context: Context) { * Compute the locale status value * @return the local status value */ - // TODO Create data class for this fun getHash(): String { return (VectorLocale.applicationLocale.toString() - + "_" + FontScale.getFontScalePrefValue(context) + + "_" + FontScale.getFontScaleValue(context).preferenceValue + "_" + ThemeUtils.getApplicationTheme(context)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt index c8406570d3..faada7ba3e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt @@ -51,7 +51,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor( try { sharedViewModel.recoverUsingBackupPass(recoveryKey) } catch (failure: Throwable) { - recoveryCodeErrorText.value = stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt) + recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt index 1fec404f7d..20046fa115 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewController.kt @@ -197,7 +197,7 @@ class KeysBackupSettingsRecyclerViewController @Inject constructor(private val s endIconResourceId(R.drawable.e2e_warning) } else { if (isSignatureValid) { - if (session.sessionParams.credentials.deviceId == it.deviceId) { + if (session.sessionParams.deviceId == it.deviceId) { description(stringProvider.getString(R.string.keys_backup_settings_valid_signature_from_this_device)) endIconResourceId(R.drawable.e2e_verified) } else { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index 3522c5a752..93d6f43763 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -164,16 +164,16 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() @OnClick(R.id.keys_backup_setup_step2_button) fun doNext() { when { - viewModel.passphrase.value.isNullOrEmpty() -> { + viewModel.passphrase.value.isNullOrEmpty() -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) } viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match) } - viewModel.passwordStrength.value?.score ?: 0 < 4 -> { + viewModel.passwordStrength.value?.score ?: 0 < 4 -> { viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak) } - else -> { + else -> { viewModel.megolmBackupCreationInfo = null viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value) @@ -190,7 +190,7 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() viewModel.prepareRecoveryKey(activity!!, null) } - else -> { + else -> { // User has entered a passphrase but want to skip this step. viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt index 9175d6c081..1478b99d3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -123,7 +123,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() .joinToString(" ") } - it.setOnClickListener { + it.debouncedClicks { copyToClipboard(activity!!, recoveryKey) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index db4c5230fd..848166381e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.inputmethod.EditorInfo import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis @@ -33,7 +32,6 @@ import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.startImportTextFromFileIntent import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* -import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.* import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -67,13 +65,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( } .disposeOnDestroyView() - ssss_key_use_file.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - startImportTextFromFileIntent(this, IMPORT_FILE_REQ) - } - .disposeOnDestroyView() + ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } sharedViewModel.observeViewEvents { when (it) { @@ -83,13 +75,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor( } } - ssss_key_submit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_key_submit.debouncedClicks { submit() } } fun submit() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 82d27aea1b..f5eb450fe1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -37,7 +36,7 @@ import javax.inject.Inject class SharedSecuredStoragePassphraseFragment @Inject constructor( private val colorProvider: ColorProvider -): VectorBaseFragment() { +) : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase @@ -83,29 +82,9 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( } } - ssss_passphrase_submit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() - - ssss_passphrase_use_key.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(SharedSecureStorageAction.UseKey) - } - .disposeOnDestroyView() - - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) - } - .disposeOnDestroyView() + ssss_passphrase_submit.debouncedClicks { submit() } + ssss_passphrase_use_key.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.UseKey) } + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) } } fun submit() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt index abe6e54092..fcedd2926e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -75,21 +74,8 @@ class BootstrapAccountPasswordFragment @Inject constructor( } .disposeOnDestroyView() - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapPasswordButton.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapPasswordButton.debouncedClicks { submit() } withState(sharedViewModel) { state -> (state.step as? BootstrapStep.AccountPassword)?.failure?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt index d84283b14c..fd7ba2dbb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -21,14 +21,11 @@ import android.view.View import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.colorizeMatchingText -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_conclusion.* -import java.util.concurrent.TimeUnit import javax.inject.Inject class BootstrapConclusionFragment @Inject constructor( @@ -42,13 +39,7 @@ class BootstrapConclusionFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bootstrapConclusionContinue.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.Completed) - } - .disposeOnDestroyView() + bootstrapConclusionContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.Completed) } } override fun invalidate() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt index d09eafee58..df4d741bf1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -23,7 +23,6 @@ import androidx.core.text.toSpannable import androidx.core.view.isGone import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -88,21 +87,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( // } } - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapSubmit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt index 952b0e5d03..d1eee9ff3f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -22,7 +22,6 @@ import android.view.inputmethod.EditorInfo import androidx.core.text.toSpannable import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R @@ -83,21 +82,8 @@ class BootstrapEnterPassphraseFragment @Inject constructor( // } } - ssss_view_show_password.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapSubmit.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt index f1847e5ab5..caf43721a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -28,7 +28,6 @@ import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import com.jakewharton.rxbinding3.widget.editorActionEvents import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.extensions.tryThis @@ -80,37 +79,10 @@ class BootstrapMigrateBackupFragment @Inject constructor( .disposeOnDestroyView() // sharedViewModel.observeViewEvents {} - bootstrapMigrateContinueButton.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - submit() - } - .disposeOnDestroyView() - - bootstrapMigrateShowPassword.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) - } - .disposeOnDestroyView() - - bootstrapMigrateForgotPassphrase.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) - } - .disposeOnDestroyView() - - bootstrapMigrateUseFile.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - startImportTextFromFileIntent(this, IMPORT_FILE_REQ) - } - .disposeOnDestroyView() + bootstrapMigrateContinueButton.debouncedClicks { submit() } + bootstrapMigrateShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapMigrateForgotPassphrase.debouncedClicks { sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) } + bootstrapMigrateUseFile.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } } private fun submit() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 05c6f7a53f..4faa4168b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -25,19 +25,16 @@ import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.view.clicks import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.colorizeMatchingText import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.core.utils.toast -import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.util.concurrent.TimeUnit import javax.inject.Inject class BootstrapSaveRecoveryKeyFragment @Inject constructor( @@ -51,34 +48,17 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase)) + val messageKey = getString(R.string.message_key) + val recoveryPassphrase = getString(R.string.recovery_passphrase) + val color = colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_link_text_color) + bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, messageKey, recoveryPassphrase) .toSpannable() - .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(messageKey, color) + .colorizeMatchingText(recoveryPassphrase, color) - recoverySave.clickableView.clicks() - .debounce(600, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - downloadRecoveryKey() - } - .disposeOnDestroyView() - - recoveryCopy.clickableView.clicks() - .debounce(600, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - shareRecoveryKey() - } - .disposeOnDestroyView() - - recoveryContinue.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - sharedViewModel.handle(BootstrapActions.GoToCompleted) - } - .disposeOnDestroyView() + recoverySave.clickableView.debouncedClicks { downloadRecoveryKey() } + recoveryCopy.clickableView.debouncedClicks { shareRecoveryKey() } + recoveryContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.GoToCompleted) } } private fun downloadRecoveryKey() = withState(sharedViewModel) { _ -> diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 7a003c3722..dce33255ce 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -367,7 +367,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( ) if (trustResult.isVerified()) { // Sign this device and upload the signature - session.sessionParams.credentials.deviceId?.let { deviceId -> + session.sessionParams.deviceId?.let { deviceId -> session.cryptoService() .crossSigningService().trustDevice(deviceId, object : MatrixCallback<Unit> { override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.kt new file mode 100644 index 0000000000..57b23d26d2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsAction.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.riotx.features.discovery + +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class DiscoverySettingsAction : VectorViewModelAction { + object RetrieveBinding : DiscoverySettingsAction() + object Refresh : DiscoverySettingsAction() + + object DisconnectIdentityServer : DiscoverySettingsAction() + data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() + data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() + data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() + data class SubmitMsisdnToken(val threePid: ThreePid.Msisdn, val code: String) : DiscoverySettingsAction() + data class CancelBinding(val threePid: ThreePid) : DiscoverySettingsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt new file mode 100644 index 0000000000..f92cb1a8bb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsController.kt @@ -0,0 +1,377 @@ +/* + * 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.riotx.features.discovery + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.attributes.ButtonStyle +import im.vector.riotx.core.epoxy.attributes.ButtonType +import im.vector.riotx.core.epoxy.attributes.IconMode +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import timber.log.Timber +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +class DiscoverySettingsController @Inject constructor( + private val colorProvider: ColorProvider, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController<DiscoverySettingsState>() { + + var listener: Listener? = null + + private val codes = mutableMapOf<ThreePid, String>() + + override fun buildModels(data: DiscoverySettingsState) { + when (data.identityServer) { + is Loading -> { + loadingItem { + id("identityServerLoading") + } + } + is Fail -> { + settingsInfoItem { + id("identityServerError") + helperText(data.identityServer.error.message) + } + } + is Success -> { + buildIdentityServerSection(data) + val hasIdentityServer = data.identityServer().isNullOrBlank().not() + if (hasIdentityServer && !data.termsNotSigned) { + buildEmailsSection(data.emailList) + buildMsisdnSection(data.phoneNumbersList) + } + } + } + } + + private fun buildIdentityServerSection(data: DiscoverySettingsState) { + val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) + + settingsSectionTitleItem { + id("idServerTitle") + titleResId(R.string.identity_server) + } + + settingsItem { + id("idServer") + title(identityServer) + } + + if (data.identityServer() != null && data.termsNotSigned) { + settingsInfoItem { + id("idServerFooter") + helperText(stringProvider.getString(R.string.settings_agree_to_terms, identityServer)) + showCompoundDrawable(true) + itemClickListener(View.OnClickListener { listener?.openIdentityServerTerms() }) + } + settingsButtonItem { + id("seeTerms") + colorProvider(colorProvider) + buttonTitle(stringProvider.getString(R.string.open_terms_of, identityServer)) + buttonClickListener { listener?.openIdentityServerTerms() } + } + } else { + settingsInfoItem { + id("idServerFooter") + showCompoundDrawable(false) + if (data.identityServer() != null) { + helperText(stringProvider.getString(R.string.settings_discovery_identity_server_info, identityServer)) + } else { + helperTextResId(R.string.settings_discovery_identity_server_info_none) + } + } + } + + settingsButtonItem { + id("change") + colorProvider(colorProvider) + if (data.identityServer() == null) { + buttonTitleId(R.string.add_identity_server) + } else { + buttonTitleId(R.string.change_identity_server) + } + buttonClickListener { listener?.onTapChangeIdentityServer() } + } + + if (data.identityServer() != null) { + settingsInfoItem { + id("removeInfo") + helperTextResId(R.string.settings_discovery_disconnect_identity_server_info) + } + settingsButtonItem { + id("remove") + colorProvider(colorProvider) + buttonTitleId(R.string.disconnect_identity_server) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonClickListener { listener?.onTapDisconnectIdentityServer() } + } + } + } + + private fun buildEmailsSection(emails: Async<List<PidInfo>>) { + settingsSectionTitleItem { + id("emails") + titleResId(R.string.settings_discovery_emails_title) + } + when (emails) { + is Incomplete -> { + loadingItem { + id("emailsLoading") + } + } + is Fail -> { + settingsInfoItem { + id("emailsError") + helperText(emails.error.message) + } + } + is Success -> { + if (emails().isEmpty()) { + settingsInfoItem { + id("emailsEmpty") + helperText(stringProvider.getString(R.string.settings_discovery_no_mails)) + } + } else { + emails().forEach { buildEmail(it) } + } + } + } + } + + private fun buildEmail(pidInfo: PidInfo) { + buildThreePid(pidInfo) + + if (pidInfo.isShared is Fail) { + buildSharedFail(pidInfo) + } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { + when (pidInfo.finalRequest) { + is Uninitialized, + is Loading -> + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + message(stringProvider.getString(R.string.settings_discovery_confirm_mail, pidInfo.threePid.value)) + } + is Fail -> + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + message(stringProvider.getString(R.string.settings_discovery_confirm_mail_not_clicked, pidInfo.threePid.value)) + textColorId(R.color.riotx_destructive_accent) + } + is Success -> Unit /* Cannot happen */ + } + when (pidInfo.finalRequest) { + is Uninitialized, + is Fail -> + buildContinueCancel(pidInfo.threePid) + is Loading -> + settingsProgressItem { + id("progress${pidInfo.threePid.value}") + } + is Success -> Unit /* Cannot happen */ + } + } + } + + private fun buildMsisdnSection(msisdns: Async<List<PidInfo>>) { + settingsSectionTitleItem { + id("msisdn") + titleResId(R.string.settings_discovery_msisdn_title) + } + + when (msisdns) { + is Incomplete -> { + loadingItem { + id("msisdnLoading") + } + } + is Fail -> { + settingsInfoItem { + id("msisdnListError") + helperText(msisdns.error.message) + } + } + is Success -> { + if (msisdns().isEmpty()) { + settingsInfoItem { + id("no_msisdn") + helperText(stringProvider.getString(R.string.settings_discovery_no_msisdn)) + } + } else { + msisdns().forEach { buildMsisdn(it) } + } + } + } + } + + private fun buildMsisdn(pidInfo: PidInfo) { + val phoneNumber = try { + PhoneNumberUtil.getInstance().parse("+${pidInfo.threePid.value}", null) + } catch (t: Throwable) { + Timber.e(t, "Unable to parse the phone number") + null + } + ?.let { + PhoneNumberUtil.getInstance().format(it, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + } + ?: pidInfo.threePid.value + + buildThreePid(pidInfo, phoneNumber) + + if (pidInfo.isShared is Fail) { + buildSharedFail(pidInfo) + } else if (pidInfo.isShared() == SharedState.BINDING_IN_PROGRESS) { + val errorText = if (pidInfo.finalRequest is Fail) { + val error = pidInfo.finalRequest.error + // Deal with error 500 + // Ref: https://github.com/matrix-org/sydent/issues/292 + if (error is Failure.ServerError + && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) { + stringProvider.getString(R.string.settings_text_message_sent_wrong_code) + } else { + errorFormatter.toHumanReadable(error) + } + } else { + null + } + settingsEditTextItem { + id("msisdnVerification${pidInfo.threePid.value}") + descriptionText(stringProvider.getString(R.string.settings_text_message_sent, phoneNumber)) + errorText(errorText) + inProgress(pidInfo.finalRequest is Loading) + interactionListener(object : SettingsEditTextItem.Listener { + override fun onValidate() { + val code = codes[pidInfo.threePid] + if (pidInfo.threePid is ThreePid.Msisdn && code != null) { + listener?.sendMsisdnVerificationCode(pidInfo.threePid, code) + } + } + + override fun onCodeChange(code: String) { + codes[pidInfo.threePid] = code + } + }) + } + buildContinueCancel(pidInfo.threePid) + } + } + + private fun buildThreePid(pidInfo: PidInfo, title: String = pidInfo.threePid.value) { + settingsTextButtonSingleLineItem { + id(pidInfo.threePid.value) + title(title) + colorProvider(colorProvider) + stringProvider(stringProvider) + when (pidInfo.isShared) { + is Loading -> { + buttonIndeterminate(true) + } + is Fail -> { + buttonType(ButtonType.NORMAL) + buttonStyle(ButtonStyle.DESTRUCTIVE) + buttonTitle(stringProvider.getString(R.string.global_retry)) + iconMode(IconMode.ERROR) + buttonClickListener { listener?.onTapRetryToRetrieveBindings() } + } + is Success -> when (pidInfo.isShared()) { + SharedState.SHARED, + SharedState.NOT_SHARED -> { + buttonType(ButtonType.SWITCH) + checked(pidInfo.isShared() == SharedState.SHARED) + switchChangeListener { _, checked -> + if (checked) { + listener?.onTapShare(pidInfo.threePid) + } else { + listener?.onTapRevoke(pidInfo.threePid) + } + } + } + SharedState.BINDING_IN_PROGRESS -> { + buttonType(ButtonType.NO_BUTTON) + when (pidInfo.finalRequest) { + is Incomplete -> iconMode(IconMode.INFO) + is Fail -> iconMode(IconMode.ERROR) + else -> iconMode(IconMode.NONE) + } + } + } + } + } + } + + private fun buildSharedFail(pidInfo: PidInfo) { + settingsInformationItem { + id("info${pidInfo.threePid.value}") + colorProvider(colorProvider) + textColorId(R.color.vector_error_color) + message((pidInfo.isShared as? Fail)?.error?.message ?: "") + } + } + + private fun buildContinueCancel(threePid: ThreePid) { + settingsContinueCancelItem { + id("bottom${threePid.value}") + interactionListener(object : SettingsContinueCancelItem.Listener { + override fun onContinue() { + when (threePid) { + is ThreePid.Email -> { + listener?.checkEmailVerification(threePid) + } + is ThreePid.Msisdn -> { + val code = codes[threePid] + if (code != null) { + listener?.sendMsisdnVerificationCode(threePid, code) + } + } + } + } + + override fun onCancel() { + listener?.cancelBinding(threePid) + } + }) + } + } + + interface Listener { + fun openIdentityServerTerms() + fun onTapRevoke(threePid: ThreePid) + fun onTapShare(threePid: ThreePid) + fun checkEmailVerification(threePid: ThreePid.Email) + fun cancelBinding(threePid: ThreePid) + fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) + fun onTapChangeIdentityServer() + fun onTapDisconnectIdentityServer() + fun onTapRetryToRetrieveBindings() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt new file mode 100644 index 0000000000..b772db7322 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsFragment.kt @@ -0,0 +1,187 @@ +/* + * 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.riotx.features.discovery + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.ensureProtocol +import im.vector.riotx.features.discovery.change.SetIdentityServerFragment +import im.vector.riotx.features.terms.ReviewTermsActivity +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class DiscoverySettingsFragment @Inject constructor( + private val controller: DiscoverySettingsController, + val viewModelFactory: DiscoverySettingsViewModel.Factory +) : VectorBaseFragment(), DiscoverySettingsController.Listener { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + private val viewModel by fragmentViewModel(DiscoverySettingsViewModel::class) + + lateinit var sharedViewModel: DiscoverySharedViewModel + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java) + + controller.listener = this + recyclerView.configureWith(controller) + + sharedViewModel.navigateEvent.observeEvent(this) { + when (it) { + is DiscoverySharedViewModelAction.ChangeIdentityServer -> + viewModel.handle(DiscoverySettingsAction.ChangeIdentityServer(it.newUrl)) + }.exhaustive + } + + viewModel.observeViewEvents { + when (it) { + is DiscoverySettingsViewEvents.Failure -> { + displayErrorDialog(it.throwable) + } + }.exhaustive + } + } + + override fun onDestroyView() { + recyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_discovery_category) + + // If some 3pids are pending, we can try to check if they have been verified here + viewModel.handle(DiscoverySettingsAction.Refresh) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) { + if (Activity.RESULT_OK == resultCode) { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } else { + // add some error? + } + } + + super.onActivityResult(requestCode, resultCode, data) + } + + override fun openIdentityServerTerms() = withState(viewModel) { state -> + if (state.termsNotSigned) { + navigator.openTerms( + this, + TermsService.ServiceType.IdentityService, + state.identityServer()?.ensureProtocol() ?: "", + null) + } + } + + override fun onTapRevoke(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.RevokeThreePid(threePid)) + } + + override fun onTapShare(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.ShareThreePid(threePid)) + } + + override fun checkEmailVerification(threePid: ThreePid.Email) { + viewModel.handle(DiscoverySettingsAction.FinalizeBind3pid(threePid)) + } + + override fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) { + viewModel.handle(DiscoverySettingsAction.SubmitMsisdnToken(threePid, code)) + } + + override fun cancelBinding(threePid: ThreePid) { + viewModel.handle(DiscoverySettingsAction.CancelBinding(threePid)) + } + + override fun onTapChangeIdentityServer() = withState(viewModel) { state -> + // we should prompt if there are bound items with current is + val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() + val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED } + + if (hasBoundIds) { + // we should prompt + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.change_identity_server) + .setMessage(getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer())) + .setPositiveButton(R.string._continue) { _, _ -> navigateToChangeIdentityServerFragment() } + .setNegativeButton(R.string.cancel, null) + .show() + Unit + } else { + navigateToChangeIdentityServerFragment() + } + } + + override fun onTapDisconnectIdentityServer() { + // we should prompt if there are bound items with current is + withState(viewModel) { state -> + val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty() + val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED } + + val message = if (hasBoundIds) { + getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer()) + } else { + getString(R.string.disconnect_identity_server_dialog_content, state.identityServer()) + } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.disconnect_identity_server) + .setMessage(message) + .setPositiveButton(R.string.disconnect) { _, _ -> viewModel.handle(DiscoverySettingsAction.DisconnectIdentityServer) } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + + override fun onTapRetryToRetrieveBindings() { + viewModel.handle(DiscoverySettingsAction.RetrieveBinding) + } + + private fun navigateToChangeIdentityServerFragment() { + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom) + .replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null) + .addToBackStack(null) + .commit() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt new file mode 100644 index 0000000000..5dc4b2354a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsState.kt @@ -0,0 +1,29 @@ +/* + * 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.riotx.features.discovery + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class DiscoverySettingsState( + val identityServer: Async<String?> = Uninitialized, + val emailList: Async<List<PidInfo>> = Uninitialized, + val phoneNumbersList: Async<List<PidInfo>> = Uninitialized, + // Can be true if terms are updated + val termsNotSigned: Boolean = false +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.kt new file mode 100644 index 0000000000..6fd45394a2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewEvents.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.riotx.features.discovery + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class DiscoverySettingsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : DiscoverySettingsViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt new file mode 100644 index 0000000000..7c5086afa7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySettingsViewModel.kt @@ -0,0 +1,373 @@ +/* + * 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.riotx.features.discovery + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.Async +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.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.identity.IdentityServiceListener +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch + +class DiscoverySettingsViewModel @AssistedInject constructor( + @Assisted initialState: DiscoverySettingsState, + private val session: Session) + : VectorViewModel<DiscoverySettingsState, DiscoverySettingsAction, DiscoverySettingsViewEvents>(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel + } + + companion object : MvRxViewModelFactory<DiscoverySettingsViewModel, DiscoverySettingsState> { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DiscoverySettingsState): DiscoverySettingsViewModel? { + val fragment: DiscoverySettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val identityService = session.identityService() + + private val identityServerManagerListener = object : IdentityServiceListener { + override fun onIdentityServerChange() = withState { state -> + val identityServerUrl = identityService.getCurrentIdentityServerUrl() + val currentIS = state.identityServer() + setState { + copy(identityServer = Success(identityServerUrl)) + } + if (currentIS != identityServerUrl) retrieveBinding() + } + } + + init { + setState { + copy(identityServer = Success(identityService.getCurrentIdentityServerUrl())) + } + startListenToIdentityManager() + observeThreePids() + } + + private fun observeThreePids() { + session.rx() + .liveThreePIds(true) + .subscribe { + retrieveBinding(it) + } + .disposeOnClear() + } + + override fun onCleared() { + super.onCleared() + stopListenToIdentityManager() + } + + override fun handle(action: DiscoverySettingsAction) { + when (action) { + DiscoverySettingsAction.Refresh -> refreshPendingEmailBindings() + DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() + DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() + is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) + is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) + is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) + is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) + is DiscoverySettingsAction.SubmitMsisdnToken -> submitMsisdnToken(action) + is DiscoverySettingsAction.CancelBinding -> cancelBinding(action) + }.exhaustive + } + + private fun disconnectIdentityServer() { + setState { copy(identityServer = Loading()) } + + viewModelScope.launch { + try { + awaitCallback<Unit> { session.identityService().disconnect(it) } + setState { copy(identityServer = Success(null)) } + } catch (failure: Throwable) { + setState { copy(identityServer = Fail(failure)) } + } + } + } + + private fun changeIdentityServer(action: DiscoverySettingsAction.ChangeIdentityServer) { + setState { copy(identityServer = Loading()) } + + viewModelScope.launch { + try { + val data = awaitCallback<String?> { + session.identityService().setNewIdentityServer(action.url, it) + } + setState { copy(identityServer = Success(data)) } + retrieveBinding() + } catch (failure: Throwable) { + setState { copy(identityServer = Fail(failure)) } + } + } + } + + private fun shareThreePid(action: DiscoverySettingsAction.ShareThreePid) = withState { state -> + if (state.identityServer() == null) return@withState + changeThreePidState(action.threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback<Unit> { identityService.startBindThreePid(action.threePid, it) } + changeThreePidState(action.threePid, Success(SharedState.BINDING_IN_PROGRESS)) + } catch (failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeThreePidState(action.threePid, Fail(failure)) + } + } + } + + private fun changeThreePidState(threePid: ThreePid, state: Async<SharedState>) { + setState { + val currentMails = emailList() ?: emptyList() + val phones = phoneNumbersList() ?: emptyList() + copy( + emailList = Success( + currentMails.map { + if (it.threePid == threePid) { + it.copy(isShared = state) + } else { + it + } + } + ), + phoneNumbersList = Success( + phones.map { + if (it.threePid == threePid) { + it.copy(isShared = state) + } else { + it + } + } + ) + ) + } + } + + private fun changeThreePidSubmitState(threePid: ThreePid, submitState: Async<Unit>) { + setState { + val currentMails = emailList() ?: emptyList() + val phones = phoneNumbersList() ?: emptyList() + copy( + emailList = Success( + currentMails.map { + if (it.threePid == threePid) { + it.copy(finalRequest = submitState) + } else { + it + } + } + ), + phoneNumbersList = Success( + phones.map { + if (it.threePid == threePid) { + it.copy(finalRequest = submitState) + } else { + it + } + } + ) + ) + } + } + + private fun revokeThreePid(action: DiscoverySettingsAction.RevokeThreePid) { + when (action.threePid) { + is ThreePid.Email -> revokeEmail(action.threePid) + is ThreePid.Msisdn -> revokeMsisdn(action.threePid) + }.exhaustive + } + + private fun revokeEmail(threePid: ThreePid.Email) = withState { state -> + if (state.identityServer() == null) return@withState + if (state.emailList() == null) return@withState + changeThreePidState(threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback<Unit> { identityService.unbindThreePid(threePid, it) } + changeThreePidState(threePid, Success(SharedState.NOT_SHARED)) + } catch (failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeThreePidState(threePid, Fail(failure)) + } + } + } + + private fun revokeMsisdn(threePid: ThreePid.Msisdn) = withState { state -> + if (state.identityServer() == null) return@withState + if (state.phoneNumbersList() == null) return@withState + changeThreePidState(threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback<Unit> { identityService.unbindThreePid(threePid, it) } + changeThreePidState(threePid, Success(SharedState.NOT_SHARED)) + } catch (failure: Throwable) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + changeThreePidState(threePid, Fail(failure)) + } + } + } + + private fun cancelBinding(action: DiscoverySettingsAction.CancelBinding) { + viewModelScope.launch { + try { + awaitCallback<Unit> { identityService.cancelBindThreePid(action.threePid, it) } + changeThreePidState(action.threePid, Success(SharedState.NOT_SHARED)) + changeThreePidSubmitState(action.threePid, Uninitialized) + } catch (failure: Throwable) { + // This could never fail + } + } + } + + private fun startListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun stopListenToIdentityManager() { + identityService.addListener(identityServerManagerListener) + } + + private fun retrieveBinding() { + retrieveBinding(session.getThreePids()) + } + + private fun retrieveBinding(threePids: List<ThreePid>) = withState { state -> + if (state.identityServer().isNullOrBlank()) return@withState + + val emails = threePids.filterIsInstance<ThreePid.Email>() + val msisdns = threePids.filterIsInstance<ThreePid.Msisdn>() + + setState { + copy( + emailList = Success(emails.map { PidInfo(it, Loading()) }), + phoneNumbersList = Success(msisdns.map { PidInfo(it, Loading()) }) + ) + } + + viewModelScope.launch { + try { + val data = awaitCallback<Map<ThreePid, SharedState>> { + identityService.getShareStatus(threePids, it) + } + setState { + copy( + emailList = Success(data.filter { it.key is ThreePid.Email }.toPidInfoList()), + phoneNumbersList = Success(data.filter { it.key is ThreePid.Msisdn }.toPidInfoList()), + termsNotSigned = false + ) + } + } catch (failure: Throwable) { + if (failure !is IdentityServiceError.TermsNotSignedException) { + _viewEvents.post(DiscoverySettingsViewEvents.Failure(failure)) + } + + setState { + copy( + emailList = Success(emails.map { PidInfo(it, Fail(failure)) }), + phoneNumbersList = Success(msisdns.map { PidInfo(it, Fail(failure)) }), + termsNotSigned = failure is IdentityServiceError.TermsNotSignedException + ) + } + } + } + } + + private fun Map<ThreePid, SharedState>.toPidInfoList(): List<PidInfo> { + return map { threePidStatus -> + PidInfo( + threePid = threePidStatus.key, + isShared = Success(threePidStatus.value) + ) + } + } + + private fun submitMsisdnToken(action: DiscoverySettingsAction.SubmitMsisdnToken) = withState { state -> + if (state.identityServer().isNullOrBlank()) return@withState + + changeThreePidSubmitState(action.threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback<Unit> { + identityService.submitValidationToken(action.threePid, action.code, it) + } + changeThreePidSubmitState(action.threePid, Uninitialized) + finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(action.threePid), true) + } catch (failure: Throwable) { + changeThreePidSubmitState(action.threePid, Fail(failure)) + } + } + } + + private fun finalizeBind3pid(action: DiscoverySettingsAction.FinalizeBind3pid, fromUser: Boolean) = withState { state -> + val threePid = when (action.threePid) { + is ThreePid.Email -> { + state.emailList()?.find { it.threePid.value == action.threePid.email }?.threePid ?: return@withState + } + is ThreePid.Msisdn -> { + state.phoneNumbersList()?.find { it.threePid.value == action.threePid.msisdn }?.threePid ?: return@withState + } + } + + changeThreePidSubmitState(action.threePid, Loading()) + + viewModelScope.launch { + try { + awaitCallback<Unit> { identityService.finalizeBindThreePid(threePid, it) } + changeThreePidSubmitState(action.threePid, Uninitialized) + changeThreePidState(action.threePid, Success(SharedState.SHARED)) + } catch (failure: Throwable) { + // If this is not from user (user did not click to "Continue", but this is a refresh when Fragment is resumed), do no display the error + if (fromUser) { + changeThreePidSubmitState(action.threePid, Fail(failure)) + } else { + changeThreePidSubmitState(action.threePid, Uninitialized) + } + } + } + } + + private fun refreshPendingEmailBindings() = withState { state -> + state.emailList()?.forEach { info -> + when (info.isShared()) { + SharedState.BINDING_IN_PROGRESS -> finalizeBind3pid(DiscoverySettingsAction.FinalizeBind3pid(info.threePid), false) + else -> Unit + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.kt new file mode 100644 index 0000000000..cde326d824 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModel.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.riotx.features.discovery + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.riotx.core.extensions.postLiveEvent +import im.vector.riotx.core.utils.LiveEvent +import javax.inject.Inject + +class DiscoverySharedViewModel @Inject constructor() : ViewModel() { + var navigateEvent = MutableLiveData<LiveEvent<DiscoverySharedViewModelAction>>() + + fun requestChangeToIdentityServer(serverUrl: String) { + navigateEvent.postLiveEvent(DiscoverySharedViewModelAction.ChangeIdentityServer(serverUrl)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModelAction.kt b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModelAction.kt new file mode 100644 index 0000000000..5889ce4a63 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/DiscoverySharedViewModelAction.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.riotx.features.discovery + +sealed class DiscoverySharedViewModelAction { + data class ChangeIdentityServer(val newUrl: String) : DiscoverySharedViewModelAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/PidInfo.kt b/vector/src/main/java/im/vector/riotx/features/discovery/PidInfo.kt new file mode 100644 index 0000000000..67739c48ce --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/PidInfo.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.riotx.features.discovery + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.identity.SharedState +import im.vector.matrix.android.api.session.identity.ThreePid + +data class PidInfo( + // Retrieved from the homeserver + val threePid: ThreePid, + // Retrieved from IdentityServer, or transient state + val isShared: Async<SharedState>, + // Contains information about a current request to submit the token (for instance SMS code received by SMS) + // Or a current binding finalization, for email + val finalRequest: Async<Unit> = Uninitialized +) diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt new file mode 100644 index 0000000000..11a2737496 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsButtonItem.kt @@ -0,0 +1,73 @@ +/* + * 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.riotx.features.discovery + +import android.widget.Button +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.ClickListener +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.attributes.ButtonStyle +import im.vector.riotx.core.epoxy.onClick +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.resources.ColorProvider + +@EpoxyModelClass(layout = R.layout.item_settings_button) +abstract class SettingsButtonItem : EpoxyModelWithHolder<SettingsButtonItem.Holder>() { + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + var buttonTitle: String? = null + + @EpoxyAttribute + @StringRes + var buttonTitleId: Int? = null + + @EpoxyAttribute + var buttonStyle: ButtonStyle = ButtonStyle.POSITIVE + + @EpoxyAttribute + var buttonClickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + if (buttonTitleId != null) { + holder.button.setText(buttonTitleId!!) + } else { + holder.button.setTextOrHide(buttonTitle) + } + + when (buttonStyle) { + ButtonStyle.POSITIVE -> { + holder.button.setTextColor(colorProvider.getColor(R.color.riotx_accent)) + } + ButtonStyle.DESTRUCTIVE -> { + holder.button.setTextColor(colorProvider.getColor(R.color.riotx_destructive_accent)) + } + } + + holder.button.onClick(buttonClickListener) + } + + class Holder : VectorEpoxyHolder() { + val button by bind<Button>(R.id.settings_item_button) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsContinueCancelItem.kt new file mode 100644 index 0000000000..0e424de540 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsContinueCancelItem.kt @@ -0,0 +1,52 @@ +/* + * 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.riotx.features.discovery + +import android.widget.Button +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_settings_continue_cancel) +abstract class SettingsContinueCancelItem : EpoxyModelWithHolder<SettingsContinueCancelItem.Holder>() { + + @EpoxyAttribute + var interactionListener: Listener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.cancelButton.setOnClickListener { + interactionListener?.onCancel() + } + + holder.continueButton.setOnClickListener { + interactionListener?.onContinue() + } + } + + class Holder : VectorEpoxyHolder() { + val cancelButton by bind<Button>(R.id.settings_item_cancel_button) + val continueButton by bind<Button>(R.id.settings_item_continue_button) + } + + interface Listener { + fun onContinue() + fun onCancel() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsEditTextItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsEditTextItem.kt new file mode 100644 index 0000000000..aaa4435f3a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsEditTextItem.kt @@ -0,0 +1,81 @@ +/* + * 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.riotx.features.discovery + +import android.view.KeyEvent +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.TextView +import androidx.core.widget.doOnTextChanged +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.google.android.material.textfield.TextInputLayout +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_settings_edit_text) +abstract class SettingsEditTextItem : EpoxyModelWithHolder<SettingsEditTextItem.Holder>() { + + @EpoxyAttribute var descriptionText: String? = null + @EpoxyAttribute var errorText: String? = null + @EpoxyAttribute var inProgress: Boolean = false + + @EpoxyAttribute + var interactionListener: Listener? = null + + private val textChangeListener: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit = { code, _, _, _ -> + code?.let { interactionListener?.onCodeChange(it.toString()) } + } + + private val editorActionListener = object : TextView.OnEditorActionListener { + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if (actionId == EditorInfo.IME_ACTION_DONE) { + interactionListener?.onValidate() + return true + } + return false + } + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.textView.setTextOrHide(descriptionText) + + holder.editText.isEnabled = !inProgress + + if (errorText.isNullOrBlank()) { + holder.textInputLayout.error = null + } else { + holder.textInputLayout.error = errorText + } + + holder.editText.doOnTextChanged(textChangeListener) + holder.editText.setOnEditorActionListener(editorActionListener) + } + + class Holder : VectorEpoxyHolder() { + val textView by bind<TextView>(R.id.settings_item_edit_text_description) + val editText by bind<EditText>(R.id.settings_item_edit_text) + val textInputLayout by bind<TextInputLayout>(R.id.settings_item_edit_text_til) + } + + interface Listener { + fun onValidate() + fun onCodeChange(code: String) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsInfoItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsInfoItem.kt new file mode 100644 index 0000000000..c28b58340b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsInfoItem.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.riotx.features.discovery + +import android.view.View +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_settings_helper_info) +abstract class SettingsInfoItem : EpoxyModelWithHolder<SettingsInfoItem.Holder>() { + + @EpoxyAttribute + var helperText: String? = null + + @EpoxyAttribute + @StringRes + var helperTextResId: Int? = null + + @EpoxyAttribute + var itemClickListener: View.OnClickListener? = null + + @EpoxyAttribute + @DrawableRes + var compoundDrawable: Int = R.drawable.vector_warning_red + + @EpoxyAttribute + var showCompoundDrawable: Boolean = false + + override fun bind(holder: Holder) { + super.bind(holder) + + if (helperTextResId != null) { + holder.text.setText(helperTextResId!!) + } else { + holder.text.setTextOrHide(helperText) + } + + holder.view.setOnClickListener(itemClickListener) + + if (showCompoundDrawable) { + holder.text.setCompoundDrawablesWithIntrinsicBounds(compoundDrawable, 0, 0, 0) + } else { + holder.text.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) + } + } + + class Holder : VectorEpoxyHolder() { + val text by bind<TextView>(R.id.settings_helper_text) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsInformationItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsInformationItem.kt new file mode 100644 index 0000000000..ad3c445ac3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsInformationItem.kt @@ -0,0 +1,50 @@ +/* + * 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.riotx.features.discovery + +import android.widget.TextView +import androidx.annotation.ColorRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.resources.ColorProvider + +@EpoxyModelClass(layout = R.layout.item_settings_information) +abstract class SettingsInformationItem : EpoxyModelWithHolder<SettingsInformationItem.Holder>() { + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + lateinit var message: String + + @EpoxyAttribute + @ColorRes + var textColorId: Int = R.color.vector_info_color + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.textView.text = message + holder.textView.setTextColor(colorProvider.getColor(textColorId)) + } + + class Holder : VectorEpoxyHolder() { + val textView by bind<TextView>(R.id.settings_item_information) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItem.kt new file mode 100644 index 0000000000..4b6e4b3edf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsItem.kt @@ -0,0 +1,73 @@ +/* + * 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.riotx.features.discovery + +import android.view.View +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_settings_simple_item) +abstract class SettingsItem : EpoxyModelWithHolder<SettingsItem.Holder>() { + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute + @StringRes + var titleResId: Int? = null + + @EpoxyAttribute + @StringRes + var descriptionResId: Int? = null + + @EpoxyAttribute + var description: CharSequence? = null + + @EpoxyAttribute + var itemClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + if (titleResId != null) { + holder.titleText.setText(titleResId!!) + } else { + holder.titleText.setTextOrHide(title) + } + + if (descriptionResId != null) { + holder.descriptionText.setText(descriptionResId!!) + } else { + holder.descriptionText.setTextOrHide(description) + } + + holder.switchButton.isVisible = false + + holder.view.setOnClickListener(itemClickListener) + } + + class Holder : VectorEpoxyHolder() { + val titleText by bind<TextView>(R.id.settings_item_title) + val descriptionText by bind<TextView>(R.id.settings_item_description) + val switchButton by bind<Switch>(R.id.settings_item_switch) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsProgressItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsProgressItem.kt new file mode 100644 index 0000000000..b48fc3b7f1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsProgressItem.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.riotx.features.discovery + +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_settings_progress) +abstract class SettingsProgressItem : EpoxyModelWithHolder<SettingsProgressItem.Holder>() { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsSectionTitleItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsSectionTitleItem.kt new file mode 100644 index 0000000000..d8b6eb7daa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsSectionTitleItem.kt @@ -0,0 +1,50 @@ +/* + * 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.riotx.features.discovery + +import android.widget.TextView +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_settings_section_title) +abstract class SettingsSectionTitleItem : EpoxyModelWithHolder<SettingsSectionTitleItem.Holder>() { + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute + @StringRes + var titleResId: Int? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + if (titleResId != null) { + holder.textView.setText(titleResId!!) + } else { + holder.textView.setTextOrHide(title) + } + } + + class Holder : VectorEpoxyHolder() { + val textView by bind<TextView>(R.id.settings_section_title_text) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/SettingsTextButtonSingleLineItem.kt b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsTextButtonSingleLineItem.kt new file mode 100644 index 0000000000..90a1a9ef99 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/SettingsTextButtonSingleLineItem.kt @@ -0,0 +1,166 @@ +/* + * 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.riotx.features.discovery + +import android.widget.Button +import android.widget.CompoundButton +import android.widget.ProgressBar +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.ClickListener +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.attributes.ButtonStyle +import im.vector.riotx.core.epoxy.attributes.ButtonType +import im.vector.riotx.core.epoxy.attributes.IconMode +import im.vector.riotx.core.epoxy.onClick +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.themes.ThemeUtils + +@EpoxyModelClass(layout = R.layout.item_settings_button_single_line) +abstract class SettingsTextButtonSingleLineItem : EpoxyModelWithHolder<SettingsTextButtonSingleLineItem.Holder>() { + + @EpoxyAttribute + lateinit var colorProvider: ColorProvider + + @EpoxyAttribute + lateinit var stringProvider: StringProvider + + @EpoxyAttribute + var title: String? = null + + @EpoxyAttribute + @StringRes + var titleResId: Int? = null + + @EpoxyAttribute + var iconMode: IconMode = IconMode.NONE + + @EpoxyAttribute + var buttonTitle: String? = null + + @EpoxyAttribute + @StringRes + var buttonTitleId: Int? = null + + @EpoxyAttribute + var buttonStyle: ButtonStyle = ButtonStyle.POSITIVE + + @EpoxyAttribute + var buttonType: ButtonType = ButtonType.NORMAL + + @EpoxyAttribute + var buttonIndeterminate: Boolean = false + + @EpoxyAttribute + var checked: Boolean? = null + + @EpoxyAttribute + var buttonClickListener: ClickListener? = null + + @EpoxyAttribute + var switchChangeListener: CompoundButton.OnCheckedChangeListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + if (titleResId != null) { + holder.textView.setText(titleResId!!) + } else { + holder.textView.setTextOrHide(title, hideWhenBlank = false) + } + + if (buttonTitleId != null) { + holder.mainButton.setText(buttonTitleId!!) + } else { + holder.mainButton.setTextOrHide(buttonTitle) + } + + if (buttonIndeterminate) { + holder.progress.isVisible = true + holder.mainButton.isInvisible = true + holder.switchButton.isInvisible = true + holder.switchButton.setOnCheckedChangeListener(null) + holder.mainButton.setOnClickListener(null) + } else { + holder.progress.isVisible = false + when (buttonType) { + ButtonType.NO_BUTTON -> { + holder.mainButton.isVisible = false + holder.switchButton.isVisible = false + } + ButtonType.NORMAL -> { + holder.mainButton.isVisible = true + holder.switchButton.isVisible = false + when (buttonStyle) { + ButtonStyle.POSITIVE -> { + holder.mainButton.setTextColor(colorProvider.getColorFromAttribute(R.attr.colorAccent)) + } + ButtonStyle.DESTRUCTIVE -> { + holder.mainButton.setTextColor(colorProvider.getColor(R.color.vector_error_color)) + } + }.exhaustive + holder.mainButton.onClick(buttonClickListener) + } + ButtonType.SWITCH -> { + holder.mainButton.isVisible = false + holder.switchButton.isVisible = true + // set to null before changing the state + holder.switchButton.setOnCheckedChangeListener(null) + checked?.let { holder.switchButton.isChecked = it } + holder.switchButton.setOnCheckedChangeListener(switchChangeListener) + } + }.exhaustive + } + + when (iconMode) { + IconMode.NONE -> { + holder.textView.setCompoundDrawables(null, null, null, null) + } + IconMode.INFO -> { + val errorColor = colorProvider.getColor(R.color.notification_accent_color) + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_notification_privacy_warning)?.apply { + ThemeUtils.tintDrawableWithColor(this, errorColor) + holder.textView.setCompoundDrawablesWithIntrinsicBounds(this, null, null, null) + } + } + IconMode.ERROR -> { + val errorColor = colorProvider.getColor(R.color.vector_error_color) + ContextCompat.getDrawable(holder.view.context, R.drawable.ic_notification_privacy_warning)?.apply { + ThemeUtils.tintDrawableWithColor(this, errorColor) + holder.textView.setCompoundDrawablesWithIntrinsicBounds(this, null, null, null) + } + } + } + } + + class Holder : VectorEpoxyHolder() { + val textView by bind<TextView>(R.id.settings_item_text) + val mainButton by bind<Button>(R.id.settings_item_button) + val switchButton by bind<Switch>(R.id.settings_item_switch) + val progress by bind<ProgressBar>(R.id.settings_item_progress) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerAction.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerAction.kt new file mode 100644 index 0000000000..14f149c282 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerAction.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.riotx.features.discovery.change + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class SetIdentityServerAction : VectorViewModelAction { + object UseDefaultIdentityServer : SetIdentityServerAction() + + data class UseCustomIdentityServer(val url: String) : SetIdentityServerAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt new file mode 100644 index 0000000000..701c187cef --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerFragment.kt @@ -0,0 +1,170 @@ +/* + * 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.riotx.features.discovery.change + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.appcompat.app.AlertDialog +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.toReducedUrl +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.discovery.DiscoverySharedViewModel +import im.vector.riotx.features.terms.ReviewTermsActivity +import kotlinx.android.synthetic.main.fragment_set_identity_server.* +import javax.inject.Inject + +class SetIdentityServerFragment @Inject constructor( + val viewModelFactory: SetIdentityServerViewModel.Factory, + val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_set_identity_server + + private val viewModel by fragmentViewModel(SetIdentityServerViewModel::class) + + lateinit var sharedViewModel: DiscoverySharedViewModel + + override fun invalidate() = withState(viewModel) { state -> + if (state.defaultIdentityServerUrl.isNullOrEmpty()) { + // No default + identityServerSetDefaultNotice.isVisible = false + identityServerSetDefaultSubmit.isVisible = false + identityServerSetDefaultAlternative.setText(R.string.identity_server_set_alternative_notice_no_default) + } else { + identityServerSetDefaultNotice.text = getString( + R.string.identity_server_set_default_notice, + state.homeServerUrl.toReducedUrl(), + state.defaultIdentityServerUrl.toReducedUrl() + ) + .toSpannable() + .colorizeMatchingText(state.defaultIdentityServerUrl.toReducedUrl(), + colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast)) + + identityServerSetDefaultNotice.isVisible = true + identityServerSetDefaultSubmit.isVisible = true + identityServerSetDefaultSubmit.text = getString(R.string.identity_server_set_default_submit, state.defaultIdentityServerUrl.toReducedUrl()) + identityServerSetDefaultAlternative.setText(R.string.identity_server_set_alternative_notice) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + sharedViewModel = activityViewModelProvider.get(DiscoverySharedViewModel::class.java) + + identityServerSetDefaultAlternativeTextInput.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + viewModel.handle(SetIdentityServerAction.UseCustomIdentityServer(identityServerSetDefaultAlternativeTextInput.text.toString())) + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + + identityServerSetDefaultAlternativeTextInput + .textChanges() + .subscribe { + identityServerSetDefaultAlternativeTil.error = null + identityServerSetDefaultAlternativeSubmit.isEnabled = it.isNotEmpty() + } + .disposeOnDestroyView() + + identityServerSetDefaultSubmit.debouncedClicks { + viewModel.handle(SetIdentityServerAction.UseDefaultIdentityServer) + } + + identityServerSetDefaultAlternativeSubmit.debouncedClicks { + viewModel.handle(SetIdentityServerAction.UseCustomIdentityServer(identityServerSetDefaultAlternativeTextInput.text.toString())) + } + + viewModel.observeViewEvents { + when (it) { + is SetIdentityServerViewEvents.Loading -> showLoading(it.message) + is SetIdentityServerViewEvents.Failure -> handleFailure(it) + is SetIdentityServerViewEvents.OtherFailure -> showFailure(it.failure) + is SetIdentityServerViewEvents.NoTerms -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.settings_discovery_no_terms_title) + .setMessage(R.string.settings_discovery_no_terms) + .setPositiveButton(R.string._continue) { _, _ -> + processIdentityServerChange() + } + .setNegativeButton(R.string.cancel, null) + .show() + Unit + } + is SetIdentityServerViewEvents.TermsAccepted -> processIdentityServerChange() + is SetIdentityServerViewEvents.ShowTerms -> { + navigator.openTerms( + this, + TermsService.ServiceType.IdentityService, + it.identityServerUrl, + null) + } + }.exhaustive + } + } + + private fun handleFailure(failure: SetIdentityServerViewEvents.Failure) { + val message = getString(failure.errorMessageId) + if (failure.forDefault) { + // Display the error in a dialog + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } else { + // Display the error inlined + identityServerSetDefaultAlternativeTil.error = message + } + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.identity_server) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == ReviewTermsActivity.TERMS_REQUEST_CODE) { + if (Activity.RESULT_OK == resultCode) { + processIdentityServerChange() + } else { + // add some error? + } + } + super.onActivityResult(requestCode, resultCode, data) + } + + private fun processIdentityServerChange() { + viewModel.currentWantedUrl?.let { + sharedViewModel.requestChangeToIdentityServer(it) + parentFragmentManager.popBackStack() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerState.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerState.kt new file mode 100644 index 0000000000..2b76d21ce5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerState.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.riotx.features.discovery.change + +import com.airbnb.mvrx.MvRxState + +data class SetIdentityServerState( + val homeServerUrl: String = "", + // Will contain the default identity server url if any + val defaultIdentityServerUrl: String? = null +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewEvents.kt new file mode 100644 index 0000000000..b840afac11 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewEvents.kt @@ -0,0 +1,31 @@ +/* + * 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.riotx.features.discovery.change + +import androidx.annotation.StringRes +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class SetIdentityServerViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : SetIdentityServerViewEvents() + data class Failure(@StringRes val errorMessageId: Int, val forDefault: Boolean) : SetIdentityServerViewEvents() + data class OtherFailure(val failure: Throwable) : SetIdentityServerViewEvents() + + data class ShowTerms(val identityServerUrl: String) : SetIdentityServerViewEvents() + + object NoTerms : SetIdentityServerViewEvents() + object TermsAccepted : SetIdentityServerViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt new file mode 100644 index 0000000000..9bec24548e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/discovery/change/SetIdentityServerViewModel.kt @@ -0,0 +1,147 @@ +/* + * 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.riotx.features.discovery.change + +import androidx.lifecycle.viewModelScope +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.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.identity.IdentityServiceError +import im.vector.matrix.android.api.session.terms.GetTermsResponse +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.R +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.ensureProtocol +import kotlinx.coroutines.launch +import java.net.UnknownHostException + +class SetIdentityServerViewModel @AssistedInject constructor( + @Assisted initialState: SetIdentityServerState, + private val mxSession: Session, + stringProvider: StringProvider) + : VectorViewModel<SetIdentityServerState, SetIdentityServerAction, SetIdentityServerViewEvents>(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SetIdentityServerState): SetIdentityServerViewModel + } + + companion object : MvRxViewModelFactory<SetIdentityServerViewModel, SetIdentityServerState> { + + override fun initialState(viewModelContext: ViewModelContext): SetIdentityServerState? { + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + return SetIdentityServerState( + homeServerUrl = session.sessionParams.homeServerUrl, + defaultIdentityServerUrl = session.identityService().getDefaultIdentityServer() + ) + } + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SetIdentityServerState): SetIdentityServerViewModel? { + val fragment: SetIdentityServerFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + var currentWantedUrl: String? = null + private set + + private val userLanguage = stringProvider.getString(R.string.resources_language) + + override fun handle(action: SetIdentityServerAction) { + when (action) { + SetIdentityServerAction.UseDefaultIdentityServer -> useDefault() + is SetIdentityServerAction.UseCustomIdentityServer -> usedCustomIdentityServerUrl(action) + }.exhaustive + } + + private fun useDefault() = withState { state -> + state.defaultIdentityServerUrl?.let { doChangeIdentityServerUrl(it, true) } + } + + private fun usedCustomIdentityServerUrl(action: SetIdentityServerAction.UseCustomIdentityServer) { + doChangeIdentityServerUrl(action.url, false) + } + + private fun doChangeIdentityServerUrl(url: String, isDefault: Boolean) { + if (url.isEmpty()) { + _viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.settings_discovery_please_enter_server, isDefault)) + return + } + val baseUrl = url.ensureProtocol().also { currentWantedUrl = it } + + _viewEvents.post(SetIdentityServerViewEvents.Loading()) + + viewModelScope.launch { + try { + // First ping the identity server v2 API + awaitCallback<Unit> { + mxSession.identityService().isValidIdentityServer(baseUrl, it) + } + // Ok, next step + checkTerms(baseUrl) + } catch (failure: Throwable) { + when { + failure is IdentityServiceError.OutdatedIdentityServer -> + _viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.identity_server_error_outdated_identity_server, isDefault)) + failure is Failure.NetworkConnection && failure.ioException is UnknownHostException -> + _viewEvents.post(SetIdentityServerViewEvents.Failure(R.string.settings_discovery_bad_identity_server, isDefault)) + else -> + _viewEvents.post(SetIdentityServerViewEvents.OtherFailure(failure)) + } + } + } + } + + private suspend fun checkTerms(baseUrl: String) { + try { + val data = awaitCallback<GetTermsResponse> { + mxSession.getTerms(TermsService.ServiceType.IdentityService, baseUrl, it) + } + + // has all been accepted? + val resp = data.serverResponse + val tos = resp.getLocalizedTerms(userLanguage) + if (tos.isEmpty()) { + // prompt do not define policy + _viewEvents.post(SetIdentityServerViewEvents.NoTerms) + } else { + val shouldPrompt = tos.any { !data.alreadyAcceptedTermUrls.contains(it.localizedUrl) } + if (shouldPrompt) { + _viewEvents.post(SetIdentityServerViewEvents.ShowTerms(baseUrl)) + } else { + _viewEvents.post(SetIdentityServerViewEvents.TermsAccepted) + } + } + } catch (failure: Throwable) { + if (failure is Failure.OtherServerError && failure.httpCode == 404) { + // 404: Same as NoTerms + _viewEvents.post(SetIdentityServerViewEvents.NoTerms) + } else { + _viewEvents.post(SetIdentityServerViewEvents.OtherFailure(failure)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 1357b30413..b17fb87f50 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -209,7 +209,7 @@ class HomeDetailFragment @Inject constructor( parentActivity.configure(groupToolbar) } groupToolbar.title = "" - groupToolbarAvatarImageView.setOnClickListener { + groupToolbarAvatarImageView.debouncedClicks { sharedActionViewModel.post(HomeActivitySharedAction.OpenDrawer) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index a8373797c6..3439b1793c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -53,13 +53,13 @@ class HomeDrawerFragment @Inject constructor( homeDrawerUserIdView.text = user.userId } } - homeDrawerHeaderSettingsView.setOnClickListener { + homeDrawerHeaderSettingsView.debouncedClicks { sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) navigator.openSettings(requireActivity()) } // Debug menu - homeDrawerHeaderDebugView.setOnClickListener { + homeDrawerHeaderDebugView.debouncedClicks { sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) navigator.openDebug(requireActivity()) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt index a05e9ee985..1a0d9baf15 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -67,9 +67,10 @@ class UnknownDeviceDetectorSharedViewModel( init { - val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull { - it.deviceId == session.sessionParams.credentials.deviceId - }?.firstTimeSeenLocalTs ?: System.currentTimeMillis() + val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) + .firstOrNull { it.deviceId == session.sessionParams.deviceId } + ?.firstTimeSeenLocalTs + ?: System.currentTimeMillis() Timber.v("## Detector - Current Session first time seen $currentSessionTs") ignoredDeviceList.addAll( diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index f348e0612a..079a2927d7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -264,7 +264,7 @@ class RoomDetailFragment @Inject constructor( setupNotificationView() setupJumpToReadMarkerView() setupJumpToBottomView() - roomToolbarContentView.setOnClickListener { + roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) } roomDetailViewModel.subscribe { renderState(it) } @@ -348,7 +348,7 @@ class RoomDetailFragment @Inject constructor( private fun setupJumpToBottomView() { jumpToBottomView.visibility = View.INVISIBLE - jumpToBottomView.setOnClickListener { + jumpToBottomView.debouncedClicks { roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) jumpToBottomView.visibility = View.INVISIBLE if (!roomDetailViewModel.timeline.isLive) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 727b33ee37..20748791aa 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -99,11 +99,13 @@ class RoomDetailViewModel @AssistedInject constructor( private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, filterEdits = false, + filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), filterTypes = false, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } else { TimelineSettings(30, filterEdits = true, + filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), filterTypes = true, allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 9c606de3b4..1484e8009b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -142,10 +142,10 @@ class RoomListFragment @Inject constructor( else -> Unit // No button in this mode } - createChatRoomButton.setOnClickListener { + createChatRoomButton.debouncedClicks { createDirectChat() } - createGroupRoomButton.setOnClickListener { + createGroupRoomButton.debouncedClicks { openRoomDirectory() } diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt index 07c0cdbc7d..fc2f34b7a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt @@ -66,13 +66,13 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted { val successMessage = when (selectedUsers.size) { 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, - selectedUsers.first().displayName) + selectedUsers.first().getBestName()) 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, - selectedUsers.first().displayName, - selectedUsers.last().displayName) + selectedUsers.first().getBestName(), + selectedUsers.last().getBestName()) else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, selectedUsers.size - 1, - selectedUsers.first().displayName, + selectedUsers.first().getBestName(), selectedUsers.size - 1) } _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 92dcfcc8aa..6f95847b20 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -25,6 +25,7 @@ import butterknife.OnClick import com.jakewharton.rxbinding3.widget.textChanges import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.utils.ensureProtocol import im.vector.riotx.core.utils.openUrlInExternalBrowser import kotlinx.android.synthetic.main.fragment_login_server_url_form.* import javax.inject.Inject @@ -96,16 +97,13 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() cleanupUi() // Static check of homeserver url, empty, malformed, etc. - var serverUrl = loginServerUrlFormHomeServerUrl.text.toString().trim() + val serverUrl = loginServerUrlFormHomeServerUrl.text.toString().trim().ensureProtocol() when { serverUrl.isBlank() -> { loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) } else -> { - if (serverUrl.startsWith("http").not()) { - serverUrl = "https://$serverUrl" - } loginServerUrlFormHomeServerUrl.setText(serverUrl) loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt index 52aaa9d4a4..6659d65dd6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LocalizedFlowDataLoginTermsChecked.kt @@ -16,7 +16,7 @@ package im.vector.riotx.features.login.terms -import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms data class LocalizedFlowDataLoginTermsChecked(val localizedFlowDataLoginTerms: LocalizedFlowDataLoginTerms, var checked: Boolean = false) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 09746adc87..88f4fc2f5f 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -31,7 +31,7 @@ import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginViewState import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_login_terms.* -import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms import javax.inject.Inject @Parcelize diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt index 104ea88daa..77293fbef6 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsViewState.kt @@ -17,7 +17,7 @@ package im.vector.riotx.features.login.terms import com.airbnb.mvrx.MvRxState -import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms data class LoginTermsViewState( val localizedFlowDataLoginTermsChecked: List<LocalizedFlowDataLoginTermsChecked> diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt index c301463c2a..42ed87a280 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/PolicyController.kt @@ -18,7 +18,7 @@ package im.vector.riotx.features.login.terms import android.view.View import com.airbnb.epoxy.TypedEpoxyController -import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms import javax.inject.Inject class PolicyController @Inject constructor() : TypedEpoxyController<List<LocalizedFlowDataLoginTermsChecked>>() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt index c9e6dcf3fd..d28d12eee8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/converter.kt @@ -17,7 +17,7 @@ package im.vector.riotx.features.login.terms import im.vector.matrix.android.api.auth.registration.TermPolicies -import org.matrix.androidsdk.rest.model.login.LocalizedFlowDataLoginTerms +import im.vector.matrix.android.internal.auth.registration.LocalizedFlowDataLoginTerms /** * This method extract the policies from the login terms parameter, regarding the user language. diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index ac725eb850..b2213eb223 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -23,8 +23,10 @@ import android.view.View import androidx.core.app.ActivityOptionsCompat import androidx.core.app.TaskStackBuilder import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -52,6 +54,7 @@ import im.vector.riotx.features.roomprofile.RoomProfileActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData +import im.vector.riotx.features.terms.ReviewTermsActivity import javax.inject.Inject import javax.inject.Singleton @@ -207,6 +210,11 @@ class DefaultNavigator @Inject constructor( } } + override fun openTerms(fragment: Fragment, serviceType: TermsService.ServiceType, baseUrl: String, token: String?, requestCode: Int) { + val intent = ReviewTermsActivity.intent(fragment.requireContext(), serviceType, baseUrl, token) + fragment.startActivityForResult(intent, requestCode) + } + private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { if (buildTask) { val stackBuilder = TaskStackBuilder.create(context) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index cc8e7cac34..07ec0e4ca2 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.view.View +import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData +import im.vector.riotx.features.terms.ReviewTermsActivity interface Navigator { @@ -67,4 +70,10 @@ interface Navigator { fun openRoomProfile(context: Context, roomId: String) fun openBigImageViewer(activity: Activity, sharedElement: View?, matrixItem: MatrixItem) + + fun openTerms(fragment: Fragment, + serviceType: TermsService.ServiceType, + baseUrl: String, + token: String?, + requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE) } diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt index 7d58c4aacc..a001567635 100755 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/BugReporter.kt @@ -31,9 +31,9 @@ import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.toOnOff -import im.vector.riotx.core.utils.getDeviceLocale import im.vector.riotx.features.settings.VectorLocale import im.vector.riotx.features.settings.VectorPreferences +import im.vector.riotx.features.settings.locale.SystemLocaleProvider import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.version.VersionProvider import okhttp3.Call @@ -58,10 +58,13 @@ import javax.inject.Singleton * BugReporter creates and sends the bug reports. */ @Singleton -class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, - private val versionProvider: VersionProvider, - private val vectorPreferences: VectorPreferences, - private val vectorFileLogger: VectorFileLogger) { +class BugReporter @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val versionProvider: VersionProvider, + private val vectorPreferences: VectorPreferences, + private val vectorFileLogger: VectorFileLogger, + private val systemLocaleProvider: SystemLocaleProvider +) { var inMultiWindowMode = false companion object { @@ -209,7 +212,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes activeSessionHolder.getSafeActiveSession()?.let { session -> userId = session.myUserId - deviceId = session.sessionParams.credentials.deviceId ?: "undefined" + deviceId = session.sessionParams.deviceId ?: "undefined" olmVersion = session.cryptoService().getCryptoVersion(context, true) } @@ -240,7 +243,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) .addFormDataPart("locale", Locale.getDefault().toString()) .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) - .addFormDataPart("default_app_language", getDeviceLocale(context).toString()) + .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) val buildNumber = context.getString(R.string.build_number) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt index e466c2311f..a75479275b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt @@ -71,7 +71,7 @@ class PublicRoomsFragment @Inject constructor( } .disposeOnDestroyView() - publicRoomsCreateNewRoom.setOnClickListener { + publicRoomsCreateNewRoom.debouncedClicks { sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt index 827db96783..7956f2fd9e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -46,7 +46,7 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C vectorBaseActivity.setSupportActionBar(createRoomToolbar) sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) setupRecyclerView() - createRoomClose.setOnClickListener { + createRoomClose.debouncedClicks { sharedActionViewModel.post(RoomDirectorySharedAction.Back) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt index a9e797ba7a..47c438695c 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/FontScale.kt @@ -17,7 +17,7 @@ package im.vector.riotx.features.settings import android.content.Context -import android.content.res.Configuration +import androidx.annotation.StringRes import androidx.core.content.edit import androidx.preference.PreferenceManager import im.vector.riotx.R @@ -29,124 +29,59 @@ object FontScale { // Key for the SharedPrefs private const val APPLICATION_FONT_SCALE_KEY = "APPLICATION_FONT_SCALE_KEY" - // Possible values for the SharedPrefs - private const val FONT_SCALE_TINY = "FONT_SCALE_TINY" - private const val FONT_SCALE_SMALL = "FONT_SCALE_SMALL" - private const val FONT_SCALE_NORMAL = "FONT_SCALE_NORMAL" - private const val FONT_SCALE_LARGE = "FONT_SCALE_LARGE" - private const val FONT_SCALE_LARGER = "FONT_SCALE_LARGER" - private const val FONT_SCALE_LARGEST = "FONT_SCALE_LARGEST" - private const val FONT_SCALE_HUGE = "FONT_SCALE_HUGE" - - private val fontScaleToPrefValue = mapOf( - 0.70f to FONT_SCALE_TINY, - 0.85f to FONT_SCALE_SMALL, - 1.00f to FONT_SCALE_NORMAL, - 1.15f to FONT_SCALE_LARGE, - 1.30f to FONT_SCALE_LARGER, - 1.45f to FONT_SCALE_LARGEST, - 1.60f to FONT_SCALE_HUGE + data class FontScaleValue( + val index: Int, + // Possible values for the SharedPrefs + val preferenceValue: String, + val scale: Float, + @StringRes + val nameResId: Int ) - private val prefValueToNameResId = mapOf( - FONT_SCALE_TINY to R.string.tiny, - FONT_SCALE_SMALL to R.string.small, - FONT_SCALE_NORMAL to R.string.normal, - FONT_SCALE_LARGE to R.string.large, - FONT_SCALE_LARGER to R.string.larger, - FONT_SCALE_LARGEST to R.string.largest, - FONT_SCALE_HUGE to R.string.huge + private val fontScaleValues = listOf( + FontScaleValue(0, "FONT_SCALE_TINY", 0.70f, R.string.tiny), + FontScaleValue(1, "FONT_SCALE_SMALL", 0.85f, R.string.small), + FontScaleValue(2, "FONT_SCALE_NORMAL", 1.00f, R.string.normal), + FontScaleValue(3, "FONT_SCALE_LARGE", 1.15f, R.string.large), + FontScaleValue(4, "FONT_SCALE_LARGER", 1.30f, R.string.larger), + FontScaleValue(5, "FONT_SCALE_LARGEST", 1.45f, R.string.largest), + FontScaleValue(6, "FONT_SCALE_HUGE", 1.60f, R.string.huge) ) + private val normalFontScaleValue = fontScaleValues[2] + /** * Get the font scale value from SharedPrefs. Init the SharedPrefs if necessary * - * @return the font scale + * @return the font scale value */ - fun getFontScalePrefValue(context: Context): String { + fun getFontScaleValue(context: Context): FontScaleValue { val preferences = PreferenceManager.getDefaultSharedPreferences(context) - var scalePreferenceValue: String - if (APPLICATION_FONT_SCALE_KEY !in preferences) { + return if (APPLICATION_FONT_SCALE_KEY !in preferences) { val fontScale = context.resources.configuration.fontScale - scalePreferenceValue = FONT_SCALE_NORMAL - - if (fontScaleToPrefValue.containsKey(fontScale)) { - scalePreferenceValue = fontScaleToPrefValue[fontScale] as String - } - - preferences.edit { - putString(APPLICATION_FONT_SCALE_KEY, scalePreferenceValue) - } + (fontScaleValues.firstOrNull { it.scale == fontScale } ?: normalFontScaleValue) + .also { preferences.edit { putString(APPLICATION_FONT_SCALE_KEY, it.preferenceValue) } } } else { - scalePreferenceValue = preferences.getString(APPLICATION_FONT_SCALE_KEY, FONT_SCALE_NORMAL)!! + val pref = preferences.getString(APPLICATION_FONT_SCALE_KEY, null) + fontScaleValues.firstOrNull { it.preferenceValue == pref } ?: normalFontScaleValue } + } - return scalePreferenceValue + fun updateFontScale(context: Context, index: Int) { + fontScaleValues.getOrNull(index)?.let { + saveFontScaleValue(context, it) + } } /** - * Provides the font scale value + * Store the font scale vale * - * @return the font scale + * @param fontScaleValue the font scale value to store */ - fun getFontScale(context: Context): Float { - val fontScale = getFontScalePrefValue(context) - - if (fontScaleToPrefValue.containsValue(fontScale)) { - for ((key, value) in fontScaleToPrefValue) { - if (value == fontScale) { - return key - } - } - } - - return 1.0f - } - - /** - * Provides the font scale description - * - * @return the font description - */ - fun getFontScaleDescription(context: Context): String { - val fontScale = getFontScalePrefValue(context) - - return if (prefValueToNameResId.containsKey(fontScale)) { - context.getString(prefValueToNameResId[fontScale] as Int) - } else context.getString(R.string.normal) - } - - /** - * Update the font size from the locale description. - * - * @param fontScaleDescription the font scale description - */ - fun updateFontScale(context: Context, fontScaleDescription: String) { - for ((key, value) in prefValueToNameResId) { - if (context.getString(value) == fontScaleDescription) { - saveFontScale(context, key) - } - } - - val config = Configuration(context.resources.configuration) - config.fontScale = getFontScale(context) - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - } - - /** - * Save the new font scale - * - * @param scaleValue the text scale - */ - fun saveFontScale(context: Context, scaleValue: String) { - if (scaleValue.isNotEmpty()) { - PreferenceManager.getDefaultSharedPreferences(context) - .edit { - putString(APPLICATION_FONT_SCALE_KEY, scaleValue) - } - } + private fun saveFontScaleValue(context: Context, fontScaleValue: FontScaleValue) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit { putString(APPLICATION_FONT_SCALE_KEY, fontScaleValue.preferenceValue) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt index e1a89ab3c4..a4ccfdba47 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorLocale.kt @@ -19,13 +19,11 @@ package im.vector.riotx.features.settings import android.content.Context import android.content.res.Configuration import android.os.Build -import androidx.preference.PreferenceManager import androidx.core.content.edit -import im.vector.riotx.BuildConfig +import androidx.preference.PreferenceManager import im.vector.riotx.R import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -41,10 +39,9 @@ object VectorLocale { private val defaultLocale = Locale("en", "US") /** - * The supported application languages + * The cache of supported application languages */ - var supportedLocales = ArrayList<Locale>() - private set + private val supportedLocales = mutableListOf<Locale>() /** * Provides the current application locale @@ -52,10 +49,13 @@ object VectorLocale { var applicationLocale = defaultLocale private set + lateinit var context: Context + /** * Init this object */ fun init(context: Context) { + this.context = context val preferences = PreferenceManager.getDefaultSharedPreferences(context) if (preferences.contains(APPLICATION_LOCALE_LANGUAGE_KEY)) { @@ -72,19 +72,14 @@ object VectorLocale { applicationLocale = defaultLocale } - saveApplicationLocale(context, applicationLocale) - } - - // init the known locales in background, using kotlin coroutines - GlobalScope.launch(Dispatchers.IO) { - initApplicationLocales(context) + saveApplicationLocale(applicationLocale) } } /** * Save the new application locale. */ - fun saveApplicationLocale(context: Context, locale: Locale) { + fun saveApplicationLocale(locale: Locale) { applicationLocale = locale PreferenceManager.getDefaultSharedPreferences(context).edit { @@ -144,6 +139,7 @@ object VectorLocale { } else { val resources = context.resources val conf = resources.configuration + @Suppress("DEPRECATION") val savedLocale = conf.locale @Suppress("DEPRECATION") @@ -165,11 +161,9 @@ object VectorLocale { } /** - * Provides the supported application locales list - * - * @param context the context + * Init the supported application locales list */ - private fun initApplicationLocales(context: Context) { + private fun initApplicationLocales() { val knownLocalesSet = HashSet<Triple<String, String, String>>() try { @@ -195,9 +189,7 @@ object VectorLocale { ) } - supportedLocales.clear() - - knownLocalesSet.mapTo(supportedLocales) { (language, country, script) -> + val list = knownLocalesSet.map { (language, country, script) -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Locale.Builder() .setLanguage(language) @@ -208,9 +200,11 @@ object VectorLocale { Locale(language, country) } } + // sort by human display names + .sortedBy { localeToLocalisedString(it).toLowerCase(it) } - // sort by human display names - supportedLocales.sortWith(Comparator { lhs, rhs -> localeToLocalisedString(lhs).compareTo(localeToLocalisedString(rhs)) }) + supportedLocales.clear() + supportedLocales.addAll(list) } /** @@ -235,22 +229,39 @@ object VectorLocale { append(locale.getDisplayCountry(locale)) append(")") } - - // In debug mode, also display information about the locale in the current locale. - if (BuildConfig.DEBUG) { - append("\n[") - append(locale.displayLanguage) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { - append(" - ") - append(locale.displayScript) - } - if (locale.displayCountry.isNotEmpty()) { - append(" (") - append(locale.displayCountry) - append(")") - } - append("]") - } } } + + /** + * Information about the locale in the current locale + * + * @param locale the locale to get info from + * @return the string + */ + fun localeToLocalisedStringInfo(locale: Locale): String { + return buildString { + append("[") + append(locale.displayLanguage) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && locale.script != "Latn") { + append(" - ") + append(locale.displayScript) + } + if (locale.displayCountry.isNotEmpty()) { + append(" (") + append(locale.displayCountry) + append(")") + } + append("]") + } + } + + suspend fun getSupportedLocales(): List<Locale> { + if (supportedLocales.isEmpty()) { + // init the known locales in background + withContext(Dispatchers.IO) { + initApplicationLocales() + } + } + return supportedLocales + } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index c995c4d986..1455e2f8d8 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -88,6 +88,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY" private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY" private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY" + private const val SETTINGS_SHOW_REDACTED_KEY = "SETTINGS_SHOW_REDACTED_KEY" private const val SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY = "SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" 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" @@ -625,6 +626,15 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true) } + /** + * Tells if the redacted message should be shown + * + * @return true if the redacted should be shown + */ + fun showRedactedMessages(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SHOW_REDACTED_KEY, true) + } + /** * Tells if the help on room list should be shown * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 802cf7b33f..dfa88d9b87 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -79,6 +79,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { private val mPasswordPreference by lazy { findPreference<VectorPreference>(VectorPreferences.SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY)!! } + private val mIdentityServerPreference by lazy { + findPreference<VectorPreference>(VectorPreferences.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)!! + } // Local contacts private val mContactSettingsCategory by lazy { @@ -130,6 +133,10 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { // Unfortunately, this is not supported in lib v7 // it.editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + it.setOnPreferenceClickListener { + notImplemented() + true + } it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> notImplemented() @@ -162,11 +169,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { // home server findPreference<VectorPreference>(VectorPreferences.SETTINGS_HOME_SERVER_PREFERENCE_KEY)!! - .summary = session.sessionParams.homeServerConnectionConfig.homeServerUri.toString() - - // identity server - findPreference<VectorPreference>(VectorPreferences.SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY)!! - .summary = session.sessionParams.homeServerConnectionConfig.identityServerUri.toString() + .summary = session.sessionParams.homeServerUrl refreshEmailsList() refreshPhoneNumbersList() @@ -236,6 +239,13 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { } } + override fun onResume() { + super.onResume() + + // Refresh identity server summary + mIdentityServerPreference.summary = session.identityService().getCurrentIdentityServerUrl() ?: getString(R.string.identity_server_not_defined) + } + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt index 66c56455b9..eeda0167a3 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt @@ -47,7 +47,7 @@ class VectorSettingsLabsFragment @Inject constructor( // useCryptoPref.isChecked = false // // useCryptoPref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValueAsVoid -> -// if (TextUtils.isEmpty(mSession.sessionParams.credentials.deviceId)) { +// if (TextUtils.isEmpty(mSession.sessionParams.deviceId)) { // activity?.let { activity -> // AlertDialog.Builder(activity) // .setMessage(R.string.room_settings_labs_end_to_end_warnings) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt index 3f69b5880e..04908e166f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt @@ -71,11 +71,11 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor( layoutManager.orientation) mRecyclerView.addItemDecoration(dividerItemDecoration) - mSummaryButton.setOnClickListener { + mSummaryButton.debouncedClicks { bugReporter.openBugReportScreen(activity!!) } - mRunButton.setOnClickListener { + mRunButton.debouncedClicks { testManager?.retry() } startUI() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt index 9c240ad093..6b021d022f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsPreferencesFragment.kt @@ -18,13 +18,13 @@ package im.vector.riotx.features.settings import android.app.Activity import android.content.Context -import android.content.Intent import android.widget.CheckedTextView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.SwitchPreference import im.vector.riotx.R +import im.vector.riotx.core.extensions.restart import im.vector.riotx.core.preference.VectorListPreference import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.features.configuration.VectorConfiguration @@ -54,13 +54,9 @@ class VectorSettingsPreferencesFragment @Inject constructor( findPreference<VectorListPreference>(ThemeUtils.APPLICATION_THEME_KEY)!! .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - vectorConfiguration.updateApplicationTheme(newValue) + ThemeUtils.setApplicationTheme(requireContext(), newValue) // Restart the Activity - activity?.let { - // Note: recreate does not apply the color correctly - it.startActivity(it.intent) - it.finish() - } + activity?.restart() true } else { false @@ -129,21 +125,6 @@ class VectorSettingsPreferencesFragment @Inject constructor( } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (resultCode == Activity.RESULT_OK) { - when (requestCode) { - REQUEST_LOCALE -> { - activity?.let { - startActivity(it.intent) - it.finish() - } - } - } - } - } - // ============================================================================================================== // user interface management // ============================================================================================================== @@ -152,14 +133,8 @@ class VectorSettingsPreferencesFragment @Inject constructor( // Selected language selectedLanguagePreference.summary = VectorLocale.localeToLocalisedString(VectorLocale.applicationLocale) - selectedLanguagePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - notImplemented() - // TODO startActivityForResult(LanguagePickerActivity.getIntent(activity), REQUEST_LOCALE) - true - } - // Text size - textSizePreference.summary = FontScale.getFontScaleDescription(activity!!) + textSizePreference.summary = getString(FontScale.getFontScaleValue(activity!!).nameResId) textSizePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { activity?.let { displayTextSizeSelection(it) } @@ -182,25 +157,20 @@ class VectorSettingsPreferencesFragment @Inject constructor( val childCount = linearLayout.childCount - val scaleText = FontScale.getFontScaleDescription(activity) + val index = FontScale.getFontScaleValue(activity).index for (i in 0 until childCount) { val v = linearLayout.getChildAt(i) if (v is CheckedTextView) { - v.isChecked = v.text == scaleText + v.isChecked = i == index v.setOnClickListener { dialog.dismiss() - FontScale.updateFontScale(activity, v.text.toString()) - activity.startActivity(activity.intent) - activity.finish() + FontScale.updateFontScale(activity, i) + activity.restart() } } } } - - companion object { - private const val REQUEST_LOCALE = 777 - } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 394587ea5d..491890de7e 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -340,7 +340,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( showDeviceListPref.isEnabled = devices.size > 0 showDeviceListPref.summary = resources.getQuantityString(R.plurals.settings_active_sessions_count, devices.size, devices.size) // val userId = session.myUserId -// val deviceId = session.sessionParams.credentials.deviceId +// val deviceId = session.sessionParams.deviceId // device name // if (null != aMyDeviceInfo) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt index f5130d5e00..8a8a5fa4e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -80,7 +80,7 @@ class DeactivateAccountFragment @Inject constructor( viewModel.handle(DeactivateAccountAction.TogglePassword) } - deactivateAccountSubmit.setOnClickListener { + deactivateAccountSubmit.debouncedClicks { viewModel.handle(DeactivateAccountAction.DeactivateAccount( deactivateAccountPassword.text.toString(), deactivateAccountEraseCheckbox.isChecked)) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index ac4c371448..88cc4ed4bc 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -73,7 +73,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As .execute { copy( cryptoDeviceInfo = it, - isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId + isMine = it.invoke()?.deviceId == session.sessionParams.deviceId ) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index d0369e7707..c3b645787f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -97,7 +97,7 @@ class DevicesViewModel @AssistedInject constructor( copy( hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null, accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), - myDeviceId = session.sessionParams.credentials.deviceId ?: "" + myDeviceId = session.sessionParams.deviceId ?: "" ) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index f248ab1482..1f667d6993 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.settings.devtools +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading @@ -31,7 +32,6 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch data class GossipingEventsPaperTrailState( @@ -50,7 +50,7 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i setState { copy(events = Loading()) } - GlobalScope.launch { + viewModelScope.launch { session.cryptoService().getGossipingEventsTrail().let { val sorted = it.sortedByDescending { it.ageLocalTs } setState { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt index 3b273adaf4..06d9ffcf7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt @@ -16,6 +16,7 @@ package im.vector.riotx.features.settings.devtools +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState @@ -31,7 +32,6 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch data class KeyRequestListViewState( @@ -48,7 +48,7 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState } fun refresh() { - GlobalScope.launch { + viewModelScope.launch { session.cryptoService().getOutgoingRoomKeyRequest().let { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.kt new file mode 100644 index 0000000000..18c5cb2aae --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocaleItem.kt @@ -0,0 +1,48 @@ +/* + * 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.riotx.features.settings.locale + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.ClickListener +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.epoxy.onClick +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_locale) +abstract class LocaleItem : VectorEpoxyModel<LocaleItem.Holder>() { + + @EpoxyAttribute var title: String? = null + @EpoxyAttribute var subtitle: String? = null + @EpoxyAttribute var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.view.onClick { clickListener?.invoke() } + holder.titleView.setTextOrHide(title) + holder.subtitleView.setTextOrHide(subtitle) + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind<TextView>(R.id.localeTitle) + val subtitleView by bind<TextView>(R.id.localeSubtitle) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.kt new file mode 100644 index 0000000000..0bfc203159 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerAction.kt @@ -0,0 +1,24 @@ +/* + * 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.riotx.features.settings.locale + +import im.vector.riotx.core.platform.VectorViewModelAction +import java.util.Locale + +sealed class LocalePickerAction : VectorViewModelAction { + data class SelectLocale(val locale: Locale) : LocalePickerAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.kt new file mode 100644 index 0000000000..5e6704818f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerController.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.riotx.features.settings.locale + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.epoxy.noResultItem +import im.vector.riotx.core.epoxy.profiles.profileSectionItem +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.settings.VectorLocale +import im.vector.riotx.features.settings.VectorPreferences +import java.util.Locale +import javax.inject.Inject + +class LocalePickerController @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val stringProvider: StringProvider +) : TypedEpoxyController<LocalePickerViewState>() { + + var listener: Listener? = null + + @ExperimentalStdlibApi + override fun buildModels(data: LocalePickerViewState?) { + val list = data?.locales ?: return + + profileSectionItem { + id("currentTitle") + title(stringProvider.getString(R.string.choose_locale_current_locale_title)) + } + localeItem { + id(data.currentLocale.toString()) + title(VectorLocale.localeToLocalisedString(data.currentLocale).capitalize(data.currentLocale)) + if (vectorPreferences.developerMode()) { + subtitle(VectorLocale.localeToLocalisedStringInfo(data.currentLocale)) + } + clickListener { listener?.onUseCurrentClicked() } + } + profileSectionItem { + id("otherTitle") + title(stringProvider.getString(R.string.choose_locale_other_locales_title)) + } + when (list) { + is Incomplete -> { + loadingItem { + id("loading") + loadingText(stringProvider.getString(R.string.choose_locale_loading_locales)) + } + } + is Success -> + if (list().isEmpty()) { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + list() + .filter { it.toString() != data.currentLocale.toString() } + .forEach { + localeItem { + id(it.toString()) + title(VectorLocale.localeToLocalisedString(it).capitalize(it)) + if (vectorPreferences.developerMode()) { + subtitle(VectorLocale.localeToLocalisedStringInfo(it)) + } + clickListener { listener?.onLocaleClicked(it) } + } + } + } + } + } + + interface Listener { + fun onUseCurrentClicked() + fun onLocaleClicked(locale: Locale) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt new file mode 100644 index 0000000000..75d758aafa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerFragment.kt @@ -0,0 +1,80 @@ +/* + * 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.riotx.features.settings.locale + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.restart +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_locale_picker.* +import java.util.Locale +import javax.inject.Inject + +class LocalePickerFragment @Inject constructor( + private val viewModelFactory: LocalePickerViewModel.Factory, + private val controller: LocalePickerController +) : VectorBaseFragment(), LocalePickerViewModel.Factory by viewModelFactory, LocalePickerController.Listener { + + private val viewModel: LocalePickerViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_locale_picker + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + localeRecyclerView.configureWith(controller) + controller.listener = this + + viewModel.observeViewEvents { + when (it) { + LocalePickerViewEvents.RestartActivity -> { + activity?.restart() + } + }.exhaustive + } + } + + override fun onDestroyView() { + super.onDestroyView() + localeRecyclerView.cleanup() + controller.listener = null + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onUseCurrentClicked() { + // Just close the fragment + parentFragmentManager.popBackStack() + } + + override fun onLocaleClicked(locale: Locale) { + viewModel.handle(LocalePickerAction.SelectLocale(locale)) + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_select_language) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.kt new file mode 100644 index 0000000000..e007f5f036 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewEvents.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.riotx.features.settings.locale + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class LocalePickerViewEvents : VectorViewEvents { + object RestartActivity : LocalePickerViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt new file mode 100644 index 0000000000..e4cc64733c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewModel.kt @@ -0,0 +1,75 @@ +/* + * 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.riotx.features.settings.locale + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +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 +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.settings.VectorLocale +import kotlinx.coroutines.launch + +class LocalePickerViewModel @AssistedInject constructor( + @Assisted initialState: LocalePickerViewState +) : VectorViewModel<LocalePickerViewState, LocalePickerAction, LocalePickerViewEvents>(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: LocalePickerViewState): LocalePickerViewModel + } + + init { + viewModelScope.launch { + val result = VectorLocale.getSupportedLocales() + + setState { + copy( + locales = Success(result) + ) + } + } + } + + companion object : MvRxViewModelFactory<LocalePickerViewModel, LocalePickerViewState> { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: LocalePickerViewState): LocalePickerViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: LocalePickerAction) { + when (action) { + is LocalePickerAction.SelectLocale -> handleSelectLocale(action) + }.exhaustive + } + + private fun handleSelectLocale(action: LocalePickerAction.SelectLocale) { + VectorLocale.saveApplicationLocale(action.locale) + _viewEvents.post(LocalePickerViewEvents.RestartActivity) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt new file mode 100644 index 0000000000..416350d827 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/LocalePickerViewState.kt @@ -0,0 +1,28 @@ +/* + * 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.riotx.features.settings.locale + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.riotx.features.settings.VectorLocale +import java.util.Locale + +data class LocalePickerViewState( + val currentLocale: Locale = VectorLocale.applicationLocale, + val locales: Async<List<Locale>> = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt b/vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt new file mode 100644 index 0000000000..d3265f3179 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/locale/SystemLocaleProvider.kt @@ -0,0 +1,44 @@ +/* + * 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.riotx.features.settings.locale + +import android.content.Context +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject + +class SystemLocaleProvider @Inject constructor( + private val context: Context +) { + + /** + * Provides the device locale + * + * @return the device locale, or null in case of error + */ + fun getSystemLocale(): Locale? { + return try { + val packageManager = context.packageManager + val resources = packageManager.getResourcesForApplication("android") + @Suppress("DEPRECATION") + resources.configuration.locale + } catch (e: Exception) { + Timber.e(e, "## getDeviceLocale() failed") + null + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt index 5ee7b6e8a3..e2fe17b461 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutViewModel.kt @@ -58,9 +58,9 @@ class SoftLogoutViewModel @AssistedInject constructor( val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() val userId = activity.session.myUserId return SoftLogoutViewState( - homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString(), + homeServerUrl = activity.session.sessionParams.homeServerUrl, userId = userId, - deviceId = activity.session.sessionParams.credentials.deviceId ?: "", + deviceId = activity.session.sessionParams.deviceId ?: "", userDisplayName = activity.session.getUser(userId)?.displayName ?: userId, hasUnsavedKeys = activity.session.hasUnsavedKeys() ) @@ -81,8 +81,6 @@ class SoftLogoutViewModel @AssistedInject constructor( } private fun getSupportedLoginFlow() { - val homeServerConnectionConfig = session.sessionParams.homeServerConnectionConfig - currentTask?.cancel() currentTask = null authenticationService.cancelPendingLoginOrRegistration() @@ -93,7 +91,7 @@ class SoftLogoutViewModel @AssistedInject constructor( ) } - currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> { + currentTask = authenticationService.getLoginFlowOfSession(session.sessionId, object : MatrixCallback<LoginFlowResult> { override fun onFailure(failure: Throwable) { setState { copy( diff --git a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt index b8856dddb1..fa392a10ad 100755 --- a/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/riotx/features/sync/widget/SyncStateView.kt @@ -23,6 +23,7 @@ import android.widget.FrameLayout import androidx.core.view.isVisible import im.vector.matrix.android.api.session.sync.SyncState import im.vector.riotx.R +import im.vector.riotx.core.utils.isAirplaneModeOn import kotlinx.android.synthetic.main.view_sync_state.view.* class SyncStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) @@ -33,10 +34,15 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute } fun render(newState: SyncState) { - syncStateProgressBar.visibility = when (newState) { - is SyncState.Running -> if (newState.afterPause) View.VISIBLE else View.GONE - else -> View.GONE + syncStateProgressBar.isVisible = newState is SyncState.Running && newState.afterPause + + if (newState == SyncState.NoNetwork) { + val isAirplaneModeOn = isAirplaneModeOn(context) + syncStateNoNetwork.isVisible = isAirplaneModeOn.not() + syncStateNoNetworkAirplane.isVisible = isAirplaneModeOn + } else { + syncStateNoNetwork.isVisible = false + syncStateNoNetworkAirplane.isVisible = false } - syncStateNoNetwork.isVisible = newState == SyncState.NoNetwork } } diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsAction.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsAction.kt new file mode 100644 index 0000000000..852a4c3301 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsAction.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.riotx.features.terms + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class ReviewTermsAction : VectorViewModelAction { + data class LoadTerms(val preferredLanguageCode: String) : ReviewTermsAction() + data class MarkTermAsAccepted(val url: String, val accepted: Boolean) : ReviewTermsAction() + object Accept : ReviewTermsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt new file mode 100644 index 0000000000..dd7e742bd2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsActivity.kt @@ -0,0 +1,85 @@ +/* + * 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.riotx.features.terms + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.replaceFragment +import im.vector.riotx.core.platform.SimpleFragmentActivity +import javax.inject.Inject + +class ReviewTermsActivity : SimpleFragmentActivity() { + + @Inject lateinit var errorFormatter: ErrorFormatter + @Inject lateinit var viewModelFactory: ReviewTermsViewModel.Factory + + private val reviewTermsViewModel: ReviewTermsViewModel by viewModel() + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun initUiAndData() { + super.initUiAndData() + + if (isFirstCreation()) { + replaceFragment(R.id.container, ReviewTermsFragment::class.java) + } + + reviewTermsViewModel.termsArgs = intent.getParcelableExtra(EXTRA_INFO) ?: error("Missing parameter") + + reviewTermsViewModel.observeViewEvents { + when (it) { + is ReviewTermsViewEvents.Loading -> Unit + is ReviewTermsViewEvents.Failure -> { + AlertDialog.Builder(this) + .setMessage(errorFormatter.toHumanReadable(it.throwable)) + .setPositiveButton(R.string.ok) { _, _ -> + if (it.finish) { + finish() + } + } + .show() + Unit + } + ReviewTermsViewEvents.Success -> { + setResult(Activity.RESULT_OK) + finish() + } + }.exhaustive + } + } + + companion object { + const val TERMS_REQUEST_CODE = 15000 + private const val EXTRA_INFO = "EXTRA_INFO" + + fun intent(context: Context, serviceType: TermsService.ServiceType, baseUrl: String, token: String?): Intent { + return Intent(context, ReviewTermsActivity::class.java).also { + it.putExtra(EXTRA_INFO, ServiceTermsArgs(serviceType, baseUrl, token)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt new file mode 100644 index 0000000000..ecf5818300 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsFragment.kt @@ -0,0 +1,111 @@ +/* + * 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.riotx.features.terms + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.terms.TermsService +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.onClick +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import kotlinx.android.synthetic.main.fragment_review_terms.* +import javax.inject.Inject + +class ReviewTermsFragment @Inject constructor( + private val termsController: TermsController +) : VectorBaseFragment(), TermsController.Listener { + + private val reviewTermsViewModel: ReviewTermsViewModel by activityViewModel() + + override fun getLayoutResId() = R.layout.fragment_review_terms + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + termsController.description = when (reviewTermsViewModel.termsArgs.type) { + TermsService.ServiceType.IdentityService -> getString(R.string.terms_description_for_identity_server) + TermsService.ServiceType.IntegrationManager -> getString(R.string.terms_description_for_integration_manager) + } + + termsController.listener = this + reviewTermsRecyclerView.configureWith(termsController) + + reviewTermsAccept.onClick { reviewTermsViewModel.handle(ReviewTermsAction.Accept) } + reviewTermsDecline.onClick { activity?.finish() } + + reviewTermsViewModel.observeViewEvents { + when (it) { + is ReviewTermsViewEvents.Loading -> showLoading(it.message) + is ReviewTermsViewEvents.Failure -> { + // Dialog is displayed by the Activity + } + ReviewTermsViewEvents.Success -> { + // Handled by the Activity + } + }.exhaustive + } + + reviewTermsViewModel.handle(ReviewTermsAction.LoadTerms(getString(R.string.resources_language))) + } + + override fun onDestroyView() { + reviewTermsRecyclerView.cleanup() + termsController.listener = null + super.onDestroyView() + } + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.terms_of_service) + } + + override fun invalidate() = withState(reviewTermsViewModel) { state -> + termsController.setData(state) + + when (state.termsList) { + is Loading -> { + reviewTermsBottomBar.isVisible = false + } + is Success -> { + reviewTermsBottomBar.isVisible = true + reviewTermsAccept.isEnabled = state.termsList.invoke().all { it.accepted } + } + else -> Unit + } + } + + override fun retry() { + reviewTermsViewModel.handle(ReviewTermsAction.LoadTerms(getString(R.string.resources_language))) + } + + override fun setChecked(term: Term, isChecked: Boolean) { + reviewTermsViewModel.handle(ReviewTermsAction.MarkTermAsAccepted(term.url, isChecked)) + } + + override fun review(term: Term) { + openUrlInExternalBrowser(requireContext(), term.url) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewEvents.kt new file mode 100644 index 0000000000..e10966ce92 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewEvents.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.riotx.features.terms + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class ReviewTermsViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : ReviewTermsViewEvents() + data class Failure(val throwable: Throwable, val finish: Boolean) : ReviewTermsViewEvents() + object Success : ReviewTermsViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt new file mode 100644 index 0000000000..69197b7b59 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewModel.kt @@ -0,0 +1,152 @@ +/* + * 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.riotx.features.terms + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +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.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.terms.GetTermsResponse +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch +import timber.log.Timber + +class ReviewTermsViewModel @AssistedInject constructor( + @Assisted initialState: ReviewTermsViewState, + private val session: Session +) : VectorViewModel<ReviewTermsViewState, ReviewTermsAction, ReviewTermsViewEvents>(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ReviewTermsViewState): ReviewTermsViewModel + } + + companion object : MvRxViewModelFactory<ReviewTermsViewModel, ReviewTermsViewState> { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ReviewTermsViewState): ReviewTermsViewModel? { + val activity: ReviewTermsActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.viewModelFactory.create(state) + } + } + + lateinit var termsArgs: ServiceTermsArgs + + override fun handle(action: ReviewTermsAction) { + when (action) { + is ReviewTermsAction.LoadTerms -> loadTerms(action) + is ReviewTermsAction.MarkTermAsAccepted -> markTermAsAccepted(action) + ReviewTermsAction.Accept -> acceptTerms() + }.exhaustive + } + + private fun markTermAsAccepted(action: ReviewTermsAction.MarkTermAsAccepted) = withState { state -> + val newList = state.termsList.invoke()?.map { + if (it.url == action.url) { + it.copy(accepted = action.accepted) + } else { + it + } + } + + if (newList != null) { + setState { + state.copy( + termsList = Success(newList) + ) + } + } + } + + private fun acceptTerms() = withState { state -> + val acceptedTerms = state.termsList.invoke() ?: return@withState + + if (acceptedTerms.any { it.accepted.not() }) { + // Should not happen + _viewEvents.post(ReviewTermsViewEvents.Failure(IllegalStateException("Please accept all terms"), false)) + return@withState + } + + _viewEvents.post(ReviewTermsViewEvents.Loading()) + + val agreedUrls = acceptedTerms.map { it.url } + + viewModelScope.launch { + try { + awaitCallback<Unit> { + session.agreeToTerms( + termsArgs.type, + termsArgs.baseURL, + agreedUrls, + termsArgs.token, + it + ) + } + _viewEvents.post(ReviewTermsViewEvents.Success) + } catch (failure: Throwable) { + Timber.e(failure, "Failed to agree to terms") + _viewEvents.post(ReviewTermsViewEvents.Failure(failure, false)) + } + } + } + + private fun loadTerms(action: ReviewTermsAction.LoadTerms) = withState { state -> + if (state.termsList is Loading || state.termsList is Success) { + return@withState + } + + setState { + copy(termsList = Loading()) + } + + viewModelScope.launch { + try { + val data = awaitCallback<GetTermsResponse> { + session.getTerms(termsArgs.type, termsArgs.baseURL, it) + } + val terms = data.serverResponse.getLocalizedTerms(action.preferredLanguageCode).map { + Term(it.localizedUrl ?: "", + it.localizedName ?: "", + it.version, + accepted = data.alreadyAcceptedTermUrls.contains(it.localizedUrl) + ) + } + + setState { + copy( + termsList = Success(terms) + ) + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to agree to terms") + setState { + copy( + termsList = Uninitialized + ) + } + _viewEvents.post(ReviewTermsViewEvents.Failure(failure, true)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewState.kt b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewState.kt new file mode 100644 index 0000000000..9583a8f5de --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ReviewTermsViewState.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.riotx.features.terms + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class ReviewTermsViewState( + val termsList: Async<List<Term>> = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.kt b/vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.kt new file mode 100644 index 0000000000..663b1a9050 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/ServiceTermsArgs.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.riotx.features.terms + +import android.os.Parcelable +import im.vector.matrix.android.api.session.terms.TermsService +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class ServiceTermsArgs( + val type: TermsService.ServiceType, + val baseURL: String, + val token: String? = null +) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/terms/Term.kt b/vector/src/main/java/im/vector/riotx/features/terms/Term.kt new file mode 100644 index 0000000000..29fae43c64 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/Term.kt @@ -0,0 +1,24 @@ +/* + * 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.riotx.features.terms + +data class Term( + val url: String, + val name: String, + val version: String? = null, + val accepted: Boolean = false +) diff --git a/vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt b/vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt new file mode 100644 index 0000000000..d0081d6cd6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/TermItem.kt @@ -0,0 +1,60 @@ +/* + * 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.riotx.features.terms + +import android.view.View +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder + +@EpoxyModelClass(layout = R.layout.item_tos) +abstract class TermItem : EpoxyModelWithHolder<TermItem.Holder>() { + + @EpoxyAttribute + var checked: Boolean = false + + @EpoxyAttribute + var name: String? = null + + @EpoxyAttribute + var description: String? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var checkChangeListener: CompoundButton.OnCheckedChangeListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + holder.checkbox.isChecked = checked + holder.title.text = name + holder.description.text = description + holder.checkbox.setOnCheckedChangeListener(checkChangeListener) + holder.view.setOnClickListener(clickListener) + } + + class Holder : VectorEpoxyHolder() { + val checkbox by bind<CheckBox>(R.id.term_accept_checkbox) + val title by bind<TextView>(R.id.term_name) + val description by bind<TextView>(R.id.term_description) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt b/vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt new file mode 100644 index 0000000000..a5518b5f15 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/terms/TermsController.kt @@ -0,0 +1,82 @@ +/* + * 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.riotx.features.terms + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import com.airbnb.mvrx.Success +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.features.discovery.settingsSectionTitleItem +import javax.inject.Inject + +class TermsController @Inject constructor( + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController<ReviewTermsViewState>() { + + var description: String? = null + var listener: Listener? = null + + override fun buildModels(data: ReviewTermsViewState?) { + data ?: return + + when (data.termsList) { + is Incomplete -> { + loadingItem { + id("loading") + } + } + is Fail -> { + errorWithRetryItem { + id("errorRetry") + text(errorFormatter.toHumanReadable(data.termsList.error)) + listener { listener?.retry() } + } + } + is Success -> buildTerms(data.termsList.invoke()) + } + } + + private fun buildTerms(termsList: List<Term>) { + settingsSectionTitleItem { + id("header") + titleResId(R.string.widget_integration_review_terms) + } + termsList.forEach { term -> + termItem { + id(term.url) + name(term.name) + description(description) + checked(term.accepted) + + clickListener(View.OnClickListener { listener?.review(term) }) + checkChangeListener { _, isChecked -> + listener?.setChecked(term, isChecked) + } + } + } + } + + interface Listener { + fun retry() + fun setChecked(term: Term, isChecked: Boolean) + fun review(term: Term) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt index 1f835164db..45e64465d6 100644 --- a/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/themes/ThemeUtils.kt @@ -52,7 +52,7 @@ object ThemeUtils { */ fun getApplicationTheme(context: Context): String { return PreferenceManager.getDefaultSharedPreferences(context) - .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE)!! + .getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE } /** diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt index 78482e0b54..42dd46bd01 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt @@ -91,7 +91,7 @@ class KnownUsersFragment @Inject constructor( } private fun setupAddByMatrixIdView() { - addByMatrixId.setOnClickListener { + addByMatrixId.debouncedClicks { sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory) } } @@ -122,7 +122,7 @@ class KnownUsersFragment @Inject constructor( } private fun setupCloseView() { - knownUsersClose.setOnClickListener { + knownUsersClose.debouncedClicks { requireActivity().finish() } } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt index 28aa2d433b..12de191b54 100644 --- a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt @@ -73,7 +73,7 @@ class UserDirectoryFragment @Inject constructor( } private fun setupCloseView() { - userDirectoryClose.setOnClickListener { + userDirectoryClose.debouncedClicks { sharedActionViewModel.post(UserDirectorySharedAction.GoBack) } } diff --git a/vector/src/main/res/drawable-xxhdpi/ic_notification_privacy_warning.png b/vector/src/main/res/drawable-xxhdpi/ic_notification_privacy_warning.png new file mode 100644 index 0000000000..fb07c14b8a Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ic_notification_privacy_warning.png differ diff --git a/vector/src/main/res/drawable/ic_trash_16.xml b/vector/src/main/res/drawable/ic_trash_16.xml new file mode 100644 index 0000000000..ca6052b447 --- /dev/null +++ b/vector/src/main/res/drawable/ic_trash_16.xml @@ -0,0 +1,41 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> + <path + android:strokeWidth="1" + android:pathData="M1.5,3.6667H3.4444H14.5" + android:strokeLineJoin="round" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:strokeWidth="1" + android:pathData="M11,3.6667L10,0.6667H6L5,3.6667" + android:strokeLineJoin="round" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:strokeWidth="1" + android:pathData="M3.5,6.1667V13.8334C3.5,14.5697 4.097,15.1667 4.8333,15.1667H11.1667C11.903,15.1667 12.5,14.5697 12.5,13.8334V6.1667" + android:strokeLineJoin="round" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:strokeWidth="1" + android:pathData="M6.5,6.1667V12.1667" + android:strokeLineJoin="round" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:strokeWidth="1" + android:pathData="M9.5,6.1667V12.1667" + android:strokeLineJoin="round" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_trash_24.xml b/vector/src/main/res/drawable/ic_trash_24.xml new file mode 100644 index 0000000000..266855d50c --- /dev/null +++ b/vector/src/main/res/drawable/ic_trash_24.xml @@ -0,0 +1,41 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M2.25,5.5H5.1667H21.75" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:pathData="M16.5,5.5L15,1H9L7.5,5.5" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:pathData="M5.25,9.25V20.75C5.25,21.8546 6.1454,22.75 7.25,22.75H16.75C17.8546,22.75 18.75,21.8546 18.75,20.75V9.25" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:pathData="M9.75,9.25V18.25" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> + <path + android:pathData="M14.25,9.25V18.25" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#2E2F32" + android:strokeLineCap="round"/> +</vector> diff --git a/vector/src/main/res/drawable/redacted_background.xml b/vector/src/main/res/drawable/redacted_background.xml deleted file mode 100644 index f253a9eaf7..0000000000 --- a/vector/src/main/res/drawable/redacted_background.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android"> - <corners android:radius="10dp" /> - <solid android:color="?vctr_redacted_message_color" /> -</shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_generic_recycler.xml b/vector/src/main/res/layout/fragment_generic_recycler.xml index 1c2dcc1c3a..bef10073fd 100644 --- a/vector/src/main/res/layout/fragment_generic_recycler.xml +++ b/vector/src/main/res/layout/fragment_generic_recycler.xml @@ -3,8 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:background="?vctr_list_header_background_color" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="?riotx_background"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" diff --git a/vector/src/main/res/layout/fragment_locale_picker.xml b/vector/src/main/res/layout/fragment_locale_picker.xml new file mode 100644 index 0000000000..1d7977d19a --- /dev/null +++ b/vector/src/main/res/layout/fragment_locale_picker.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/localeRecyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?riotx_background" + tools:listitem="@layout/item_locale" /> diff --git a/vector/src/main/res/layout/fragment_review_terms.xml b/vector/src/main/res/layout/fragment_review_terms.xml new file mode 100644 index 0000000000..d97628670c --- /dev/null +++ b/vector/src/main/res/layout/fragment_review_terms.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/reviewTermsRecyclerView" + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/reviewTermsBottomBar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:listitem="@layout/item_tos" /> + + <LinearLayout + android:id="@+id/reviewTermsBottomBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="4dp" + android:gravity="center_vertical|end" + android:orientation="horizontal" + android:paddingStart="16dp" + android:paddingTop="@dimen/layout_vertical_margin" + android:paddingEnd="16dp" + android:paddingBottom="@dimen/layout_vertical_margin" + app:layout_constraintBottom_toBottomOf="parent"> + + <Button + android:id="@+id/reviewTermsDecline" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_marginEnd="8dp" + android:text="@string/decline" /> + + <Button + android:id="@+id/reviewTermsAccept" + style="@style/VectorButtonStyle" + android:layout_width="wrap_content" + android:text="@string/accept" /> + + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_set_identity_server.xml b/vector/src/main/res/layout/fragment_set_identity_server.xml new file mode 100644 index 0000000000..20e849292d --- /dev/null +++ b/vector/src/main/res/layout/fragment_set_identity_server.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?riotx_background"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/identityServerSetDefaultNotice" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?riotx_text_primary" + android:textSize="16sp" + android:visibility="gone" + tools:text="@string/identity_server_set_default_notice" + tools:visibility="visible" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/identityServerSetDefaultSubmit" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:layout_marginTop="4dp" + android:layout_marginBottom="16dp" + android:text="@string/identity_server_set_default_submit" + android:textAllCaps="false" + android:visibility="gone" + tools:visibility="visible" /> + + <TextView + android:id="@+id/identityServerSetDefaultAlternative" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?riotx_text_primary" + android:textSize="16sp" + tools:text="@string/identity_server_set_alternative_notice" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/identityServerSetDefaultAlternativeTil" + style="@style/VectorTextInputLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + app:errorEnabled="true"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/identityServerSetDefaultAlternativeTextInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/settings_discovery_enter_identity_server" + android:imeOptions="actionDone" + android:inputType="textUri" + android:maxLines="1" + android:textColor="?riotx_text_primary" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.button.MaterialButton + android:id="@+id/identityServerSetDefaultAlternativeSubmit" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:text="@string/identity_server_set_alternative_submit" + android:textAllCaps="false" /> + + </LinearLayout> + +</ScrollView> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_locale.xml b/vector/src/main/res/layout/item_locale.xml new file mode 100644 index 0000000000..a446f23e78 --- /dev/null +++ b/vector/src/main/res/layout/item_locale.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:foreground="?attr/selectableItemBackground" + android:minHeight="64dp"> + + <TextView + android:id="@+id/localeTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + android:textColor="?riotx_text_primary" + android:textSize="16sp" + app:layout_constraintBottom_toTopOf="@+id/localeSubtitle" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="English" /> + + <TextView + android:id="@+id/localeSubtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + android:textColor="?riotx_text_secondary" + android:textSize="14sp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/localeTitle" + tools:text="details" + tools:visibility="visible" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/vector/src/main/res/layout/item_settings_button.xml b/vector/src/main/res/layout/item_settings_button.xml new file mode 100644 index 0000000000..d994a7fe99 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_button.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?riotx_background" + android:minHeight="64dp"> + + <Button + android:id="@+id/settings_item_button" + style="@style/VectorButtonStyleText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + tools:text="@string/action_change" /> + +</FrameLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_settings_button_single_line.xml b/vector/src/main/res/layout/item_settings_button_single_line.xml new file mode 100644 index 0000000000..93b5ac9b2d --- /dev/null +++ b/vector/src/main/res/layout/item_settings_button_single_line.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?riotx_background" + android:minHeight="68dp" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingTop="@dimen/layout_vertical_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + android:paddingBottom="@dimen/layout_vertical_margin"> + + <TextView + android:id="@+id/settings_item_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:textColor="?android:textColorPrimary" + android:textSize="15sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/settings_item_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:drawableLeft="@drawable/ic_notification_privacy_warning" + tools:drawableStart="@drawable/ic_notification_privacy_warning" + tools:drawableTint="@color/vector_error_color" + tools:text="foo@bar.test" /> + + <Button + android:id="@+id/settings_item_button" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/global_retry" + tools:visibility="visible" /> + + <ProgressBar + android:id="@+id/settings_item_progress" + style="?android:attr/progressBarStyle" + android:layout_width="20dp" + android:layout_height="20dp" + android:layout_marginEnd="16dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + + <Switch + android:id="@+id/settings_item_switch" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_settings_continue_cancel.xml b/vector/src/main/res/layout/item_settings_continue_cancel.xml new file mode 100644 index 0000000000..07cb168b86 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_continue_cancel.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?riotx_background" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin"> + + <Button + android:id="@+id/settings_item_cancel_button" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/cancel" + android:textColor="@color/riotx_destructive_accent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/settings_item_continue_button" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/_continue" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_settings_edit_text.xml b/vector/src/main/res/layout/item_settings_edit_text.xml new file mode 100644 index 0000000000..a1b3b26bb5 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_edit_text.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?riotx_background" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin"> + + <TextView + android:id="@+id/settings_item_edit_text_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:orientation="vertical" + android:textColor="?android:textColorSecondary" + android:textSize="15sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/settings_text_message_sent" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/settings_item_edit_text_til" + style="@style/VectorTextInputLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:errorEnabled="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/settings_item_edit_text_description"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/settings_item_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone|flagNoPersonalizedLearning" + android:inputType="numberDecimal" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + tools:text="1234" /> + + </com.google.android.material.textfield.TextInputLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_settings_helper_info.xml b/vector/src/main/res/layout/item_settings_helper_info.xml new file mode 100644 index 0000000000..f52e3564c5 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_helper_info.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/settings_helper_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:drawablePadding="8dp" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingTop="8dp" + android:paddingEnd="@dimen/layout_horizontal_margin" + android:paddingBottom="8dp" + android:textColor="?android:textColorSecondary" + android:textSize="14sp" + tools:drawableStart="@drawable/vector_warning_red" + tools:text="If you don’t want this, opt out below. You can also manage any of these preferences in Settings." /> diff --git a/vector/src/main/res/layout/item_settings_information.xml b/vector/src/main/res/layout/item_settings_information.xml new file mode 100644 index 0000000000..4108d50fb8 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_information.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?riotx_background" + android:foreground="?attr/selectableItemBackground" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin"> + + <TextView + android:id="@+id/settings_item_information" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:textSize="12sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/settings_discovery_confirm_mail" + tools:textColor="@color/vector_info_color" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_settings_progress.xml b/vector/src/main/res/layout/item_settings_progress.xml new file mode 100644 index 0000000000..36ca0ae606 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_progress.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?riotx_background" + android:minHeight="48dp" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin"> + + <ProgressBar + android:id="@+id/settings_item_enter_progress" + style="?android:attr/progressBarStyle" + android:layout_width="20dp" + android:layout_height="20dp" + android:layout_marginEnd="26dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_settings_section_title.xml b/vector/src/main/res/layout/item_settings_section_title.xml new file mode 100644 index 0000000000..616e62eae9 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_section_title.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/settings_section_title_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?vctr_list_header_background_color" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingTop="@dimen/layout_vertical_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + android:paddingBottom="@dimen/layout_vertical_margin" + android:textColor="?riotx_text_primary" + android:textSize="18sp" + android:textStyle="bold" + tools:text="Title" /> diff --git a/vector/src/main/res/layout/item_settings_simple_item.xml b/vector/src/main/res/layout/item_settings_simple_item.xml new file mode 100644 index 0000000000..2aeda8c295 --- /dev/null +++ b/vector/src/main/res/layout/item_settings_simple_item.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorBackgroundFloating" + android:orientation="horizontal" + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingTop="@dimen/layout_vertical_margin" + android:paddingEnd="@dimen/layout_horizontal_margin" + android:paddingBottom="@dimen/layout_vertical_margin"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:id="@+id/settings_item_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:orientation="vertical" + android:textColor="?android:textColorPrimary" + android:textSize="15sp" + android:textStyle="bold" + tools:text="Title" /> + + <TextView + android:id="@+id/settings_item_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:orientation="vertical" + android:textColor="?android:textColorSecondary" + android:textSize="15sp" + tools:text="Description / Value" /> + </LinearLayout> + + <Switch + android:id="@+id/settings_item_switch" + android:layout_width="50dp" + android:layout_height="50dp" + android:layout_gravity="center" /> + +</LinearLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 3ae80424cc..7cc929306e 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -66,16 +66,15 @@ android:id="@+id/decorationSpace" android:layout_width="4dp" android:layout_height="8dp" - android:layout_toEndOf="@id/messageStartGuideline" - /> + android:layout_toEndOf="@id/messageStartGuideline" /> <ImageView android:id="@+id/messageE2EDecoration" android:layout_width="16dp" android:layout_height="16dp" android:layout_alignTop="@id/viewStubContainer" - android:layout_marginTop="7dp" android:layout_alignEnd="@id/decorationSpace" + android:layout_marginTop="7dp" android:visibility="gone" tools:src="@drawable/ic_shield_warning" tools:visibility="visible" /> @@ -119,7 +118,7 @@ <ViewStub android:id="@+id/messageContentRedactedStub" style="@style/TimelineContentStubBaseParams" - android:layout_height="20dp" + android:layout_height="wrap_content" android:layout_marginEnd="56dp" android:layout="@layout/item_timeline_event_redacted_stub" /> diff --git a/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml index 2f930577f0..acc60e6590 100644 --- a/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml @@ -1,4 +1,11 @@ -<View xmlns:android="http://schemas.android.com/apk/res/android" +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="20dp" - android:background="@drawable/redacted_background" /> \ No newline at end of file + android:layout_height="wrap_content" + android:drawableStart="@drawable/ic_trash_16" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:text="@string/event_redacted" + android:textColor="?riotx_text_primary_body_contrast" + android:textSize="14sp" + app:drawableTint="?riotx_text_primary_body_contrast" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_tos.xml b/vector/src/main/res/layout/item_tos.xml new file mode 100644 index 0000000000..c38a71d23a --- /dev/null +++ b/vector/src/main/res/layout/item_tos.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:padding="16dp"> + + <CheckBox + android:id="@+id/term_accept_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/term_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:textColor="?riotx_text_primary" + android:textStyle="bold" + app:layout_constraintEnd_toStartOf="@id/term_policy_arrow" + app:layout_constraintStart_toEndOf="@id/term_accept_checkbox" + app:layout_constraintTop_toTopOf="parent" + tools:text="Integration Manager" /> + + <TextView + android:id="@+id/term_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:textColor="?riotx_text_secondary" + app:layout_constraintEnd_toStartOf="@id/term_policy_arrow" + app:layout_constraintStart_toStartOf="@+id/term_name" + app:layout_constraintTop_toBottomOf="@+id/term_name" + tools:text="Use bots, bridges, widget and sticker packs." /> + + <!-- Do not use drawableEnd on the TextView because of RTL support --> + <ImageView + android:id="@+id/term_policy_arrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:rotationY="@integer/rtl_mirror_flip" + android:src="@drawable/ic_material_chevron_right_black" + android:tint="?riotx_text_primary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_sync_state.xml b/vector/src/main/res/layout/view_sync_state.xml index bc828045fe..0e7ddabc21 100644 --- a/vector/src/main/res/layout/view_sync_state.xml +++ b/vector/src/main/res/layout/view_sync_state.xml @@ -33,6 +33,19 @@ android:text="@string/no_connectivity_to_the_server_indicator" android:textColor="@color/white" android:visibility="gone" + tools:layout_marginTop="10dp" + tools:visibility="visible" /> + + <TextView + android:id="@+id/syncStateNoNetworkAirplane" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/notification_accent_color" + android:gravity="center" + android:text="@string/no_connectivity_to_the_server_indicator_airplane" + android:textColor="@color/white" + android:visibility="gone" + tools:layout_marginTop="10dp" tools:visibility="visible" /> </merge> \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_submit.xml b/vector/src/main/res/menu/menu_submit.xml new file mode 100644 index 0000000000..5539cf4307 --- /dev/null +++ b/vector/src/main/res/menu/menu_submit.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_submit" + android:icon="@drawable/ic_material_done" + android:title="@string/ok" + app:iconTint="@color/riotx_accent" + app:showAsAction="always" /> + +</menu> \ No newline at end of file diff --git a/vector/src/main/res/values-da/strings.xml b/vector/src/main/res/values-da/strings.xml index 62383f40bb..411a8d4099 100644 --- a/vector/src/main/res/values-da/strings.xml +++ b/vector/src/main/res/values-da/strings.xml @@ -378,7 +378,7 @@ Er du sikker?</string> <string name="start_new_chat_prompt_msg">Er du sikker på, at du ønsker at starte en ny chat med %s?</string> <string name="start_voice_call_prompt_msg">Er du sikker på, at du ønsker at starte et opkald?</string> - <string name="start_video_call_prompt_msg">Er du sikker på, at du ønsker at du ønsker at starte et videoopkald?</string> + <string name="start_video_call_prompt_msg">Er du sikker på, at du ønsker at starte et videoopkald\?</string> <string name="groups_invite_header">Inviter</string> <string name="groups_list">Liste over grupper</string> diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index c452fbc901..1071b975d3 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -37,8 +37,8 @@ <string name="ongoing_conference_call_voice">Sprache</string> <string name="ongoing_conference_call_video">Video</string> <string name="cannot_start_call">Das Gespräch kann nicht gestartet werden, bitte später erneut versuchen</string> - <string name="missing_permissions_warning">Aufgrund fehlender Berechtigungen stehen eventuell einige Funktionen vielleicht nicht zur Verfügung…</string> - <string name="missing_permissions_to_start_conf_call">Dir fehlt das Recht ein Konferenzgespräch in diesem Raum zu starten</string> + <string name="missing_permissions_warning">Aufgrund fehlender Berechtigungen stehen einige Funktionen eventuell nicht zur Verfügung…</string> + <string name="missing_permissions_to_start_conf_call">Ihnen fehlt die Berechtigung, ein Konferenzgespräch in diesem Raum zu starten</string> <string name="missing_permissions_title_to_start_conf_call">Kann Gespräch nicht starten</string> <string name="device_information">Sitzungsinformationen</string> <string name="room_no_conference_call_in_encrypted_rooms">Konferenzgespräche werden in verschlüsselten Räumen nicht unterstützt</string> @@ -84,7 +84,7 @@ <string name="local_address_book_header">Lokales Adressbuch</string> <string name="matrix_only_filter">Nur Matrix-Kontakte</string> <string name="no_conversation_placeholder">Keine Konversationen</string> - <string name="no_contact_access_placeholder">Du hast Riot nicht erlaubt auf deine lokalen Kontakte zuzugreifen</string> + <string name="no_contact_access_placeholder">Sie haben Riot nicht erlaubt, auf lokale Kontakte zuzugreifen</string> <string name="no_result_placeholder">Keine Ergebnisse</string> <!-- Rooms fragment --> @@ -101,10 +101,10 @@ <string name="send_bug_report_include_crash_logs">Absturzberichte übermitteln</string> <string name="send_bug_report_include_screenshot">Screenshot übermitteln</string> <string name="send_bug_report">Problem melden</string> - <string name="send_bug_report_description">Bitte beschreibe das Problem. Was hast du genau gemacht? Was sollte passieren? Was passierte tatsächlich?</string> + <string name="send_bug_report_description">Bitte beschreiben Sie das Problem. Was haben Sie genau gemacht\? Was sollte passieren\? Was passierte tatsächlich\?</string> <string name="send_bug_report_placeholder">Problembeschreibung</string> - <string name="send_bug_report_logs_description">Um Probleme diagnostizieren zu können, werden Protokolle dieses Clients zusammen mit dem Fehlerbericht übermittelt. Dieser Fehlerbericht wird, wie die Protokolle und Screenshot, nicht öffentlich sichtbar sein. Wenn du nur den oben eingegebenen Text senden möchtest, bitte die nachfolgenden Haken entsprechend entfernen:</string> - <string name="send_bug_report_alert_message">Du scheinst dein Telefon frustriert zu schütteln. Möchtest du das Fenster zum Senden eines Fehlerberichts öffnen?</string> + <string name="send_bug_report_logs_description">Um Probleme diagnostizieren zu können, werden Protokolle dieses Clients zusammen mit dem Fehlerbericht übermittelt. Dieser Fehlerbericht wird, wie die Protokolle und der Screenshot, nicht öffentlich sichtbar sein. Wenn Sie nur den oben eingegebenen Text senden möchten, bitte die nachfolgenden Haken entsprechend entfernen:</string> + <string name="send_bug_report_alert_message">Sie scheinen Ihr Telefon frustriert zu schütteln. Möchten Sie das Fenster zum Senden eines Fehlerberichts öffnen\?</string> <string name="send_bug_report_sent">Der Fehlerbericht wurde erfolgreich übermittelt</string> <string name="send_bug_report_failed">Der Fehlerbericht konnte nicht übermittelt werden (%s)</string> <string name="send_bug_report_progress">Fortschritt (%s%%)</string> @@ -124,7 +124,7 @@ <string name="start_new_chat">Neuen Chat starten</string> <string name="start_voice_call">Sprachanruf starten</string> - <string name="start_video_call">Video-Anruf starten</string> + <string name="start_video_call">Videoanruf starten</string> <string name="option_send_files">Dateien senden</string> <string name="option_take_photo_video">Foto oder Video aufnehmen</string> @@ -136,7 +136,7 @@ <string name="auth_skip">Überspringen</string> <string name="auth_send_reset_email">Rücksetz-E-Mail senden</string> <string name="auth_return_to_login">Zur Anmeldemaske zurückkehren</string> - <string name="auth_user_id_placeholder">E-Mail oder Nutzername</string> + <string name="auth_user_id_placeholder">E-Mail oder Benutzername</string> <string name="auth_password_placeholder">Passwort</string> <string name="auth_new_password_placeholder">Neues Passwort</string> <string name="auth_user_name_placeholder">Benutzername</string> @@ -147,11 +147,11 @@ <string name="auth_repeat_password_placeholder">Passwort wiederholen</string> <string name="auth_repeat_new_password_placeholder">Neues Passwort bestätigen</string> <string name="auth_invalid_login_param">Benutzername und/oder Passwort falsch</string> - <string name="auth_invalid_user_name">Nutzernamen dürfen nur Buchstaben, Nummern, Punkte, Binde- und Unterstriche enthalten</string> + <string name="auth_invalid_user_name">Benutzernamen dürfen nur Buchstaben, Nummern, Punkte, Binde- und Unterstriche enthalten</string> <string name="auth_invalid_password">Passwort zu kurz (min. 6 Zeichen)</string> <string name="auth_missing_password">Passwort fehlt</string> - <string name="auth_invalid_email">Dies sieht nicht nach einer gültigen E-Mail-Adresse aus</string> - <string name="auth_invalid_phone">Dies sieht nicht nach einer gültigen Telefonnummer aus</string> + <string name="auth_invalid_email">Dies scheint keine gültige E-Mail-Adresse zu sein</string> + <string name="auth_invalid_phone">Dies scheint keine gültige Telefonnummer zu sein</string> <string name="auth_email_already_defined">Diese E-Mail-Adresse wird bereits verwendet.</string> <string name="auth_missing_email">E-Mail-Adresse fehlt</string> <string name="auth_missing_phone">Telefonnummer fehlt</string> @@ -247,13 +247,13 @@ <string name="permissions_rationale_popup_title">Information</string> <string name="permissions_rationale_msg_storage">Riot benötigt die Berechtigung, auf deine Fotos und Videos zugreifen zu können, um Anhänge zu senden und zu speichern.\n\nBitte erlaube den Zugriff im nächsten Dialog, um Dateien von deinem Gerät zu versenden.</string> <string name="permissions_rationale_msg_camera">Riot benötigt die Berechtigung, auf deine Kamera zugreifen zu können, um Bilder aufzunehmen und Video-Anrufe durchzuführen.</string> - <string name="permissions_rationale_msg_camera_explanation"> - -Bitte erlaube den Zugriff im nächsten Dialog, um den Anruf zu durchzuführen.</string> + <string name="permissions_rationale_msg_camera_explanation">" +\n +\nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf zu durchzuführen."</string> <string name="permissions_rationale_msg_record_audio">Riot benötigt die Berechtigung, auf dein Mikrofon zugreifen zu können, um (Sprach-)Anrufe tätigen zu können.</string> - <string name="permissions_rationale_msg_record_audio_explanation"> - -Bitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen.</string> + <string name="permissions_rationale_msg_record_audio_explanation">" +\n +\nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen."</string> <string name="permissions_rationale_msg_camera_and_audio">Riot benötigt die Berechtigung, auf deine Kamera und dein Mikrofon zugreifen zu können, um Video-Anrufe durchführen zu können. Bitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen.</string> @@ -633,7 +633,7 @@ Achtung: Diese Datei wird vielleicht gelöscht, wenn die App deinstalliert wird. <string name="encryption_never_send_to_unverified_devices_title">Nur zu verifizierten Sitzungen verschlüsseln</string> <string name="encryption_never_send_to_unverified_devices_summary">Von dieser Sitzung aus keine verschlüsselten Nachrichten an nicht-verifizierte Sitzungen senden.</string> - <string name="encryption_information_not_verified">NICHT verifiziert</string> + <string name="encryption_information_not_verified">Nicht verifiziert</string> <string name="encryption_information_verified">Verifiziert</string> <string name="encryption_information_blocked">Auf der Blockierliste</string> @@ -646,8 +646,8 @@ Achtung: Diese Datei wird vielleicht gelöscht, wenn die App deinstalliert wird. <string name="encryption_information_unblock">Zulassen</string> <string name="encryption_information_verify_device">Sitzung verifizieren</string> - <string name="encryption_information_verify_device_warning">Um zu verifizieren, dass dieser Sitzung vertraut werden kann, kontaktiere bitte den/die Eigentümer!n der Sitzung über andere Kommunikationsmittel (z. B. persönlich oder telefonisch) und vergewissere dich, ob der Schüssel, den er/sie in den Benutzereinstellungen für diese Sitzung sieht, mit folgendem übereinstimmt:</string> - <string name="encryption_information_verify_device_warning2">Wenn er übereinstimmt, drücke unten den Bestätigen-Button. Stimmt er nicht überein, überwacht jemand anderes diese Sitzung und du solltest ggf. den Blockieren-Button drücken. In Zukunft wird dieser Bestätigungsprozess noch komfortabler gestaltet.</string> + <string name="encryption_information_verify_device_warning">Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des/der anderen Nutzer!n und bestätige:</string> + <string name="encryption_information_verify_device_warning2">Falls sie nicht übereinstimmen, wurde die Kommunikation vielleicht kompromittiert.</string> <string name="encryption_information_verify_key_match">Ich bestätige, dass die Schlüssel übereinstimmen</string> <string name="e2e_enabling_on_app_update">Riot unterstützt jetzt Ende-zu-Ende-Verschlüsselung, du musst dich jedoch erneut anmelden, um sie zu aktivieren. @@ -776,9 +776,9 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.</string <string name="groups_header">Communities</string> <string name="no_group_placeholder">Keine Gruppen</string> - <string name="start_new_chat_prompt_msg">Bist du sicher, dass du einen neuen Chat mit %s starten möchtest?</string> - <string name="start_voice_call_prompt_msg">Bist du sicher, dass du einen Sprachanruf starten möchtest?</string> - <string name="start_video_call_prompt_msg">Bist du sicher, dass du einen Videoanruf starten möchtest?</string> + <string name="start_new_chat_prompt_msg">Sind Sie sicher, dass Sie einen neuen Chat mit %s starten möchten\?</string> + <string name="start_voice_call_prompt_msg">Sind Sie sicher, dass Sie einen Sprachanruf starten möchten\?</string> + <string name="start_video_call_prompt_msg">Sind Sie sicher, dass Sie einen Videoanruf starten möchten\?</string> <string name="groups_list">Gruppenliste</string> @@ -828,7 +828,7 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.</string <string name="settings_flair">Flair</string> - <string name="send_bug_report_rage_shake">Schüttele um einen Fehler zu melden</string> + <string name="send_bug_report_rage_shake">Schüttele, um einen Fehler zu melden</string> <string name="actions">Aktionen</string> <string name="list_members">Mitglieder auflisten</string> @@ -902,9 +902,7 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.</string <string name="title_activity_choose_sticker">Sende einen Sticker</string> <string name="option_send_sticker">Sende Sticker</string> - <string name="no_sticker_application_dialog_content">Du hast aktuell keine Stickerpackete aktiviert. - -Willst du welche hinzufügen?</string> + <string name="no_sticker_application_dialog_content">Sie haben aktuell keine Stickerpakete aktiviert. Möchten Sie welche hinzufügen\?</string> <string name="settings_deactivate_account_section">Deaktiviere Account</string> <string name="settings_deactivate_my_account">Deaktivere meinen Account</string> @@ -952,7 +950,7 @@ Willst du welche hinzufügen?</string> <string name="error_empty_field_your_password">Bitte gib dein Passwort ein.</string> - <string name="send_bug_report_description_in_english">Wenn möglich, beschreibe bitte in Englisch.</string> + <string name="send_bug_report_description_in_english">Wenn möglich, schreiben Sie bitte auf Englisch.</string> <string name="room_message_placeholder_reply_to_encrypted">Sende verschlüsselte Antwort…</string> <string name="room_message_placeholder_reply_to_not_encrypted">Sende unverschlüsselte Antwort…</string> <string name="settings_preview_media_before_sending">Zeige Medien vor dem Senden</string> @@ -982,7 +980,7 @@ Willst du welche hinzufügen?</string> <string name="room_participants_action_unignore_prompt">Zeige alle Nachrichten dieses Benutzers? Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.</string> - <string name="missing_permissions_error">Nicht berechtigt diese Aktion durchzuführen.</string> + <string name="missing_permissions_error">Nicht berechtigt, diese Aktion durchzuführen.</string> <plurals name="format_time_s"> <item quantity="one">1s</item> <item quantity="other">%ds</item> @@ -1045,7 +1043,7 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.</string <string name="unknown_error">Entschuldige, ein Fehler trat auf</string> <string name="room_sliding_menu_version_x">Version %s</string> - <string name="status_theme">Status.im-Thema</string> + <string name="status_theme">Status.im-Design</string> <string name="encryption_export_notice">Bitte eine Passphrase erstellen um exportierte Schlüssel zu verschlüsseln. Du musst dieselbe Passphrase eingeben um die Schlüssel importieren zu können.</string> <string name="passphrase_create_passphrase">Erzeuge Passphrase</string> @@ -1212,7 +1210,7 @@ Um sicherzustellen, dass du nichts verpasst, lass deine Updates einfach aktivier <string name="keys_backup_is_not_finished_please_wait">Schlüsselsicherung ist nicht abgeschlossen. Bitte warten…</string> <string name="skip">Überspringen</string> - <string name="done">Erledigt</string> + <string name="done">Fertig</string> <string name="settings_notification_advanced">Erweiterte Benachrichtigungseinstellungen</string> <string name="settings_troubleshoot_test_bing_settings_title">Angepasste Einstellungen.</string> @@ -1246,7 +1244,7 @@ Um sicherzustellen, dass du nichts verpasst, lass deine Updates einfach aktivier <string name="recovery_key">Wiederherstellungsschlüssel</string> <string name="unexpected_error">Unerwarteter Fehler</string> <string name="keys_backup_setup_backup_started_title">Sicherung gestartet</string> - <string name="keys_backup_setup_skip_title">Bist du sicher?</string> + <string name="keys_backup_setup_skip_title">Sind Sie sicher\?</string> <string name="keys_backup_restore_key_enter_hint">Wiederherstellungsschlüssel eingeben</string> <string name="keys_backup_restore_setup_recovery_key">Nachrichtenwiederherstellung</string> @@ -1284,26 +1282,26 @@ Dieser Fehler ist außerhalb von Riot passiert. Es gibt kein Google-Konto auf de <string name="keys_backup_setup_backup_started_message">Deine Verschlüsselungsschlüssel werden nun im Hintergrund auf deinem Heimserver gesichert. Die initiale Sicherung kann mehrere Minuten dauern.</string> - <string name="keys_backup_setup_skip_msg">Du kannst den Zugang zu deinen Nachrichten verlieren, wenn du dich abmeldest oder das Gerät verlierst.</string> + <string name="keys_backup_setup_skip_msg">Sie verlieren möglicherweise den Zugang zu Ihren Nachrichten, wenn Sie sich abmelden oder das Gerät verlieren.</string> - <string name="keys_backup_restore_is_getting_backup_version">Rufe Backupversion ab…</string> - <string name="keys_backup_restore_with_passphrase">Nutze deine Wiederherstellungspassphrase um deinen sicheren Chatverlauf zu entschlüsseln</string> - <string name="keys_backup_restore_use_recovery_key">nutze deinen Wiederherstellungsschlüssel</string> - <string name="keys_backup_restore_with_passphrase_helper_with_link">Wen du deine Wiederherstellungspassphrase nicht weißt, kannst du %s.</string> + <string name="keys_backup_restore_is_getting_backup_version">Rufe Backup-Version ab…</string> + <string name="keys_backup_restore_with_passphrase">Nutzen Sie Ihre Wiederherstellungspassphrase, um Ihren verschlüsselten Chatverlauf lesen zu können</string> + <string name="keys_backup_restore_use_recovery_key">nutzen Sie Ihren Wiederherstellungsschlüssel</string> + <string name="keys_backup_restore_with_passphrase_helper_with_link">Wenn Sie Ihre Wiederherstellungspassphrase nicht wissen, können Sie %s.</string> - <string name="keys_backup_restore_with_recovery_key">Nutze deinen Wiederherstellungsschlüssel um deinen verschlüsselten Chatverlauf zu entschlüsseln</string> - <string name="keys_backup_restore_with_key_helper">Wiederherstellungsschlüssel verloren\? Du kannst einen neuen in den Einstellungen einrichten.</string> - <string name="keys_backup_passphrase_error_decrypt">Sicherung konnte mit dieser Passphrase nicht entschlüsselt werden. Bitte stelle sicher, dass du die korrekte Wiederherstellungspassphrase eingegeben hast.</string> - <string name="network_error_please_check_and_retry">Netzwerkfehler: Bitte überprüfe deine Verbindung und versuche es erneut.</string> + <string name="keys_backup_restore_with_recovery_key">Nutzen Sie Ihren Wiederherstellungsschlüssel, um Ihren verschlüsselten Chatverlauf lesen zu können</string> + <string name="keys_backup_restore_with_key_helper">Haben Sie Ihren Wiederherstellungsschlüssel verloren\? Sie können einen neuen in den Einstellungen einrichten.</string> + <string name="keys_backup_passphrase_error_decrypt">Sicherung konnte mit dieser Passphrase nicht entschlüsselt werden. Bitte stellen Sie sicher, dass Sie die korrekte Wiederherstellungspassphrase eingegeben haben.</string> + <string name="network_error_please_check_and_retry">Netzwerkfehler: Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.</string> - <string name="keys_backup_recovery_code_empty_error_message">Bitte gib deinen Wiederherstellungsschlüssel ein</string> - <string name="keys_backup_recovery_code_error_decrypt">Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stelle sicher, dass du den korrekten Wiederherstellungsschlüssel eingegeben hast.</string> + <string name="keys_backup_recovery_code_empty_error_message">Bitte geben Sie Ihren Wiederherstellungsschlüssel ein</string> + <string name="keys_backup_recovery_code_error_decrypt">Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stellen Sie sicher, dass Sie den korrekten Wiederherstellungsschlüssel eingegeben haben.</string> <string name="keys_backup_restore_success_title">Sicherung wiederhergestellt %s !</string> <string name="keys_backup_restore_success_description">%1$d Sitzungsschlüssel wurde(n) wiederhergestellt und %2$d vorher unbekannte(r) Schlüssel wurde(n) hinzugefügt</string> <plurals name="keys_backup_restore_success_description_part1"> <item quantity="one">Backup mit %d Schlüssel wiederhergestellt.</item> - <item quantity="other">Backup mit %d Schlüssel wiederhergestellt.</item> + <item quantity="other">Backup mit %d Schlüsseln wiederhergestellt.</item> </plurals> <plurals name="keys_backup_restore_success_description_part2"> <item quantity="one">%d neuer Schlüssel wurde dieser Sitzung hinzugefügt.</item> @@ -1330,20 +1328,20 @@ Dieser Fehler ist außerhalb von Riot passiert. Es gibt kein Google-Konto auf de <string name="keys_backup_settings_untrusted_backup">Um die Schlüsselsicherung für diese Sitzung zu verwenden, stelle sie jetzt mit deiner Passphrase oder deinem Wiederherstellungsschlüssel wieder her.</string> <string name="keys_backup_settings_delete_confirm_message">Deine gesicherten Verschlüsselungsschlüssel vom Server löschen\? Du wirst deinen Wiederherstellungsschlüssel nicht mehr nutzen können, um deinen verschlüsselten Chatverlauf zu lesen.</string> - <string name="sign_out_bottom_sheet_warning_no_backup">Beim Ausloggen gehen ihre verschlüsselten Nachrichten verloren</string> - <string name="sign_out_bottom_sheet_warning_backing_up">Schlüssel-Sicherung wird durchgeführt. Wenn sie sich jetzt ausloggen, dann gehen ihre verschlüsselten Nachrichten verloren.</string> - <string name="sign_out_bottom_sheet_warning_backup_not_active">Schlüssel-Sicherung sollte bei all deinen Sitzungen aktiviert sein um einen Verlust deiner verschlüsselten Nachrichten zu verhindern.</string> + <string name="sign_out_bottom_sheet_warning_no_backup">Beim Ausloggen gehen Ihre verschlüsselten Nachrichten verloren</string> + <string name="sign_out_bottom_sheet_warning_backing_up">Schlüssel-Sicherung wird durchgeführt. Wenn Sie sich jetzt ausloggen, dann gehen Ihre verschlüsselten Nachrichten verloren.</string> + <string name="sign_out_bottom_sheet_warning_backup_not_active">Schlüssel-Sicherung sollte bei all Ihren Sitzungen aktiviert sein, um einen Verlust Ihrer verschlüsselten Nachrichten zu verhindern.</string> <string name="sign_out_bottom_sheet_dont_want_secure_messages">Ich möchte meine verschlüsselten Nachrichten nicht</string> <string name="sign_out_bottom_sheet_backing_up_keys">Sichere Schlüssel…</string> <string name="keys_backup_activate">Schlüssel-Sicherung verwenden</string> - <string name="are_you_sure">Bist du sicher\?</string> + <string name="are_you_sure">Sind Sie sicher\?</string> <string name="backup">Sicherung</string> - <string name="sign_out_bottom_sheet_will_lose_secure_messages">Alle verschlüsselten Nachrichten gehen verloren wenn du dich ausloggst ohne diese vorher zu sichern.</string> + <string name="sign_out_bottom_sheet_will_lose_secure_messages">Alle verschlüsselten Nachrichten gehen verloren, wenn Sie sich ausloggen, ohne diese vorher zu sichern.</string> <string name="stay">Bleiben</string> <string name="abort">Abbrechen</string> - <string name="action_sign_out_confirmation_simple">Bist du sicher, dass du dich ausloggen möchtest\?</string> + <string name="action_sign_out_confirmation_simple">Sind Sie sicher, dass Sie sich ausloggen möchten\?</string> <string name="encryption_message_recovery">Wiederherstellung verschlüsselter Nachrichten</string> <string name="error_empty_field_enter_user_name">Bitte gib einen Benutzernamen ein.</string> @@ -1398,7 +1396,7 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine <string name="keys_backup_restoring_importing_keys_waiting_message">Importiere Schlüssel…</string> <string name="ignore">Ignorieren</string> - <string name="auth_login_sso">Mit single-sign-on anmelden</string> + <string name="auth_login_sso">Mit Single-Sign-On anmelden</string> <string name="login_error_unknown_host">Diese URL ist nicht erreichbar, bitte prüfen</string> <string name="login_error_ssl_handshake">Dein Gerät nutzt eine veraltetes TLS-Sicherheitsprotokoll, das anfällig für Angriffe ist. Zu deiner Sicherheit wirst du nicht in der Lage sein, dich zu verbinden</string> <string name="settings_send_message_with_enter">Schicke Nachricht mit Eingabetaste</string> @@ -1660,7 +1658,7 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine <string name="people_no_identity_server">Kein Integrationsserver konfiguriert.</string> <string name="call_failed_no_ice_title">Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen</string> - <string name="call_failed_no_ice_use_alt">Versuche es mit %s</string> + <string name="call_failed_no_ice_use_alt">Versuchen Sie es mit %s</string> <string name="call_failed_dont_ask_again">Nicht erneut fragen</string> <string name="auth_add_email_message_2">Richte eine E-Mail für die Kontowiederherstellung ein. Optional, kannst du später einrichten, dass Personen dich über diese Adresse finden.</string> @@ -1708,9 +1706,9 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine <string name="settings_discovery_bad_identity_server">Konnte keine Verbindung zum Heimserver herstellen</string> <string name="resources_script">Latn</string> - <string name="call_failed_no_ice_description">Bitte frage die Administration deines Heimservers (%1$s) um einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren. + <string name="call_failed_no_ice_description">Bitte fragen Sie den Administrator Ihres Home-Servers (%1$s) nach der Einrichtung eines TURN-Servers, damit Anrufe zuverlässig funktionieren. \n -\nAlternativ kannst du einen öffentlichen Server auf %2$s nutzen, doch wird das nicht zu zuverlässig sein und es wird deine IP-Adresse mit dem Server geteilt. Du kannst dies auch in den Einstellungen konfigurieren.</string> +\nAlternativ können Sie einen öffentlichen Server auf %2$s nutzen. Dies wird jedoch weniger zuverlässig sein und Ihre IP-Adresse gegenüber diesem Server preisgeben. Sie können dies auch in den Einstellungen anpassen.</string> <string name="login_error_no_homeserver_found">Dies ist keine Adresse eines Matrixservers</string> <string name="login_error_homeserver_not_found">Kann Home-Server nicht bei dieser URL erreichen. Bitte überprüfen</string> <string name="settings_call_ringtone_use_default_stun_sum">Wir nutzen %s als Assistenten wenn dein Home-Server keinen anbietet (Deine IP-Adresse wird während des Anrufs geteilt)</string> @@ -2315,7 +2313,7 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine <string name="bootstrap_loading_text">Dies könnte einige Sekunden dauern, gedulde dich bitte.</string> <string name="bootstrap_loading_title">Wiederherstellung einrichten.</string> <string name="your_recovery_key">Dein Wiederherstellungsschlüssel</string> - <string name="bootstrap_finish_title">Du bist fertig!</string> + <string name="bootstrap_finish_title">Geschafft!</string> <string name="keep_it_safe">Bewahre es sicher auf</string> <string name="finish">Abschließen</string> @@ -2354,4 +2352,96 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine <string name="bootstrap_crosssigning_save_cloud">Kopier es in deinen persönlichen Cloud-Speicher</string> <string name="encryption_not_enabled">Verschlüsselung ist nicht aktiviert</string> + <string name="auth_flow_not_supported">Dies kann nicht von einem mobilen Gerät erfolgen</string> + + <string name="settings_when_rooms_are_upgraded">Wenn Räume verbessert werden</string> + <string name="encryption_enabled">Verschlüsselung aktiviert</string> + <string name="encryption_enabled_tile_description">Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahre mehr & verifiziere Benutzer in deren Profil.</string> + <string name="encryption_unknown_algorithm_tile_description">Die Verschlüsselung in diesem Raum wird nicht unterstützt</string> + + <string name="qr_code_scanned_verif_waiting">Warte auf %s…</string> + + <string name="set_recovery_passphrase">%s setzen</string> + <string name="settings_troubleshoot_title">Fehlerbehebung</string> + <string name="room_created_summary_item">%s hat den Raum erstellt und konfiguriert.</string> + + <string name="qr_code_scanned_self_verif_notice">Fast geschafft! Zeigt das andere Gerät das gleiche Schild an\?</string> + <string name="qr_code_scanned_verif_waiting_notice">Fast geschafft! Warte auf Bestätigung…</string> + <string name="settings_messages_in_e2e_one_to_one">Verschlüsselte Nachrichten in 1:1 Chats</string> + <string name="room_message_placeholder">Nachricht…</string> + + <string name="security_prompt_text">Verifiziere dich & andere, um eure Chats zu schützen</string> + + <string name="bootstrap_enter_recovery">Geben Sie zum Fortfahren Ihren %s ein</string> + <string name="use_file">Datei benutzen</string> + + <string name="bootstrap_invalid_recovery_key">Dies ist kein gültiger Wiederherstellungsschlüssel</string> + <string name="recovery_key_empty_error_message">Bitte gib deinen Wiederherstellungsschlüssel ein</string> + + <string name="bootstrap_info_text">Verschlüsselte Nachrichten und Verifizierungen mit einer %s absichern und entsperren.</string> + <string name="bootstrap_skip_text">Das setzen eines Wiederherstellungspassworts ermöglicht das sichern & entsperren von verschlüsselten Nachrichten und Verifizierungen. +\n +\nWenn du kein Nachrichtenpasswort setzen willst, erzeuge stattdessen einen Nachrichtenschlüssel.</string> + <string name="bootstrap_skip_text_no_gen_key">Das setzen eines Wiederherstellungspassworts ermöglicht das sichern & entsperren von verschlüsselten Nachrichten und Verifizierungen.</string> + + + <string name="upgrade_security">Verschlüsselungsupgrade verfügbar</string> + <string name="enter_backup_passphrase">%s eingeben</string> + <string name="backup_recovery_passphrase">Wiederherstellungspasswort</string> + <string name="bootstrap_progress_checking_backup">Überprüfe Wiederherstellungsschlüssel</string> + <string name="bootstrap_progress_checking_backup_with_info">Überprüfe Sicherungsstatus (%s)</string> + <string name="bootstrap_progress_compute_curve_key">Erzeuge Kurvenschlüssel</string> + <string name="bootstrap_progress_generating_ssss">Generiere SSSS Schlüssel aus dem Passwort</string> + <string name="bootstrap_progress_generating_ssss_with_info">Generiere SSSS Schlüssel aus dem Passwort (%s)</string> + <string name="bootstrap_progress_generating_ssss_recovery">Generiere SSSS Schlüssel aus dem Wiederherstellungsschlüssel</string> + <string name="bootstrap_progress_storing_in_sss">Speichere Schlüsselbackup Schlüssel in SSSS</string> + <string name="new_session_review_with_info">%1$s (%2$s)</string> + + <string name="bootstrap_migration_enter_backup_password">Gib dein Passwort für das Schlüsselbackup ein, um fortzufahren.</string> + <string name="bootstrap_migration_use_recovery_key">nutze deinen Schlüsselbackup Wiederherstellungsschlüssel</string> + <string name="bootstrap_migration_with_passphrase_helper_with_link">Wenn du dein Schlüsselbackup Passwort nicht weißt, kannst du %s.</string> + <string name="bootstrap_migration_backup_recovery_key">Schlüsselbackup Wiederherstellungsschlüssel</string> + + <string name="settings_security_prevent_screenshots_title">Verhindere Screenshots innerhalb der Anwendung</string> + <string name="settings_security_prevent_screenshots_summary">Das Aktivieren dieser Einstellung setzt das FLAG_SECURE in allen Aktivitäten. Starte die Anwendung neu, damit die Änderung wirksam wird.</string> + + <string name="media_file_added_to_gallery">Datei wurde der Galerie hinzugefügt</string> + <string name="error_adding_media_file_to_gallery">Datei konnte nicht zur Galerie hinzugefügt werden</string> + <string name="change_password_summary">Neues Benutzerpasswort festlegen…</string> + + <string name="use_other_session_content_description">Nutze die neueste Version von Riot auf deinen anderen Geräten, Riot Web, Riot Desktop, Riot iOS, RiotX für Android oder einen anderen cross-signing fähigen Matrix client</string> + <string name="riot_desktop_web">Riot Web +\nRiot Desktop</string> + <string name="riot_ios_android">Riot iOS +\nRiot X für Android</string> + <string name="or_other_mx_capabale_client">oder einen anderen cross-signing fähigen Matrix Client</string> + <string name="use_latest_riot">Nutze die neueste Version von Riot auf deinen anderen Geräten:</string> + <string name="command_description_discard_session">Erzwingt das Verferfen der aktuell ausgehende Gruppensitzung in einem verschlüsseltem Raum</string> + <string name="command_description_discard_session_not_handled">Wird nur in verschlüsselten Räumen unterstützt</string> + <string name="enter_secret_storage_passphrase_or_key">Benutze dein %1$s oder deinen %2$s um fortzufahren.</string> + <string name="use_recovery_key">Wiederherstellungsschlüssel verwenden</string> + <string name="enter_secret_storage_input_key">Wähle deinen Wiederherstellungsschüssel, gib ihn ein oder füge ihn aus der Zwischenablage ein</string> + <string name="keys_backup_recovery_key_error_decrypt">Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stelle sicher, dass du den korrekten Wiederherstellungsschlüssel eingegeben hast.</string> + <string name="failed_to_access_secure_storage">Konnte nicht auf gesicherten Speicher zugreifen</string> + + <string name="unencrypted">Unverschlüsselt</string> + <string name="encrypted_unverified">Verschlüsselt von einem unbekannten Gerät</string> + <string name="review_logins">Überprüfe, wo du eingeloggt bist</string> + <string name="verify_other_sessions">Verifiziere alle deine Sitzungen, um sicher zu stellen, dass dein Konto & deine Nachrichten sicher sind</string> + <string name="verify_this_session">Bestätige neue Anmeldung für dein Konto: %1$s</string> + + <string name="cross_signing_verify_by_text">Verifiziere manuell mit einem Text</string> + <string name="crosssigning_verify_session">Verifiziere Anmeldung</string> + <string name="cross_signing_verify_by_emoji">Verifiziere interaktiv mit Emojis</string> + <string name="confirm_your_identity">Bestätige deine Identität, indem du diesen Login von einer deiner anderen Sitzungen verifizierst, um Zugriff auf deine verschlüsselten Nachrichten zu erhalten.</string> + <string name="mark_as_verified">Als vertraut markieren</string> + + <string name="error_empty_field_choose_user_name">Bitte wähle einen Benutzernamen.</string> + <string name="error_empty_field_choose_password">Bitte wähle ein Passwort.</string> + <string name="external_link_confirmation_title">Überprüfe diesen Link genau</string> + <string name="external_link_confirmation_message">Dieser Link %1$s bringt dich zu einer anderen Seite: %2$s. +\n +\nWillst du wirklich fortfahren\?</string> + + <string name="create_room_dm_failure">Konnte Direktnachricht nicht erzeugen. Prüfe die Nutzer, die du einladen willst und versuche es erneut.</string> </resources> diff --git a/vector/src/main/res/values-eo/strings.xml b/vector/src/main/res/values-eo/strings.xml index 8d4f587fc8..55bcd09d3a 100644 --- a/vector/src/main/res/values-eo/strings.xml +++ b/vector/src/main/res/values-eo/strings.xml @@ -5,23 +5,23 @@ <string name="dark_theme">Malhela haŭto</string> <string name="black_them">Nigra haŭto</string> - <string name="notification_sync_in_progress">Sinkroniganta</string> - <string name="notification_listening_for_events">Atentanta pri eventoj</string> + <string name="notification_sync_in_progress">Spegulante…</string> + <string name="notification_listening_for_events">Atentante eventojn</string> <string name="notification_noisy_notifications">Laŭtaj sciigoj</string> <string name="notification_silent_notifications">Silentaj sciigoj</string> <string name="title_activity_home">Mesaĝoj</string> - <string name="title_activity_room">Babilejo</string> + <string name="title_activity_room">Ĉambro</string> <string name="resources_country_code">EO</string> <string name="title_activity_settings">Agordoj</string> - <string name="title_activity_bug_report">Cimraporto</string> - <string name="title_activity_group_details">Komunumaj detaloj</string> + <string name="title_activity_bug_report">Erar-raporto</string> + <string name="title_activity_group_details">Detaloj pri komunumo</string> <string name="title_activity_choose_sticker">Sendi glumarkon</string> <string name="dialog_title_third_party_licences">Permesiloj de eksteraj liverantoj</string> - <string name="loading">Ŝarganta…</string> + <string name="loading">Enlegante…</string> <string name="ok">Bone</string> <string name="cancel">Nuligi</string> @@ -33,7 +33,7 @@ <string name="redact">Forigi</string> <string name="quote">Citi</string> <string name="download">Elŝuti</string> - <string name="share">Diskonigi</string> + <string name="share">Kunhavigi</string> <string name="speak">Paroli</string> <string name="clear">Forviŝi</string> <string name="later">Poste</string> @@ -43,40 +43,41 @@ <string name="view_decrypted_source">Vidi malĉifritan fonton</string> <string name="delete">Forigi</string> <string name="rename">Renomi</string> - <string name="title_activity_member_details">Membro-Detaloj</string> + <string name="title_activity_member_details">Detaloj pri ano</string> <string name="title_activity_historical">Historia</string> <string name="report_content">Raporti enhavon</string> - <string name="active_call">Aktiva alvoko</string> - <string name="send_bug_report">Cimraporto</string> - <string name="ongoing_conference_call">Daŭranta telekonferenco.\nAliĝi %1$s aŭ %2$s.</string> - <string name="ongoing_conference_call_voice">voĉe</string> - <string name="ongoing_conference_call_video">videe</string> - <string name="cannot_start_call">Ne eblas ekigi la alvokon, bonvolu provi poste</string> - <string name="missing_permissions_title_to_start_conf_call">Ne eblas ekigi alvokon</string> - <string name="missing_permissions_warning">Pro mankantaj permesoj, kelkaj ebloj eble mankos…</string> + <string name="active_call">Aktiva voko</string> + <string name="send_bug_report">Raporti eraron</string> + <string name="ongoing_conference_call">Daŭranta grupa voko. +\nAliĝi kiel %1$s aŭ %2$s.</string> + <string name="ongoing_conference_call_voice">Voĉe</string> + <string name="ongoing_conference_call_video">Vide</string> + <string name="cannot_start_call">Ne eblas ekigi la vokon, bonvolu provi poste</string> + <string name="missing_permissions_title_to_start_conf_call">Ne eblas ekigi vokon</string> + <string name="missing_permissions_warning">Pro mankantaj permesoj, kelkaj funkcioj eble mankos…</string> <string name="missing_permissions_error">Pro mankantaj permesoj, tiu ago ne eblas.</string> - <string name="device_information">Aparata informo</string> + <string name="device_information">Informoj pri salutaĵo</string> <string name="send_anyway">Tamen sendi</string> <string name="or">aŭ</string> <string name="invite">Inviti</string> - <string name="offline">Malinterrete</string> + <string name="offline">Eksterrete</string> <string name="action_exit">Eliri</string> <string name="actions">Agoj</string> - <string name="action_voice_call">Telefono</string> - <string name="action_video_call">videotelefono</string> + <string name="action_voice_call">Voĉvoko</string> + <string name="action_video_call">Vidvoko</string> <string name="action_global_search">Universala serĉo</string> - <string name="call_anyway">Tamen alvoki</string> + <string name="call_anyway">Tamen voki</string> <string name="action_quick_reply">Rapida respondo</string> <string name="action_open">Malfermi</string> <string name="action_close">Fermi</string> <string name="room_message_placeholder_reply_to_encrypted">Sendi ĉifritan respondon…</string> <string name="room_message_placeholder_reply_to_not_encrypted">Sendi respondon (neĉifritan)…</string> <string name="bottom_action_people">Homoj</string> - <string name="bottom_action_rooms">Babilejoj</string> + <string name="bottom_action_rooms">Ĉambroj</string> <string name="bottom_action_groups">Komunumoj</string> - <string name="home_filter_placeholder_home">Filtri nomojn de babilejoj</string> + <string name="home_filter_placeholder_home">Filtri nomojn de ĉambroj</string> <string name="home_filter_placeholder_people">Filtri homojn</string> <string name="home_filter_placeholder_rooms">Filtri nomojn de babilejoj</string> <string name="room_details_people">Homoj</string> @@ -93,9 +94,422 @@ <string name="rooms">Babilejoj</string> <string name="room_jump_to_first_unread">Salti al unua nelegita mesaĝo.</string> - <string name="title_activity_keys_backup_setup">Ŝlosilo-Sekukopio</string> - <string name="title_activity_keys_backup_restore">Usi Ŝlosilo-Sekukopion</string> - <string name="title_activity_verify_device">Kontroli aparaton</string> + <string name="title_activity_keys_backup_setup">Savkopiado de ŝlosiloj</string> + <string name="title_activity_keys_backup_restore">Uzi savkopiadon de ŝlosiloj</string> + <string name="title_activity_verify_device">Kontroli salutaĵon</string> <string name="encryption_information_verify_device">Kontroli aparaton</string> + <string name="resources_script">Latn</string> + + <string name="status_theme">Haŭto de Status.im</string> + + <string name="keys_backup_is_not_finished_please_wait">Savkopio de ŝlosiloj ne finiĝis; bonvolu atendi…</string> + <string name="sign_out_bottom_sheet_warning_no_backup">Vi perdos viajn ĉifritajn mesaĝojn se vi nun adiaŭos</string> + <string name="sign_out_bottom_sheet_warning_backing_up">Savkopio de ŝlosiloj progresas. Se vi nun adiaŭos, vi perdos aliron al viaj ĉifritaj mesaĝoj.</string> + <string name="sign_out_bottom_sheet_warning_backup_not_active">Sekura savkopiado de ŝlosiloj devus esti aktiva en ĉiuj viaj salutaĵoj por eviti perdon de aliro al viaj ĉifritaj mesaĝoj.</string> + <string name="sign_out_bottom_sheet_dont_want_secure_messages">Mi ne volas miajn ĉifritajn mesaĝojn</string> + <string name="sign_out_bottom_sheet_backing_up_keys">Savkopiante ŝlosilojn…</string> + <string name="keys_backup_activate">Uzi savkopiadon de ŝolsiloj</string> + <string name="are_you_sure">Ĉu vi certas\?</string> + <string name="backup">Savkopii</string> + <string name="sign_out_bottom_sheet_will_lose_secure_messages">Vi perdos aliron al viaj ĉifritaj mesaĝoj malse vi savkopios viajn ŝlosilojn antaŭ adiaŭo.</string> + + <string name="stay">Resti</string> + <string name="none">Neniu</string> + <string name="revoke">Senvalidigi</string> + <string name="disconnect">Malkonekti</string> + <string name="missing_permissions_to_start_conf_call">Vi bezonas permeson inviti por komenci grupan vokon en ĉi tiu ĉambro</string> + <string name="room_no_conference_call_in_encrypted_rooms">Grupaj vokoj ne estas subtenataj de ĉifrataj ĉambroj</string> + <string name="accept">Akcepti</string> + <string name="skip">Preterpasi</string> + <string name="done">Finite</string> + <string name="abort">Ĉesigi</string> + <string name="ignore">Malatenti</string> + <string name="review">Kontroli</string> + <string name="decline">Rifuzi</string> + + <string name="action_sign_out">Adiaŭi</string> + <string name="action_sign_out_confirmation_simple">Ĉu vi certe volas adiaŭi\?</string> + <string name="action_mark_all_as_read">Marki ĉion legita</string> + <string name="action_historical">Historia</string> + <string name="action_mark_room_read">Marki legita</string> + <string name="copied_to_clipboard">Kopiiĝis al tondujo</string> + <string name="disable">Malŝalti</string> + + <string name="dialog_title_confirmation">Konfirmo</string> + <string name="dialog_title_warning">Averto</string> + <string name="dialog_title_error">Eraro</string> + + <string name="bottom_action_home">Hejmo</string> + <string name="bottom_action_favourites">Elstarigitaj</string> + <string name="home_filter_placeholder_favorites">Filtri elstarigitajn</string> + <string name="home_filter_placeholder_groups">Filtri nomojn de komunumoj</string> + + <string name="invitations_header">Invitoj</string> + <string name="low_priority_header">Malalta prioritato</string> + <string name="system_alerts_header">Sistemaj avertoj</string> + + <string name="direct_chats_header">Interparoloj</string> + <string name="local_address_book_header">Loka adresaro</string> + <string name="no_conversation_placeholder">Neniuj interparoloj</string> + <string name="no_contact_access_placeholder">Vi ne permesis al Riot aliron al viaj lokaj kontaktoj</string> + <string name="no_result_placeholder">Neniuj rezultoj</string> + <string name="people_no_identity_server">Neniu identiga servilo estas agordita.</string> + + <string name="rooms_header">Ĉambroj</string> + <string name="no_room_placeholder">Neniuj ĉambroj</string> + <string name="no_public_room_placeholder">Neniuj publikaj ĉambroj disponeblas</string> + <plurals name="public_room_nb_users"> + <item quantity="one">1 uzanto</item> + <item quantity="other">%d uzantoj</item> + </plurals> + + <string name="groups_invite_header">Inviti</string> + <string name="groups_header">Komunumoj</string> + <string name="no_group_placeholder">Neniuj grupoj</string> + + <string name="send_bug_report_include_logs">Sendi protokolon</string> + <string name="send_bug_report_include_crash_logs">Sendi protokolon pri fiasko</string> + <string name="send_bug_report_include_screenshot">Sendi ekrankopion</string> + <string name="send_bug_report_description">Bonvolu priskribi la eraron. Kion vi faris\? Kion vi atendis\? Kio tamen vere okazis\?</string> + <string name="send_bug_report_description_in_english">Se tio eblas, bonvolu priskribi per la angla lingvo.</string> + <string name="send_bug_report_placeholder">Priskribu vian problemon ĉi tie</string> + <string name="send_bug_report_sent">Sukcese sendis la erar-raporton</string> + <string name="send_bug_report_failed">Malsukcesis sendi la erar-raporton (%s)</string> + <string name="send_bug_report_progress">Progreso (%s%%)</string> + + <string name="send_files_in">Sendi al</string> + <string name="read_receipt">Legite</string> + + <string name="join_room">Aliĝi al ĉambro</string> + <string name="username">Uzantonomo</string> + <string name="create_account">Krei konton</string> + <string name="login">Saluti</string> + <string name="logout">Adiaŭi</string> + <string name="hs_url">URL de hejmservilo</string> + <string name="identity_url">URL de identiga servilo</string> + <string name="search">Serĉi</string> + + <string name="start_new_chat">Komenci novan babilon</string> + <string name="start_voice_call">Komenci voĉvokon</string> + <string name="start_video_call">Komenci vidvokon</string> + + <string name="option_send_voice">Sendi voĉon</string> + + <string name="start_new_chat_prompt_msg">Ĉu vi certe volas komenci novan babilon kun %s\?</string> + <string name="start_voice_call_prompt_msg">Ĉu vi certe volas komenci novan voĉvokon\?</string> + <string name="start_video_call_prompt_msg">Ĉu vi certe volas komenci novan vidvokon\?</string> + <string name="call_failed_no_ice_title">Voko malsukcesis pro misagordita servilo</string> + <string name="call_failed_no_ice_description">Bonvolu peti de la administranto de via hejmservilo (%1$s) agordon de TURN-servilo, por dependebla funkciigo de vokoj. +\n +\nAlternative vi povas provi publikan servilon je %2$s, sed tio ne funkcios same dependeble, kaj montros vian IP-adreson al tiu servilo. Vi ankaŭ povas administri tion en la Agordoj.</string> + <string name="call_failed_no_ice_use_alt">Provu uzon de %s</string> + <string name="call_failed_dont_ask_again">Ne demandi ree</string> + + <string name="option_send_files">Sendi dosierojn</string> + <string name="option_send_sticker">Sendi glumarkon</string> + <string name="option_take_photo_video">Foti aŭ filmi</string> + <string name="option_take_photo">Foti</string> + <string name="option_take_video">Filmi</string> + + <string name="no_sticker_application_dialog_content">Vi havas neniujn ŝaltitajn glumarkarojn. +\n +\nĈu vi volas iujn aldoni nun\?</string> + + <string name="go_on_with">pluiĝi per…</string> + <string name="error_no_external_application_found">Pardonu, troviĝis neniu ekstera aplikaĵo por ĉi tiu ago.</string> + + <string name="auth_login">Saluti</string> + <string name="auth_login_sso">Saluti per ununura saluto</string> + <string name="auth_register">Krei konton</string> + <string name="auth_skip">Preterpasi</string> + <string name="auth_send_reset_email">Sendi restarigan retleteron</string> + <string name="auth_return_to_login">Reiri al salutejo</string> + <string name="auth_user_id_placeholder">Retpoŝtadreso aŭ uzantonomo</string> + <string name="auth_password_placeholder">Pasvorto</string> + <string name="auth_new_password_placeholder">Nova pasvorto</string> + <string name="auth_user_name_placeholder">Uzantonomo</string> + <string name="auth_email_placeholder">Retpoŝtadreso</string> + <string name="auth_opt_email_placeholder">Retpoŝtadreso (malnepra)</string> + <string name="auth_phone_number_placeholder">Telefonnumero</string> + <string name="auth_opt_phone_number_placeholder">Telefonnumero (malnepra)</string> + <string name="auth_repeat_password_placeholder">Ripetu pasvorton</string> + <string name="auth_repeat_new_password_placeholder">Konfirmu vian novan pasvorton</string> + <string name="auth_invalid_login_param">Malĝusta uzantonomo kaj/aŭ pasvorto</string> + <string name="auth_invalid_user_name">Uzantonomoj povas enhavi nur literojn, ciferojn, punktojn, streketojn, kaj substrekojn</string> + <string name="auth_invalid_password">Pasvorto estas tro mallonga (almenaŭ 6 signoj)</string> + <string name="auth_missing_password">Mankas pasvorto</string> + <string name="auth_invalid_email">Ĉi tio ne ŝajnas esti valida retpoŝtadreso</string> + <string name="auth_invalid_phone">Ĉi tio ne ŝajnas esti valida telefonnumero</string> + <string name="auth_email_already_defined">Ĉi tiu retpoŝtadreso jam estas difinita.</string> + <string name="auth_missing_email">Mankas retpoŝtadreso</string> + <string name="auth_missing_phone">Mankas telefonnumero</string> + <string name="auth_missing_email_or_phone">Mankas retpoŝtadreso aŭ telefonnumero</string> + <string name="auth_password_dont_match">Pasvortoj ne akordas</string> + <string name="auth_forgot_password">Ĉu vi forgesis pasvorton\?</string> + <string name="auth_use_server_options">Uzi proprajn agordojn pri servilo (altnivela)</string> + <string name="auth_email_validation_message">Bonvolu kontroli vian retpoŝton por daŭrigi la registriĝon</string> + <string name="auth_threepid_warning_message">Registriĝo per retpoŝtadreso kaj telefonnumero samtempe ankoraŭ ne estas subtenata, ĝis la «API» ekekzistos. Nur la telefonnumero estos konsiderata. +\n +\nVi povas aldoni vian retpoŝtadreson al via profilo en agordoj.</string> + <string name="auth_recaptcha_message">Ĉi tiu hejmservilo volas certiĝi, ke vi ne estas roboto</string> + <string name="auth_username_in_use">Uzantonomo jam uziĝas</string> + <string name="auth_home_server">Hejmservilo:</string> + <string name="auth_identity_server">Identiga servilo:</string> + <string name="auth_reset_password_next_step_button">Mi kontrolis mian retpoŝtadreson</string> + <string name="auth_reset_password_message">Por restarigi vian pasvorton, enigu la retpoŝtadreson ligitan al via konto:</string> + <string name="auth_reset_password_missing_email">Necesas enigi la retpoŝtadreson ligitan al via konto.</string> + <string name="auth_reset_password_missing_password">Necesas enigi novan pasvorton.</string> + <string name="auth_reset_password_email_validation_message">Retletero sendiĝis al %s. Vizitinte la enhavitan ligilon, klaku sube.</string> + <string name="auth_reset_password_error_unauthorized">Malsukcesis kontroli retpoŝtadreson: certiĝu, ke vi klakis la ligilon en la retletero</string> + <string name="auth_reset_password_success_message">Via pasvorto restariĝis. +\n +\nVi adiaŭis ĉiujn viajn salutaĵojn kaj ne plu ricevos pasivajn sciigojn. Por reŝalti sciigojn, resalutu ĉiun vian aparaton.</string> + <string name="auth_accept_policies">Bonvolu tralegi kaj akcepti la politikojn de ĉi tiu hejmservilo:</string> + + <string name="login_error_must_start_http">URL komenciĝu per http[s]://</string> + <string name="login_error_network_error">Ne povas asluti: reta eraro</string> + <string name="login_error_unable_login">Ne povas saluti</string> + <string name="login_error_registration_network_error">Ne povas registriĝi: reta eraro</string> + <string name="login_error_unable_register">Ne povas registriĝi</string> + <string name="login_error_unable_register_mail_ownership">Ne povas registriĝi: fiasko pri posedo de retpoŝtadreso</string> + <string name="login_error_forbidden">Nevalida uzantonomo/pasvorto</string> + <string name="read_receipts_list">Listo de legokonfirmoj</string> + + <string name="groups_list">Listo de grupoj</string> + + <string name="compression_options">Sendi en formato</string> + <string name="compression_opt_list_original">Originala</string> + <string name="compression_opt_list_large">Granda</string> + <string name="compression_opt_list_medium">Meza</string> + <string name="compression_opt_list_small">Malgranda</string> + + <string name="attachment_cancel_download">Ĉu nuligi la elŝuton\?</string> + <string name="attachment_cancel_upload">Ĉu nuligi la alŝuton\?</string> + <string name="attachment_remaining_time_seconds">%d s</string> + <string name="attachment_remaining_time_minutes">%1$dm %2$ds</string> + + <string name="yesterday">Hieraŭ</string> + <string name="today">Hodiaŭ</string> + + <string name="room_info_room_name">Nomo de ĉambro</string> + <string name="room_info_room_topic">Temo de ĉambro</string> + + <string name="settings_call_category">Vokoj</string> + <string name="notification_sync_init">Pravalorigante servon</string> + <plurals name="room_details_selected"> + <item quantity="one">1 elektita</item> + <item quantity="other">%d elektitaj</item> + </plurals> + <string name="malformed_id">Misformita identigilo. Ĝi devus esti retpoŝtadreso aŭ identigilo de Matrix, kiel «@lokaparto:mallokaparto»</string> + <string name="room_details_people_invited_group_name">INVITITA</string> + <string name="room_details_people_present_group_name">ALIĜINTA</string> + + <string name="room_event_action_report_prompt_reason">Kialo pro raporto de ĉi tiu enhavo</string> + <string name="room_event_action_report_prompt_ignore_user">Ĉu vi volas kaŝi ĉiujn mesaĝojn de ĉi tiu uzanto\? +\n +\nRimarku, ke tia ago reekigos la aplikaĵon kaj eble daŭros iom da tempo.</string> + <string name="room_event_action_cancel_upload">Nuligi alŝuton</string> + <string name="room_event_action_cancel_download">Nuligi elŝuton</string> + + <string name="search_hint">Serĉi</string> + <string name="search_members_hint">Filtri ĉambranojn</string> + <string name="search_no_results">Neniuj rezultoj</string> + <string name="tab_title_search_rooms">ĈAMBROJ</string> + <string name="tab_title_search_messages">MESAĜOJ</string> + <string name="room_recents_favourites">ELSTARIGITAJ</string> + <string name="room_recents_conversations">ĈAMBROJ</string> + <string name="room_recents_invites">INVITOJ</string> + <string name="room_recents_start_chat">Komenci babilon</string> + <string name="room_recents_create_room">Krei ĉambron</string> + <string name="room_recents_join_room">Aliĝi al ĉambro</string> + <string name="room_recents_join_room_title">Aliĝi al ĉambro</string> + <plurals name="directory_search_rooms"> + <item quantity="one">1 ĉambro</item> + <item quantity="other">%d ĉambroj</item> + </plurals> + <string name="room_settings_all_messages">Ĉiuj mesaĝoj</string> + <string name="room_settings_mention_only">Nur mencioj</string> + <string name="room_settings_mute">Silentigi</string> + <string name="room_settings_favourite">Elstarigi</string> + <string name="room_settings_forget">Forgesi</string> + <string name="room_sliding_menu_messages">Mesaĝoj</string> + <string name="room_sliding_menu_settings">Agordoj</string> + <string name="room_sliding_menu_version">Versio</string> + <string name="room_sliding_menu_version_x">Versio %s</string> + <string name="settings_invited_to_room">Kiam mi estas invitita al ĉambro</string> + <string name="settings_call_invitations">Invitoj al vokoj</string> + <string name="settings_messages_sent_by_bot">Mesaĝoj senditaj de roboto</string> + + <string name="settings_background_sync">Fona spegulado</string> + <string name="settings_background_fdroid_sync_mode">Reĝimo de fona spegulado (eksperimenta)</string> + <string name="settings_background_fdroid_sync_mode_battery">Optimumigita por baterio</string> + <string name="settings_background_fdroid_sync_mode_battery_description">Riot spegulos fone, per maniero konservanta la limigitajn rimedojn de la aparato (ĉefe la baterion). +\nDepende de la stato de la rimedoj de via aparato, la spegulado povus esti prokrastita de la operaciumo.</string> + <string name="settings_background_fdroid_sync_mode_real_time">Optimumigita por tujeco</string> + <string name="settings_background_fdroid_sync_mode_real_time_description">Riot spegulos fone, ripete, je preciza tempo (agordebla). +\nĈi tio influos uzadon de baterio kaj radiilo, kaj aperigos ĉiaman sciigon pri tio, ke Riot aŭskultas okazojn.</string> + <string name="settings_background_fdroid_sync_mode_disabled">Neniu fona spegulado</string> + <string name="settings_background_fdroid_sync_mode_disabled_description">Vi sciiĝos pri envenaj mesaĝoj dum la aplikaĵo estas fone.</string> + <string name="settings_background_sync_update_error">Malsukcesis ĝisdatigi agordojn.</string> + + + <string name="settings_start_on_boot">Ruliĝi je eko de sistemo</string> + <string name="settings_enable_background_sync">Ŝalti fonan speguladon</string> + <string name="settings_set_sync_timeout">Tempolimo de petoj por spegulado</string> + <string name="settings_set_workmanager_delay">Preferata intertempo de spegulado</string> + <string name="settings_set_workmanager_delay_summary">%s +\nLa spegulado povas esti prokrastita, depende de la rimedoj (baterio) aŭ la stato de la aparato (dormeto).</string> + <string name="settings_set_sync_delay">Prokrasto inter ĉiu spegulado</string> + <string name="settings_second">sekundo</string> + <string name="settings_seconds">sekundoj</string> + + <string name="settings_version">Versio</string> + <string name="settings_olm_version">Versio de olm</string> + <string name="settings_app_term_conditions">Uzokondiĉoj</string> + <string name="settings_copyright">Kopirajto</string> + <string name="settings_privacy_policy">Privateca politiko</string> + <string name="settings_clear_cache">Vakigi kaŝmemoron</string> + <string name="settings_clear_media_cache">Vakigi kaŝmemoron de vidaŭdaĵoj</string> + + <string name="settings_user_settings">Agordoj de uzanto</string> + <string name="settings_notifications">Sciigoj</string> + <string name="settings_ignored_users">Malatentataj uzantoj</string> + <string name="settings_other">Aliaj</string> + <string name="settings_advanced">Altnivelaj</string> + <string name="settings_integrations">Kunigoj</string> + <string name="settings_integrations_summary">Uzu kunigilon por administri robotojn, pontojn, fenestraĵojn kaj glumarkarojn. +\nKunigiloj ricevas datumojn pri agordoj kaj povas modifi fenestraĵojn, sendi invitojn al ĉambroj, kaj agordi povnivelojn laŭ via rajtigo.</string> + <string name="settings_cryptography">Ĉifroteĥnikaro</string> + <string name="settings_cryptography_manage_keys">Administrado de ĉifraj ŝlasiloj</string> + <string name="settings_notifications_targets">Celoj de sciigoj</string> + <string name="settings_contact">Lokaj kontaktoj</string> + <string name="settings_devices_list">Salutaĵoj</string> + <string name="settings_send_typing_notifs">Sendi sciigojn pri tajpado</string> + <string name="settings_send_markdown">MarkDown-formatado</string> + <string name="settings_send_markdown_summary">Formati mesaĝojn per la sintakso de MarkDown antaŭ sendo. Tio ebligas altnivelan formatadon, ekzemple uzon de steletoj por montri kursivan tekston.</string> + <string name="conference_call_warning_title">Averto!</string> + <string name="conference_call_warning_message">Grupa vokado ankoraŭ estas programata kaj eble ne estos dependebla.</string> + + <string name="markdown_has_been_enabled">MarkDown ŝaltiĝis.</string> + <string name="markdown_has_been_disabled">MarkDown malŝaltiĝis.</string> + + <string name="notification_silent">Silente</string> + <string name="notification_noisy">Laŭte</string> + + <string name="encrypted_message">Ĉifrita mesaĝo</string> + + <string name="create">Krei</string> + <string name="create_community">Krei komunumon</string> + <string name="community_name">Nomo de komunumo</string> + <string name="community_name_hint">Ekzemplo</string> + <string name="community_id">Identigilo de komunumo</string> + <string name="community_id_hint">ekzemplo</string> + + <string name="group_details_home">Hejmo</string> + <string name="joined">Aliĝintaj</string> + <string name="invited">Invititaj</string> + <string name="login_signup">Registriĝi</string> + <string name="login_signin">Saluti</string> + <string name="login_signin_sso">Pluiĝi per ununura saluto</string> + + <string name="login_reset_password_success_notice">Via pasvorto estas restarigita.</string> + <string name="login_reset_password_success_notice_2">Vi adiaŭis ĉiujn viajn salutaĵojn kaj ne plu ricevados pasivajn sciigojn. Por reŝalti sciigojn, resalutu per ĉiu via aparato.</string> + <string name="login_reset_password_success_submit">Reen al salutejo</string> + + <string name="login_reset_password_cancel_confirmation_title">Averto</string> + <string name="login_reset_password_cancel_confirmation_content">Via pasvorto ankoraŭ ne ŝanĝiĝis. +\n +\nĈu haltigi la ŝanĝan procedon\?</string> + + <string name="login_set_email_title">Agordi retpoŝtadreson</string> + <string name="unencrypted">Neĉifrita</string> + <string name="encrypted_unverified">Ĉifrita de nekonata aparato</string> + <string name="cross_signing_verify_by_text">Mane kontroli per teksto</string> + <string name="crosssigning_verify_session">Kontroli saluton</string> + <string name="cross_signing_verify_by_emoji">Interage kontroli per bildsignoj</string> + <string name="confirm_your_identity">Konfirmu vian identecon per kontrolo de ĉi tiu saluto el unu el viaj aliaj salutaĵoj, donante al ĝi aliron al viaj ĉifritaj mesaĝoj.</string> + <string name="mark_as_verified">Marki fidata</string> + + <string name="error_empty_field_choose_user_name">Bonvolu elekti uzantonomon.</string> + <string name="error_empty_field_choose_password">Bonvolu elekti pasvorton.</string> + <string name="external_link_confirmation_title">Bone kontrolu ĉi tiun ligilon</string> + <string name="external_link_confirmation_message">La ligilo %1$s kondukas vin al alia retejo: %2$s. +\n +\nĈu vi certe volas daŭrigi\?</string> + + <string name="create_room_dm_failure">Ni ne povis krei vian rektan babilon. Bonvolu kontroli la invitotajn uzantojn kaj reprovi.</string> + <string name="send_bug_report_logs_description">Por diagnozi problemojn, protokolo de ĉi tiu kliento sendiĝos kune kun ĉi tiu erar-raporto. Ĉi tiu erar-raporto, inkluzive la protokolon kaj la ekrankopion, ne estos publike videbla. Se vi preferus sendi nur la ĉi-supran tekston, bonvolu malmarki:</string> + <string name="send_bug_report_alert_message">Ŝajnas, ke vi kolere skuas la telefonon. Ĉu vi volas malfermi la erar-raportilon\?</string> + <string name="send_bug_report_app_crashed">La aplikaĵo lastatempe fiaskis. Ĉu vi volas malfermi la fiasko-raportilon\?</string> + <string name="send_bug_report_rage_shake">Kolere skuu por raporti eraron</string> + + <string name="login_error_invalid_home_server">Bonvolu enigi validan URL-on</string> + <string name="login_error_unknown_host">La URL ne estas atingebla, bonvolu kontroli ĝin</string> + <string name="login_error_no_homeserver_found">Ĉi tio ne estas valida adreso de servilo de Matrix</string> + <string name="login_error_homeserver_not_found">Ne povas atingi hejmservilon je ĉi tiu URL, bonvolu kontroli ĝin</string> + <string name="login_error_ssl_handshake">Via aparato uzas eksdatan sekurecan protokolon TLS, neŝirmatan kontraŭ atakoj; por via sekureco vi ne povos konektiĝi</string> + <string name="login_error_bad_json">Misformita JSON</string> + <string name="login_error_not_json">Ne enhavis valdiajn JSON-datumojn</string> + <string name="login_error_limit_exceeded">Tro multaj petoj sendiĝis</string> + <string name="login_error_user_in_use">Ĉi tiu uzantonomo jam estas uzata</string> + <string name="login_error_login_email_not_yet">La retpoŝt-ligilo, kiu ankoraŭ ne estas klakita</string> + + <string name="e2e_need_log_in_again">"Vi bezonas resaluti por generi tutvoje ĉifrajn ŝlosilojn por ĉi tiu salutaĵo kaj sendi la publikan ŝlosilon al via hejmservilo. +\nĈi tio necesas nur unufoje. +\nPardonu la maloportunon."</string> + + <string name="e2e_re_request_encryption_key">Repeti ĉifrajn ŝlosilojn de aliaj viaj salutaĵoj.</string> + + <string name="e2e_re_request_encryption_key_sent">Peto de ŝlosilo sendiĝis.</string> + + <string name="e2e_re_request_encryption_key_dialog_title">Peto sendiĝis.</string> + <string name="e2e_re_request_encryption_key_dialog_content">Bonvolu ruli Rioton sur alia aparato kiu scipovas malĉifri la mesaĝon, por ke ĝi povu sendi al vi la ŝlosilojn al ĉi tiu salutaĵo.</string> + + <string name="incoming_call">Envena voko</string> + <string name="incoming_video_call">Envena vidvoko</string> + <string name="incoming_voice_call">Envena voĉvoko</string> + <string name="call_in_progress">Voko progresas…</string> + <string name="video_call_in_progress">Vidvoko progresas…</string> + + <string name="call_error_camera_init_failed">Ne povas iniciati la filmilon</string> + <string name="media_picker_both_capture_title">Foti aŭ filmi</string> + <string name="media_picker_cannot_record_video">Ne povas filmi</string> + + <string name="permissions_rationale_popup_title">Informoj</string> + <string name="permissions_rationale_msg_storage">Riot bezonas permeson aliri viajn fotojn kaj filmojn, por sendi kaj konservi kunsendaĵojn. +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por povi sendi dosierojn el via telefono.</string> + <string name="permissions_rationale_msg_camera">Riot bezonas permeson aliri vian filmilon por foti kaj vidvoki.</string> + <string name="permissions_rationale_msg_camera_explanation">" +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por ebligi la vokon."</string> + <string name="permissions_rationale_msg_record_audio">Riot bezonas permeson aliri vian mikrofonon por fari voĉvokojn.</string> + <string name="permissions_rationale_msg_record_audio_explanation">" +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por ebligi la vokon."</string> + <string name="permissions_rationale_msg_camera_and_audio">Riot bezonsa premeson aliri viajn filmilon kaj mikrofonon por fari vidvokojn. +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por ebligi la vokon.</string> + <string name="permissions_rationale_msg_contacts">Riot povas kontroli vian adresaron por trovi aliajn uzantojn de Matrix per iliaj retpoŝtadresoj kaj telefonnumeroj. Se vi konsentas kunhavi vian adresaron por tiu celo, bonvolu permesi aliron per la sekva ŝprucpeto.</string> + <string name="permissions_msg_contacts_warning_other_androids">Riot povas kontroli vian adresaron por trovi aliajn uzantojn de Matrix per iliaj retpoŝtadresoj kaj telefonnumeroj. +\n +\nĈu vi konsentas kunhavi vian adresaron por tiu celo\?</string> + + <string name="permissions_action_not_performed_missing_permissions">Pardonu. Ago ne efektiviĝis, pro mankantaj permesoj</string> + + <string name="media_slider_saved">Konservite</string> + <string name="media_slider_saved_message">Ĉu konservi al elŝutujo\?</string> + <string name="yes">JES</string> + <string name="no">NE</string> + <string name="_continue">Daŭrigi</string> + + <string name="remove">Forigi</string> + <string name="join">Aliĝi</string> + <string name="preview">Antaŭrigardi</string> + <string name="reject">Rifuzi</string> + + <string name="list_members">Listigi ĉambranojn</string> + <string name="room_sync_in_progress">Spegulante…</string> + <string name="room_preview_invitation_format">Vi estis invitita al ĉi tiu ĉambro de %s</string> </resources> diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000..3ec6bf684a --- /dev/null +++ b/vector/src/main/res/values-et/strings.xml @@ -0,0 +1,33 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="resources_language">et</string> + <string name="resources_country_code">EE</string> + <string name="resources_script">Latn</string> + + <string name="light_theme">Hele teema</string> + <string name="dark_theme">Tume teema</string> + <string name="black_them">Must teema</string> + <string name="status_theme">Status.im teema</string> + + <string name="notification_sync_init">Käivitan teenuse</string> + <string name="notification_sync_in_progress">Sünkroniseerin…</string> + <string name="notification_listening_for_events">Kuulan, kas leidub sündmusi</string> + <string name="notification_noisy_notifications">Lärmakad teavitused</string> + <string name="notification_silent_notifications">Vaiksed teavitused</string> + + <string name="title_activity_home">Sõnumid</string> + <string name="title_activity_room">Jututuba</string> + <string name="title_activity_settings">Seadistused</string> + <string name="title_activity_member_details">Jututoa liikme üksikasjad</string> + <string name="title_activity_historical">Ajalooline</string> + <string name="title_activity_bug_report">Veateade</string> + <string name="title_activity_group_details">Kogukonna üksikasjad</string> + <string name="title_activity_choose_sticker">Saada kleeps</string> + <string name="title_activity_keys_backup_setup">Võtmete varundus</string> + <string name="title_activity_keys_backup_restore">Kasuta võtmete varundust</string> + <string name="title_activity_verify_device">Verifitseeri sessioon</string> + + <string name="keys_backup_is_not_finished_please_wait">Võtmete varundus pole veel valmis, oota natuke…</string> + <string name="encrypted_message">Krüptitud sõnum</string> + +</resources> diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index d044bb5c1a..72addef9e4 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -2075,4 +2075,217 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös <string name="verification_conclusion_not_secure">Ei turvallinen</string> <string name="sent_a_video">Video.</string> <string name="sent_an_image">Kuva.</string> + <string name="command_description_verify">Pyyntö annetun käyttäjä-ID:n vahvistamiseksi</string> + <string name="verification_conclusion_compromised">Johonkin näistä on mahdollisesti murtauduttu: +\n +\n - Kotipalvelimesi +\n - Varmentamasi toisen käyttäjän kotipalvelin +\n - Sinun tai toisen käyttäjän Internet-yhteys +\n - Sinun tai toisen käyttäjän käyttämä laite</string> + + <string name="sent_an_audio_file">Äänitiedosto</string> + <string name="sent_a_file">Tiedosto</string> + + <string name="verification_sent">Varmennus lähetetty</string> + <string name="verification_request">Varmennuspyyntö</string> + + + <string name="verification_verify_device">Varmenna istunto</string> + <string name="verification_verify_device_manually">Varmenna manuaalisesti</string> + + <string name="verification_scan_notice">Lue koodi toisen käyttäjän laitteesta varmentaaksenne toisenne tietoturvallisesti</string> + <string name="verification_scan_their_code">Lue toisen käyttäjän koodi</string> + <string name="verification_scan_emoji_title">Lukeminen ei onnistu</string> + <string name="verification_scan_emoji_subtitle">Vertailkaa emojeilla jos et ole toisen käyttäjän luona</string> + + <string name="verification_no_scan_emoji_title">Varmenna vertaamalla emojeja</string> + + <string name="verify_by_emoji_title">Varmenna emojeilla</string> + <string name="verify_by_emoji_description">Jos koodin lukeminen ei onnistu, varmenna vertaamalla lyhyttä sarjaa emojeja.</string> + + <string name="a13n_qr_code_description">QR-koodi</string> + + <string name="verification_verify_user">Varmenna %s</string> + <string name="verification_verified_user">Varmennettu %s</string> + <string name="verification_request_waiting_for">Odotetaan käyttäjää %s…</string> + <string name="verification_request_alert_description">Varmenna %s tarkistamalla teidän kummankin laitteella näkyvä koodi. +\n +\nJos mahdollista, tehkää tämä kummankin ollessa paikalla parhaan tietoturvan saavuttamiseksi.</string> + <string name="room_profile_not_encrypted_subtitle">Huoneessa olevat viesti eivät ole salattu osapuolten välisellä salauksella.</string> + <string name="room_profile_encrypted_subtitle">Huoneen viestit ovat salattu osapuolten välisellä salauksella. +\n +\nViestisi salataan niin, että vain sinä ja vastaanottaja pystytte avaamaan ne henkilökohtaisilla salausavaimillanne.</string> + <string name="room_profile_section_security">Tietoturva</string> + <string name="room_profile_section_more">Lisää</string> + <string name="room_profile_section_more_settings">Huoneen asetukset</string> + <string name="room_profile_section_more_uploads">Tiedostojen lähetys</string> + <string name="room_member_power_level_admins">Ylläpitäjät</string> + <string name="room_member_power_level_moderators">Moderaattorit</string> + <string name="room_member_power_level_users">Käyttäjät</string> + + <string name="room_member_power_level_admin_in">Ylläpitäjä %1$s:ssä</string> + <string name="room_member_power_level_moderator_in">Moderaattori %1$s:ssä</string> + <string name="room_member_jump_to_read_receipt">Siirry lukukuittaukseen</string> + + <string name="rendering_event_error_type_of_event_not_handled">RiotX ei osaa käsitellä tapahtumia joiden tyyppi on \'%1$s\'</string> + <string name="rendering_event_error_type_of_message_not_handled">RiotX ei osaa käsitellä viestejä joiden tyyppi on \'%1$s\'</string> + <string name="rendering_event_error_exception">RiotX ei osannut piirtää tapahtuman jonka tunniste on \'%1$s\' sisältöä</string> + + <string name="room_list_sharing_header_recent_rooms">Viimeaikaiset huoneet</string> + <string name="room_list_sharing_header_other_rooms">Muut huoneet</string> + + <string name="command_description_rainbow">Lähettää annetun viestin väritettynä sateenkaaren väreillä</string> + <string name="room_settings_enable_encryption">Ota käyttöön osapuolten välinen salaus</string> + <string name="room_settings_enable_encryption_warning">Salausta ei voi enää poistaa käytöstä sen jälkeen kun se on otettu käyttöön.</string> + + <string name="room_settings_enable_encryption_dialog_title">Otetaanko salaus käyttöön\?</string> + <string name="room_settings_enable_encryption_dialog_content">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.</string> + <string name="room_settings_enable_encryption_dialog_submit">Ota salaus käyttöön</string> + + <string name="verification_request_notice">Salauksen mahdollistamiseksi varmenna %s tarkastamalla kertakäyttöinen koodi.</string> + <string name="verification_request_start_notice">Tee tämä toisen käyttäjän ollessa läsnä tai käyttäkää toista viestintävälinettä tietoturvan varmistamiseksi.</string> + + <string name="verification_emoji_notice">Verratkaa emojeja ja varmistakaa että ne ovat samassa järjestyksessä kummallakin.</string> + <string name="verification_code_notice">Vertaa koodia joka näkyy toisen käyttäjän ruudulla.</string> + <string name="verification_conclusion_ok_notice">Tämän käyttäjän kanssa käyty viestintä on nyt päästä päähän salattu ja sitä ei pysty kukaan ulkopuolinen vakoilemaan.</string> + <string name="verification_conclusion_ok_self_notice">Istuntosi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi ja muut käyttäjät näkevät sen luotettuna.</string> + + + <string name="settings_active_sessions_list">Aktiiviset istunnot</string> + <string name="settings_active_sessions_show_all">Näytä kaikki istunnot</string> + <string name="settings_active_sessions_manage">Istuntojen hallinta</string> + <string name="settings_active_sessions_signout_device">Kirjaudu ulos tästä istunnosta</string> + + <string name="settings_failed_to_get_crypto_device_info">Ei salaukseen liittyvää tietoa</string> + + <string name="settings_active_sessions_verified_device_desc">Istunto on luotettu, koska olet vahvistanut sen:</string> + <string name="settings_active_sessions_unverified_device_desc">Vahvista tämä istunto jotta se merkitään luotetuksi ja se saa pääsyn salattuihin viesteihin. Jos et ole kirjautunut tähän istuntoon, tunnuksesi on saattanut vuotaa hyökkääjälle:</string> + + <plurals name="settings_active_sessions_count"> + <item quantity="one">%d käynnissä oleva istunto</item> + <item quantity="other">%d käynnissä olevaa istuntoa</item> + </plurals> + + <string name="crosssigning_verify_this_session">Varmenna tämä kirjautuminen</string> + <string name="crosssigning_other_user_not_trust">Muut käyttäjät eivät välttämättä luota siihen</string> + <string name="verification_open_other_to_verify">Käytä olemassaolevaa istuntoa tämän istunnon varmentamiseksi jotta se saa oikeudet salattuihin viesteihin.</string> + + + <string name="verification_profile_verify">Varmenna</string> + <string name="verification_profile_verified">Varmennettu</string> + <string name="verification_profile_warning">Varoitus</string> + + <string name="room_member_profile_failed_to_get_devices">Ei saatu istuntoja</string> + <string name="room_member_profile_sessions_section_title">Istunnot</string> + <string name="trusted">Luotettu</string> + <string name="not_trusted">Ei luotettu</string> + + <string name="verification_profile_device_verified_because">"Tämä istunto on luotettu salattuun viestintään koska %1$s (%2$s) varmisti sen:"</string> + <string name="verification_profile_device_new_signing">%1$s (%2$s) kirjautui sisään uuteen istuntoon:</string> + <string name="verification_profile_device_untrust_info">Tämän käyttäjän kanssa käyty viestintä merkitään virheiksi kunnes käyttäjä luottaa tähän istuntoon. Voit vaihtoehtoisesti käsin varmentaa sen.</string> + + + <string name="reset_cross_signing">Nollaa avaimet</string> + + <string name="a11y_qr_code_for_verification">QR-koodi</string> + + <string name="qr_code_scanned_by_other_notice">Melkein valmis! Näkyykä %s:lla sama kilven kuva\?</string> + <string name="qr_code_scanned_by_other_yes">Kyllä</string> + <string name="qr_code_scanned_by_other_no">Ei</string> + + <string name="no_connectivity_to_the_server_indicator">Yhteys kotipalvelimeen on poikki</string> + + <string name="settings_dev_tools">Kehittäjän työkalut</string> + <plurals name="poll_info"> + <item quantity="one">%d ääni</item> + <item quantity="other">%d ääntä</item> + </plurals> + <plurals name="poll_info_final"> + <item quantity="one">%d ääni - lopulliset tulokset</item> + <item quantity="other">%d ääntä - lopulliset tulokset</item> + </plurals> + <string name="command_description_poll">Luo yksinkertaisen äänestyksen</string> + <string name="verification_use_passphrase">Jos et pääse käsiksi olemassaolevaan istuntoon</string> + + <string name="new_signin">Uusi sisäänkirjautuminen</string> + + <string name="enter_secret_storage_passphrase_warning">Varoitus:</string> + <string name="message_action_item_redact">Poista…</string> + <string name="share_confirm_room">Haluatko lähettää tämän liitteen %1$s\?:lle\?</string> + <plurals name="send_images_with_original_size"> + <item quantity="one">Lähetä kuva alkuperäisessä koossa</item> + <item quantity="other">Lähetä kuvat alkuperäisessä koossa</item> + </plurals> + + <string name="delete_event_dialog_title">Vahvista poisto</string> + <string name="delete_event_dialog_content">Haluatko varmasti poistaa tämän tapahtuman\? Huomaa, että jos poistat huoneen nimen tai otsikon muutostapahtuman, se voi perua muutoksen.</string> + <string name="delete_event_dialog_reason_checkbox">Anna syy</string> + <string name="event_redacted_by_user_reason_with_reason">Käyttäjä poistanut tapahtuman, syynä: %1$s</string> + <string name="event_redacted_by_admin_reason_with_reason">Tapahtuma moderoitu huoneen ylläpitäjän toimesta, syynä: %1$s</string> + + <string name="keys_backup_restore_success_title_already_up_to_date">Avaimet ovat jo ajan tasalla!</string> + + <string name="login_mobile_device_riotx">RiotX Android</string> + + <string name="settings_key_requests">Avainpyynnöt</string> + + <string name="refresh">Päivitä</string> + + <string name="new_session">Uusi kirjautuminen. Olitko se sinä\?</string> + <string name="new_session_review">Paina tarkastellaksesi ja varmentaaksesi</string> + <string name="verify_new_session_was_not_me">En ollut</string> + <string name="verify_new_session_compromized">Tilillesi saatetaan olla murtauduttu</string> + + <string name="verify_cancel_self_verification_from_untrusted">Jos perut, et voi lukea salattuja viestejäsi tällä laitteella eivätkä muut käyttäjät luota siihen</string> + <string name="verify_cancel_self_verification_from_trusted">Jos perut, et voi lukea salattuja viestejäsi uudella laitteellasi eivätkä muut käyttäjät luota siihen</string> + <string name="verify_cancelled_notice">Varmenna laitteesi ohjelman asetuksista.</string> + <string name="verification_cancelled">Vahvistus peruttu</string> + + <string name="recovery_passphrase">Palautussalasana</string> + <string name="account_password">Tilin salasana</string> + + <string name="set_recovery_passphrase">Aseta %s</string> + <string name="confirm_recovery_passphrase">Vahvista %s</string> + + <string name="enter_account_password">Anna %s jatkaaksesi.</string> + + <string name="bootstrap_info_text">Pura ja salaa salatut viestit ja luottamukset asettamalla %s</string> + <string name="bootstrap_info_confirm_text">Anna %s uudestaan vahvistaaksesi sen.</string> + <string name="bootstrap_dont_reuse_pwd">Älä käytä tilisi salasanaa muualla.</string> + + + <string name="bootstrap_loading_text">Odota hetki, kiitos.</string> + <string name="bootstrap_loading_title">Alustetaan palautusta.</string> + <string name="your_recovery_key">Palautusavaimesi</string> + <string name="bootstrap_finish_title">Valmista!</string> + <string name="keep_it_safe">Pidä se turvassa</string> + <string name="finish">Lopeta</string> + + <string name="bootstrap_save_key_description">Käytä %1$s:tä turvaverkkona jos onnistut unohtamaan %2$s:n.</string> + + <string name="bootstrap_crosssigning_progress_initializing">Julkaistaan luodut identiteettiavaimet</string> + <string name="bootstrap_crosssigning_progress_pbkdf2">Luodaan salausavain salasanasta</string> + <string name="bootstrap_crosssigning_progress_default_key">Määritetään SSSS-oletusavain</string> + <string name="bootstrap_crosssigning_progress_save_msk">Synkronoidaan pääavain</string> + <string name="bootstrap_crosssigning_progress_save_usk">Synkronoidaan käyttäjän avain</string> + <string name="bootstrap_crosssigning_progress_save_ssk">Synkronoidaan allekirjoitusavain</string> + <string name="bootstrap_crosssigning_progress_key_backup">Alustetaan avainten varmuuskopiointi</string> + + + <string name="bootstrap_cross_signing_success">%2$s ja %1$s asetettu. +\n +\nPidä ne tallessa. Tarvitset ne salattujen viestiesi ja tietojesi avaamiseen, jos suljet kaikki istuntosi.</string> + + <string name="bootstrap_crosssigning_print_it">Tulosta se jos mahdollista ja säilytä tuloste turvallisessa paikassa</string> + <string name="bootstrap_crosssigning_save_usb">Tallenna se muistitikulle tai varmuuskopiolevylle talteen</string> + <string name="bootstrap_crosssigning_save_cloud">Kopioi se henkilökohtaiseen pilvitallennustilaasi</string> + + <string name="auth_flow_not_supported">Tätä ei pysty tekemään kännykällä</string> + + <string name="room_member_power_level_custom">Mukautettu</string> + <string name="room_member_power_level_custom_in">Mukautettu (%1$d) %2$s:ssä</string> + + <string name="delete_event_dialog_reason_hint">Syy poistoon</string> + + <string name="message_key">Viestin avain</string> </resources> diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 190d781f2a..13f3808328 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -588,8 +588,8 @@ Veuillez noter que cette action redémarrera l’application et pourra prendre u <string name="room_settings_addresses_disable_main_address_prompt_msg">Vous n’aurez aucune adresse principale spécifiée pour ce salon.</string> <string name="room_settings_addresses_e2e_enabled">Le chiffrement est activé sur ce salon.</string> <string name="room_settings_addresses_e2e_disabled">Le chiffrement est désactivé sur ce salon.</string> - <string name="room_settings_addresses_e2e_encryption_warning">Activer le chiffrement -(attention : ne peut pas être désactivé ensuite !)</string> + <string name="room_settings_addresses_e2e_encryption_warning">Activer le chiffrement +\n(attention : ne peut pas être désactivé ensuite !)</string> <string name="failed_to_load_timeline_position">%s a essayé de charger un point précis dans l’historique du salon mais ne l’a pas trouvé.</string> @@ -1560,7 +1560,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq <string name="send_suggestion_sent">Merci, la suggestion a bien été envoyée</string> <string name="send_suggestion_failed">Échec d’envoi de la suggestion (%s)</string> - <string name="settings_labs_show_hidden_events_in_timeline">Afficher les évènements cachés dans l’historique</string> + <string name="settings_labs_show_hidden_events_in_timeline">Afficher les évènements cachés dans les discussions</string> <string name="store_riotx_title">RiotX − Client Matrix nouvelle génération</string> <string name="store_riotx_short_description">Un client pour Matrix plus rapide et plus léger utilisant les derniers frameworks Android</string> @@ -1600,7 +1600,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq <string name="room_directory_search_hint">Nom ou identifiant (#exemple:matrix.org)</string> - <string name="labs_swipe_to_reply_in_timeline">Activer le balayement pour répondre dans l’historique</string> + <string name="labs_swipe_to_reply_in_timeline">Activer le balayement pour répondre dans les discussions</string> <string name="link_copied_to_clipboard">Lien copié dans le presse-papiers</string> @@ -2102,7 +2102,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq <string name="command_description_rainbow">Envoie le message fourni coloré comme un arc-en-ciel</string> <string name="command_description_rainbow_emote">Envoie la réaction fournie colorée comme un arc-en-ciel</string> - <string name="settings_category_timeline">Historique</string> + <string name="settings_category_timeline">Discussions</string> <string name="settings_category_composer">Éditeur de messages</string> diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index ca32ce03f2..acdb4bf5b2 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -2382,4 +2382,10 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró <string name="error_empty_field_choose_password">Kérlek válassz jelszót.</string> <string name="external_link_confirmation_title">Ezt a hivatkozást ellenőrizd le még egyszer</string> <string name="create_room_dm_failure">A közvetlen üzenetedet nem sikerült elkészíteni. Ellenőrizd azokat a felhasználókat akiket meg szeretnél hívni és próbáld újra.</string> + <string name="use_other_session_content_description">Használd a legújabb Riotot a másik eszközödön, úgy mint Riot Web, Asztali Riot, RiotX for Android vagy más eszközök közötti hitelesítést támogató másik Matrix klienst</string> + <string name="confirm_your_identity">Erősítsd meg ebben a bejelentkezésben a személyazonosságodat egy másik munkamenetből, hogy hozzáférhess a titkosított üzenetekhez.</string> + <string name="external_link_confirmation_message">%1$s hivatkozás egy másik oldalra visz: %2$s. +\n +\nFolytatod\?</string> + </resources> diff --git a/vector/src/main/res/values-nb-rNO/strings.xml b/vector/src/main/res/values-nb-rNO/strings.xml index 54cac5a112..26aa4c3ff8 100644 --- a/vector/src/main/res/values-nb-rNO/strings.xml +++ b/vector/src/main/res/values-nb-rNO/strings.xml @@ -723,4 +723,12 @@ <string name="send_files_in">Send inn i</string> <string name="hs_url">Hjemmetjener URL</string> <string name="start_new_chat">Start ny chat</string> + <string name="start_voice_call">Start lydsamtale</string> + <string name="start_video_call">Start videosamtale</string> + + <string name="option_send_voice">Send lydopptak</string> + + <string name="start_new_chat_prompt_msg">Er du sikker på du vil starte en samtale med %s\?</string> + <string name="start_voice_call_prompt_msg">Er du sikker på du vil starte en lydsamtale\?</string> + <string name="start_video_call_prompt_msg">Er du sikker på du vil starte en videosamtale\?</string> </resources> diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml old mode 100755 new mode 100644 index eac681e604..7d7e8e72e0 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1605,4 +1605,59 @@ <string name="identity_server_not_defined">U gebruikt geen identiteitsserver</string> <string name="identity_server_not_defined_for_password_reset">Er is geen identiteitsserver geconfigureerd. Dit is vereist om uw wachtwoord opnieuw in te stellen.</string> + <string name="security_warning_identity_server">Vorige versies van Riot hadden een veiligheidsfout die er voor kon zorgen dat je Identiteits Server (%1$s) toegang tot je account had. Indien je %2$s vertrouwt, dan kun je dit negeren; anders log je uit en weer in. +\n +\nLees meer details hier: +\nhttps://medium.com/@RiotChat/36b4792ea0d6</string> + + <string name="error_user_already_logged_in">Het lijkt er op dat je probeert verbinding te maken met een andere thuisserver. Wil je uitloggen\?</string> + + <string name="edit">Bewerken</string> + <string name="reply">Beantwoorden</string> + + <string name="global_retry">Opnieuw proberen</string> + <string name="room_list_empty">Betreed een kamer om de applicatie te gebruiken.</string> + <string name="send_you_invite">Heeft je een uitnodiging gestuurd</string> + <string name="invited_by">Uitgenodigd door %s</string> + + <string name="room_list_catchup_empty_title">Je bent helemaal bij!</string> + <string name="room_list_catchup_empty_body">Je hebt geen ongelezen berichten meer</string> + <string name="room_list_catchup_welcome_title">Welkom thuis!</string> + <string name="room_list_catchup_welcome_body">Ongelezen berichten inhalen</string> + <string name="room_list_people_empty_title">Gesprekken</string> + <string name="room_list_people_empty_body">Je directe gesprekken zullen hier worden weergegeven</string> + <string name="room_list_rooms_empty_title">Kamers</string> + <string name="room_list_rooms_empty_body">Je kamers zullen hier worden weergegeven</string> + + <string name="title_activity_emoji_reaction_picker">Reacties</string> + <string name="reactions_agree">Bevestigen</string> + <string name="reactions_like">Leuk vinden</string> + <string name="message_add_reaction">Reactie Toevoegen</string> + <string name="message_view_reaction">Reacties Bekijken</string> + <string name="reactions">Reacties</string> + + <string name="event_redacted_by_user_reason">Gebeurtenis verwijderd door gebruiker</string> + <string name="event_redacted_by_admin_reason">Gebeurtenis gemodereerd door gesprek beheerder</string> + <string name="last_edited_info_message">Laatst bewerkt door %1$s op %2$s</string> + + + <string name="malformed_message">Niet correcte gebeurtenis, kan niet weergeven</string> + <string name="create_new_room">Maak een nieuw gesprek aan</string> + <string name="error_no_network">Geen netwerk. Controleer uw internet verbinding.</string> + <string name="action_change">Wijzigen</string> + <string name="change_room_directory_network">Wijzig netwerk</string> + <string name="please_wait">Even wachten…</string> + <string name="group_all_communities">Alle Gemeenschappen</string> + + <string name="room_preview_no_preview">Dit gesprek kan niet worden voorvertoond</string> + <string name="room_preview_world_readable_room_not_supported_yet">De voorvertoning van wereld-leesbare gesprekken zijn nog niet ondersteund in RiotX</string> + + <string name="fab_menu_create_room">Gesprekken</string> + <string name="fab_menu_create_chat">Directe Berichten</string> + + <string name="create_room_title">Nieuw Gesprek</string> + <string name="create_room_action_create">AANMAKEN</string> + <string name="create_room_name_hint">Gespreksnaam</string> + <string name="create_room_public_title">Publiek</string> + <string name="create_room_public_description">Iedereen zal dit gesprek kunnen toetreden</string> </resources> diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index b0c1492acb..301ae3299e 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -2239,4 +2239,14 @@ Spróbuj uruchomić ponownie aplikację.</string> <string name="settings_developer_mode_summary">Tryb programisty aktywuje ukryte funkcje i może również spowodować, że aplikacja będzie mniej stabilna. Tylko dla programistów!</string> <string name="room_profile_not_encrypted_subtitle">Wiadomości w tym pokoju nie są szyfrowane end-to-end.</string> <string name="room_profile_section_more_uploads">Przesłane pliki</string> + <string name="settings_key_requests">Prośby o klucze</string> + + <string name="e2e_use_keybackup">Odblokuj historię zaszyfrowanych wiadomości</string> + + <string name="login_mobile_device_riotx">RiotX Android</string> + + <string name="refresh">Odśwież</string> + + <string name="verify_new_session_notice">Użyj tej sesji do weryfikacji nowej, nadając jej dostęp do zaszyfrowanych wiadomości.</string> + <string name="verify_new_session_was_not_me">To nie ja</string> </resources> diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 014a8ae65c..5837fa39ad 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -21,11 +21,11 @@ <string name="copy">Копировать</string> <string name="resend">Повторить отправку</string> <string name="redact">Удалить</string> - <string name="quote">Цитата</string> + <string name="quote">Цитировать</string> <string name="share">Поделиться</string> <string name="later">Позже</string> <string name="forward">Переслать</string> - <string name="permalink">Постоянная ссылка</string> + <string name="permalink">Копировать ссылку</string> <string name="view_source">Просмотр исходного кода</string> <string name="view_decrypted_source">Просмотр расшифрованного исходного кода</string> <string name="delete">Удалить</string> @@ -80,7 +80,7 @@ <string name="low_priority_header">Маловажные</string> <!-- People fragment --> - <string name="direct_chats_header">Диалоги</string> + <string name="direct_chats_header">Беседы</string> <string name="local_address_book_header">Локальные контакты</string> <string name="matrix_only_filter">Только Matrix контакты</string> <string name="no_conversation_placeholder">Нет диалогов</string> @@ -116,7 +116,7 @@ <string name="read_receipt">Прочитан</string> <string name="join_room">Войти в Комнату</string> - <string name="username">Логин</string> + <string name="username">Имя пользователя</string> <string name="create_account">Создать аккаунт</string> <string name="login">Войти</string> <string name="logout">Выйти</string> @@ -145,7 +145,7 @@ <string name="auth_email_placeholder">Адрес электронной почты</string> <string name="auth_opt_email_placeholder">Адрес электронной почты (не обязательно)</string> <string name="auth_phone_number_placeholder">Номер телефона</string> - <string name="auth_opt_phone_number_placeholder">Номер телефона (дополнительный)</string> + <string name="auth_opt_phone_number_placeholder">Номер телефона (по желанию)</string> <string name="auth_repeat_password_placeholder">Повтор пароля</string> <string name="auth_repeat_new_password_placeholder">Подтвердите ваш новый пароль</string> <string name="auth_invalid_login_param">Неверный логин и/или пароль</string> @@ -208,7 +208,7 @@ <string name="compression_opt_list_original">Оригинал</string> <string name="compression_opt_list_large">Крупный</string> <string name="compression_opt_list_medium">Средний</string> - <string name="compression_opt_list_small">Маленький</string> + <string name="compression_opt_list_small">Мелкий</string> <!-- media upload / download messages --> <string name="attachment_cancel_download">"Отменить загрузку?</string> @@ -290,7 +290,7 @@ <string name="room_title_one_member">1 пользователь</string> <!-- Chat participants --> - <string name="room_participants_leave_prompt_title">Покинуть чат</string> + <string name="room_participants_leave_prompt_title">Покинуть комнату</string> <string name="room_participants_leave_prompt_msg">Вы уверены, что хотите покинуть чат?</string> <string name="room_participants_remove_prompt_msg">Вы уверены, что хотите исключить %s из чата?</string> <string name="room_participants_create">Создать</string> @@ -460,7 +460,7 @@ <string name="settings_user_settings">Параметры пользователя</string> <string name="settings_notifications">Уведомления</string> <string name="settings_ignored_users">Игнорируемые</string> - <string name="settings_other">Другое</string> + <string name="settings_other">Другой</string> <string name="settings_advanced">Дополнительно</string> <string name="settings_cryptography">Криптография</string> <string name="settings_notifications_targets">Отправлять уведомления на</string> @@ -703,8 +703,8 @@ <string name="settings_keep_media">Сохранить медиа</string> <string name="light_theme">Светлая тема</string> - <string name="dark_theme">Темная тема</string> - <string name="black_them">Черная тема</string> + <string name="dark_theme">Тёмная тема</string> + <string name="black_them">Чёрная тема</string> <string name="settings_notification_ringtone">Звук уведомлений</string> <string name="settings_12_24_timestamps">Показывать метки времени в 12-часовом формате</string> @@ -1245,7 +1245,7 @@ <string name="keys_backup_is_not_finished_please_wait">Резервное копирование ключей не завершено, пожалуйста, подождите…</string> <string name="skip">Пропустить</string> - <string name="done">Skip</string> + <string name="done">Готово</string> <string name="settings_notification_advanced">Расширенные настройки уведомлений</string> <string name="settings_notification_advanced_summary">Установка важности уведомления по событию, Настроить звук, светодиод, вибрацию</string> @@ -1364,7 +1364,7 @@ <string name="settings_troubleshoot_test_fcm_failed_service_not_available">[%1$s] Эта ошибка вне контроля Riot. Причины могут быть разными. Возможно, это будет работать, если вы повторите попытку позже, вы также можете проверить, что службы Google Play не ограничены в использовании данных в настройках системы, или что часы вашего устройства установлены правильно, или это может произойти на модифицированных прошивках.</string> <string name="settings_troubleshoot_test_fcm_failed_too_many_registration">[%1$s] Эта ошибка вне контроля Riot, и, по словам Google, эта ошибка означает, что на устройстве слишком много приложений, зарегистрированных в FCM. Ошибка возникает только в тех случаях, когда существует огромное количество приложений, поэтому она не должна влиять на обычного пользователя.</string> - <string name="sign_out_bottom_sheet_warning_no_backup">Ваши зашифрованные сообщения будут потеряны если выйдете сейчас</string> + <string name="sign_out_bottom_sheet_warning_no_backup">Ваши зашифрованные сообщения будут потеряны, если выйдете сейчас</string> <string name="sign_out_bottom_sheet_warning_backing_up">Выполняется резервное копирование ключа. Если выйти сейчас, Вы потеряете доступ к Вашим зашифрованным сообщениям.</string> <string name="sign_out_bottom_sheet_dont_want_secure_messages">Мне не нужны мои зашифрованные сообщения</string> <string name="sign_out_bottom_sheet_backing_up_keys">Выполняется резервное копирование ключей…</string> @@ -1550,7 +1550,7 @@ <string name="invalid_or_expired_credentials">Вы вышли из системы из-за недействительных или истекших учетных данных.</string> <string name="edit">Редактировать</string> - <string name="reply">Ответ</string> + <string name="reply">Ответить</string> <string name="global_retry">Повторить</string> <string name="room_list_empty">Присоединитесь к комнате, чтобы начать использовать приложение.</string> @@ -1566,19 +1566,19 @@ <string name="room_list_rooms_empty_title">Комнаты</string> <string name="room_list_rooms_empty_body">Ваши комнаты будут отображаться здесь</string> - <string name="title_activity_emoji_reaction_picker">Реакция</string> - <string name="reactions_agree">Соглашаться</string> + <string name="title_activity_emoji_reaction_picker">Отсебятины</string> + <string name="reactions_agree">Принять</string> <string name="reactions_like">Нравиться</string> - <string name="message_add_reaction">Добавить действие</string> - <string name="message_view_reaction">Посмотреть реакции</string> + <string name="message_add_reaction">Добавить отсебятину</string> + <string name="message_view_reaction">Просмотреть отсебятины</string> <string name="settings_integration_manager">Менеджер интеграции</string> <string name="integration_manager_not_configured">Менеджер интеграции не настроен.</string> - <string name="reactions">Реакции</string> + <string name="reactions">Отсебятины</string> - <string name="event_redacted_by_user_reason">Удаленное пользователем событие</string> + <string name="event_redacted_by_user_reason">Событие удалено пользователем</string> <string name="event_redacted_by_admin_reason">Мероприятие, модерируемое администратором помещения</string> - <string name="last_edited_info_message">Последний раз редактировался %1$s на %2$s</string> + <string name="last_edited_info_message">Последний раз редактировалось %1$s на %2$s</string> <string name="malformed_message">Некорректное событие, не может быть отображено</string> @@ -1591,7 +1591,7 @@ <string name="room_preview_no_preview">Эту комнату нельзя предварительно просмотреть</string> <string name="fab_menu_create_room">Комнаты</string> - <string name="fab_menu_create_chat">Прямые сообщения</string> + <string name="fab_menu_create_chat">Личная переписка</string> <string name="create_room_title">Новая комната</string> <string name="create_room_action_create">СОЗДАТЬ</string> @@ -1656,7 +1656,7 @@ <string name="room_preview_world_readable_room_not_supported_yet">Предварительный просмотр открытой комнаты в RiotX пока не поддерживается</string> - <string name="bottom_action_people_x">Прямые сообщения</string> + <string name="bottom_action_people_x">Личная переписка</string> <string name="send_file_step_idle">Ждите…</string> <string name="send_file_step_encrypting_thumbnail">Шифрование миниатюры…</string> @@ -1839,7 +1839,7 @@ <string name="message_ignore_user">Заблокировать пользователя</string> <string name="room_list_quick_actions_notifications_all">Все сообщения</string> - <string name="room_list_quick_actions_notifications_mentions">Только упоминания</string> + <string name="room_list_quick_actions_notifications_mentions">Только при упоминаниях</string> <string name="room_list_quick_actions_settings">Настройки</string> <string name="room_list_quick_actions_leave">Покинуть комнату</string> <string name="room_join_rules_public">%1$s сделал комнату доступной для всех, у кого есть ссылка.</string> @@ -1847,4 +1847,178 @@ <string name="labs_allow_extended_logging_summary">Подробные логи помогут разработчикам, предоставив больше информации, когда вы отправляете RageShake. Даже когда они разрешены, приложение не логирует ваши сообщения и другие приватные данные.</string> + <string name="a11y_create_menu_close">Закройте меню создания комнаты…</string> + <string name="a11y_jump_to_bottom">Вниз</string> + + <string name="attachment_type_contact">Контакт</string> + <string name="attachment_type_sticker">Стикер</string> + <string name="report_content_custom_hint">Причина отчёта о контенте</string> + <string name="report_content_custom_submit">ОТЧЁТ</string> + <string name="block_user">Заблокировать пользователя</string> + + <string name="room_list_quick_actions_notifications_all_noisy">Все сообщения (громко)</string> + <string name="room_list_quick_actions_notifications_mute">Без звука</string> + <string name="command_description_spoiler">Отправить данное сообщение под спойлером</string> + <string name="spoiler">Спойлер</string> + <string name="reaction_search_type_hint">Введите ключевые слова, чтобы найти отсебятину.</string> + + <string name="help_long_click_on_room_for_more_options">Длительный щелчок по комнате открывает опции</string> + + + <string name="timeline_unread_messages">Непрочитанные сообщения</string> + + <string name="login_splash_title">Развяжите своё общение</string> + <string name="login_splash_text1">Общайтесь с людьми напрямую или в группах</string> + <string name="login_splash_submit">Начать</string> + + <string name="login_server_title">Выберите сервер</string> + <string name="login_server_text">Как и электронная почта, учетные записи имеют один дом, хотя вы можете общаться с кем угодно</string> + <string name="login_server_modular_text">Премиум-хостинг для организаций</string> + <string name="login_server_modular_learn_more">Узнать больше</string> + <string name="login_server_other_title">Другое</string> + <string name="login_server_other_text">Пользовательские и расширенные настройки</string> + + <string name="login_continue">Продолжить</string> + <string name="login_connect_to">Подключиться к %1$s</string> + <string name="login_connect_to_modular">Подключиться к Modular</string> + <string name="login_connect_to_a_custom_server">Подключиться к пользовательскому серверу</string> + <string name="login_signup">Зарегистрироваться</string> + <string name="login_signin">Войти в систему</string> + <string name="login_signin_sso">Продолжить с SSO</string> + + <string name="login_server_url_form_modular_hint">Модульный адрес</string> + <string name="login_server_url_form_other_hint">Адрес</string> + <string name="login_server_url_form_modular_text">Премиум-хостинг для организаций</string> + <string name="login_server_url_form_modular_notice">Введите адрес Modular Riot или сервера, который вы хотите использовать.</string> + <string name="login_sso_error_message">Произошла ошибка при загрузке страницы: %1$s (%2$d)</string> + <string name="login_mode_not_supported">Приложение не может войти на этот сервер, так как он поддерживает следующие типы входа: %1$s. +\nВы хотите войти с помощью веб-клиента\?</string> + <string name="login_registration_disabled">Извините, этот сервер не принимает новые учётные записи.</string> + <string name="login_registration_not_supported">Приложение не может создать учётную запись на сём сервере. +\n +\nЖелаете зарегистрироваться через веб-клиент\?</string> + + <string name="login_login_with_email_error">Сей адрес электронной почты не связан ни с одной учетной записью.</string> + + <string name="login_reset_password_on">Сбросить пароль на %1$s</string> + <string name="login_reset_password_submit">Далее</string> + <string name="login_reset_password_email_hint">Email</string> + <string name="login_reset_password_password_hint">Новый пароль</string> + + <string name="login_reset_password_warning_title">Внимание!</string> + <string name="login_reset_password_warning_content">Смена пароля приведёт к сбросу всех сквозных ключей шифрования во всех ваших сеансах, что сделает зашифрованную историю разговоров нечитаемой. Настройте резервное копирование ключей или экспортируйте ключи от комнаты из другого сеанса, прежде чем сбрасывать пароль.</string> + <string name="login_reset_password_warning_submit">Продолжить</string> + + <string name="login_reset_password_error_not_found">Данный email не связан ни с одним аккаунтом</string> + + <string name="login_reset_password_mail_confirmation_title">Проверьте свою почту</string> + <string name="login_reset_password_mail_confirmation_notice">Письмо с подтверждением было отправлено на %1$s.</string> + <string name="login_reset_password_mail_confirmation_notice_2">Нажмите на ссылку, чтобы подтвердить свой новый пароль. Как только вы перейдете по ссылке, которую он содержит, нажмите ниже.</string> + <string name="login_reset_password_success_title">Успешно!</string> + <string name="login_reset_password_success_notice">Ваш пароль был сброшен.</string> + <string name="login_reset_password_success_notice_2">Вы вышли из всех сеансов и больше не будете получать push-уведомления. Чтобы снова иметь возможность получать уведомления, необходимо повторно войти в систему.</string> + <string name="login_reset_password_success_submit">Назад, чтобы войти в систему</string> + + <string name="login_reset_password_cancel_confirmation_title">Предупреждение</string> + <string name="login_reset_password_cancel_confirmation_content">Ваш пароль еще не изменен. +\n +\nОстановить процесс смены пароля\?</string> + + <string name="login_set_email_title">Установить адрес электронной почты</string> + <string name="login_set_email_mandatory_hint">Электронная почта</string> + <string name="login_set_email_optional_hint">Электронная почта (по желанию)</string> + <string name="login_set_email_submit">Далее</string> + + <string name="login_set_msisdn_title">Установить номер телефона</string> + <string name="login_set_msisdn_notice">Укажите номер своего телефона, ежели желаете, чтобы люди, которым вы небезразличны, смогли вас найти.</string> + <string name="login_set_msisdn_notice2">Пожалуйста, используйте международный формат.</string> + <string name="login_set_msisdn_mandatory_hint">Номер телефона</string> + <string name="login_set_msisdn_optional_hint">Номер телефона (по желанию)</string> + <string name="login_set_msisdn_submit">Далее</string> + + <string name="login_msisdn_confirm_title">Подтвердить номер телефона</string> + <string name="login_msisdn_confirm_notice">Мы только что отправили код на %1$s. Введите его ниже, чтобы подтвердить, что это вы.</string> + <string name="login_msisdn_confirm_hint">Введите код</string> + <string name="login_msisdn_confirm_send_again">Отправить повторно</string> + <string name="login_msisdn_confirm_submit">Далее</string> + + <string name="login_msisdn_error_not_international">Международные телефонные номера должны начинаться с \'+\'</string> + <string name="login_msisdn_error_other">Номер телефона кажется недействительным. Пожалуйста, проверьте его</string> + + <string name="login_signup_to">Зарегистрироваться в %1$s</string> + <string name="login_signin_username_hint">Имя пользователя или email</string> + <string name="login_signup_username_hint">Имя пользователя</string> + <string name="login_signup_password_hint">Пароль</string> + <string name="login_signup_submit">Далее</string> + <string name="login_signup_error_user_in_use">Это имя пользователя занято</string> + <string name="login_signup_cancel_confirmation_title">Предупреждение</string> + <string name="login_signup_cancel_confirmation_content">Ваш аккаунт еще не создан. +\n +\nОстановить процесс регистрации\?</string> + + <string name="login_a11y_choose_matrix_org">Выбрать matrix.org</string> + <string name="login_a11y_choose_modular">Выбрать modular</string> + <string name="login_a11y_choose_other">Выбрать учреждённый сервер</string> + <string name="login_a11y_captcha_container">Пожалуйста, пройдите проверку на каптчу</string> + <string name="login_terms_title">Принять условия для продолжения</string> + + <string name="login_wait_for_email_title">Пожалуйста, проверьте ваш email</string> + <string name="login_wait_for_email_notice">Мы только что отправили email на %1$s. +\nПожалуйста, нажмите на содержащуюся в нём ссылку, чтобы продолжить создание аккаунта.</string> + <string name="login_error_outdated_homeserver_title">Домашний сервер устарел</string> + <string name="login_error_outdated_homeserver_content">Сей домашний сервер использует слишком старую версию для подключения. Попросите вашего администратора обновить версию на домашнем сервере.</string> + + <plurals name="login_error_limit_exceeded_retry_after"> + <item quantity="one">Слишком много запросов было отправлено. Вы сможете повторить попытку через %1$d секунду…</item> + <item quantity="few">Слишком много запросов было отправлено. Вы сможете повторить попытку через %1$d секунды…</item> + <item quantity="many">Слишком много запросов было отправлено. Вы сможете повторить попытку через %1$d секунд…</item> + </plurals> + + <string name="signed_out_title">Вы вышли из системы</string> + <string name="signed_out_notice">Сие может быть обусловлено различными причинами: +\n +\n- Вы сменили пароль в другом сеансе. +\n +\n- Вы удалили сей сеанс из иного сеанса. +\n +\n- Администратор вашего сервера заблокировал вам доступ из соображений безопасности.</string> + <string name="signed_out_submit">Войти снова</string> + + <string name="soft_logout_title">Вы вышли из системы</string> + <string name="soft_logout_signin_title">Войти</string> + <string name="soft_logout_signin_notice">Администратор вашего домашнего сервера (%1$s) вывел вас из вашего аккаунта %2$s (%3$s).</string> + <string name="soft_logout_signin_e2e_warning_notice">Войдите, чтобы восстановить ключи шифрования, хранящиеся исключительно на сём устройстве. Они нужны вам для чтения всех ваших защищённых сообщений на любом устройстве.</string> + <string name="soft_logout_signin_submit">Войти</string> + <string name="soft_logout_signin_password_hint">Пароль</string> + <string name="soft_logout_clear_data_title">Очистить личные данные</string> + <string name="soft_logout_clear_data_notice">Внимание: Ваши личные данные (включая ключи шифрования) всё ещё хранятся на сём устройстве. +\n +\nОчистите его, если вы закончили использовать сие устройство или хотите войти в другую учётную запись.</string> + <string name="soft_logout_clear_data_submit">Очистить все данные</string> + + <string name="soft_logout_clear_data_dialog_title">Очистить данные</string> + <string name="soft_logout_clear_data_dialog_content">Очистить все данные, хранящиеся в данный момент на сём устройстве\? +\nВойдите ещё раз, чтобы получить доступ к данным своего аккаунта и сообщениям.</string> + <string name="soft_logout_clear_data_dialog_e2e_warning_content">Вы потеряете доступ к защищённым сообщениям, если не войдёте в систему для восстановления ключей шифрования.</string> + <string name="soft_logout_clear_data_dialog_submit">Очистить данные</string> + <string name="soft_logout_sso_not_same_user_error">Текущая сессия предназначена для пользователя %1$s, а вы предоставляете учётные данные для пользователя %2$s. Такая безалаберность не поддерживается в RiotX. +\nПожалуйста, сначала очистите данные, а затем снова войдите под другим аккаунтом.</string> + + <string name="permalink_malformed">Ваша ссылка на matrix.to неверна</string> + <string name="bug_report_error_too_short">Описание слишком короткое</string> + + <string name="settings_show_devices_list">Посмотреть все мои сеансы</string> + <string name="settings_advanced_settings">Дополнительные настройки</string> + <string name="settings_developer_mode">Режим разработчика</string> + <string name="settings_developer_mode_summary">Режим разработчика активирует скрытые функции, а также может сделать приложение менее стабильным. Только для разработчиков!</string> + <string name="settings">Настройки</string> + <string name="devices_current_device">Текущий сеанс</string> + <string name="devices_other_devices">Другие сеансы</string> + + <string name="create_room_encryption_title">Включено шифрование</string> + <string name="verification_conclusion_warning">Недоверительный вход</string> + <string name="room_profile_section_more_uploads">Отправленные файлы</string> + <string name="cross_signing_verify_by_emoji">Интерактивная проверка по отсебятинам</string> + <string name="error_empty_field_choose_user_name">Пожалуйста, выберите имя пользователя.</string> + <string name="error_empty_field_choose_password">Пожалуйста, выберите пароль.</string> </resources> diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 970587e702..1696a02aef 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -592,7 +592,7 @@ <string name="encryption_import_import">Importo</string> <string name="passphrase_enter_passphrase">Jepni frazëkalimin</string> <string name="encryption_never_send_to_unverified_devices_title">Fshehtëzoje vetëm për sesione të verifikuar</string> - <string name="encryption_information_not_verified">JO i verifikuar</string> + <string name="encryption_information_not_verified">Jo i Verifikuar</string> <string name="encryption_information_verified">I verifikuar</string> <string name="encryption_information_blocked">Në Listë të Zezë</string> @@ -916,8 +916,8 @@ <string name="encryption_information_unverify">Hiqi verifikimin</string> <string name="encryption_information_unblock">Hiqe nga listë e zezë</string> - <string name="encryption_information_verify_device_warning">Që të verifikoni se këtij sesioni mund t’i zihet besë, ju lutemi, lidhuni me të zotët e saj përmes ndonjë rruge tjetër (p.sh., personalisht, ose përmes një thirrjeje telefonike) dhe pyetini nëse përputhet apo jo kyçi që shohin te Rregullime të tyret të Përdoruesit për këtë sesion me kyçin më poshtë:</string> - <string name="encryption_information_verify_device_warning2">Nëse përputhet, shtypni butonin e verifikimit më poshtë. Nëse jo, atëherë dikush tjetër po e përgjon këtë sesion dhe duhet ta kaloni në listë të zezë. Në të ardhmen, ky proces verifikimi do të jetë më i sofistikuar.</string> + <string name="encryption_information_verify_device_warning">Ripohojeni duke krahasuar sa vijon me Rregullimet e Përdoruesit te sesioni juaj tjetër:</string> + <string name="encryption_information_verify_device_warning2">Nëse s’përputhen, siguria e komunikimeve tuaja mund të jetë komprometuar.</string> <string name="directory_server_placeholder">URL Shërbyesi Home</string> <plurals name="notification_unread_notified_messages_in_room_rooms"> <item quantity="one">1 dhomë</item> @@ -2147,7 +2147,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani </plurals> <string name="poll_item_selected_aria">Mundësi e Përzgjedhur</string> <string name="command_description_poll">Krijoni një pyetësor të thjeshtë</string> - <string name="verification_cannot_access_other_session">Përdorni metodë rimarrjesh</string> + <string name="verification_cannot_access_other_session">Përdorni Frazëkalim Rikthimesh ose Kyç</string> <string name="verification_use_passphrase">Nëse s’hyni dot në një sesion ekzistues</string> <string name="new_signin">Hyrje e Re</string> @@ -2182,7 +2182,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani <string name="refresh">Rifreskoje</string> - <string name="new_session">Sesion i Ri</string> + <string name="new_session">Hyrje e re. Ju qetë\?</string> <string name="new_session_review">Prekeni, që ta shqyrtoni & verifikoni</string> <string name="verify_new_session_notice">Përdoreni këtë sesion që të verifikoni atë të riun tuaj, duke i akorduar hyrje te mesazhe të fshehtëzuar.</string> <string name="verify_new_session_was_not_me">Ky s’qeshë unë</string> @@ -2204,7 +2204,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani <string name="verify_cancelled_notice">Verifikoni pajisjet tuaja që prej Rregullimeve.</string> <string name="verification_cancelled">Verifikimi u Anulua</string> - <string name="recovery_passphrase">Fjalëkalim Mesazhesh</string> + <string name="recovery_passphrase">Frazëkalim Rikthimesh</string> <string name="message_key">Kyç Mesazhesh</string> <string name="account_password">Fjalëkalim Llogarie</string> @@ -2278,4 +2278,73 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani <string name="auth_invalid_login_param_space_in_password">Emër përdoruesi dhe/ose fjalëkalim i pasaktë. Fjalëkalimi i dhënë fillon ose mbaron me hapësirë, ju lutemi, kontrollojeni.</string> + <string name="settings_rageshake" /> + <string name="bootstrap_crosssigning_progress_initializing">Po publikohen kyçe të krijuar identiteti</string> + <string name="room_message_placeholder">Mesazh…</string> + + <string name="upgrade_security">Ka të gatshëm përmirësim fshehtëzimi</string> + <string name="security_prompt_text">Verifikoni veten & të tjerët, që t’i mbani bisedat tuaja të sigurta</string> + + <string name="bootstrap_enter_recovery">Që të vazhdohet, jepni %s tuaj</string> + <string name="use_file">Përdor Kartelë</string> + + <string name="enter_backup_passphrase">Jepni %s</string> + <string name="backup_recovery_passphrase">Frazëkalim Rikthimesh</string> + <string name="bootstrap_invalid_recovery_key">S’është kyç rimarrjesh i vlefshëm</string> + <string name="recovery_key_empty_error_message">Ju lutemi, jepni një kyç rimarrjesh</string> + + <string name="bootstrap_progress_checking_backup">Po kontrollohet Kyç kopjeruajtjeje</string> + <string name="bootstrap_progress_checking_backup_with_info">Po kontrollohet Kyç kopjeruajtjeje (%s)</string> + <string name="bootstrap_progress_generating_ssss">Po prodhohet kyç SSSS nga frazëkalim</string> + <string name="bootstrap_progress_generating_ssss_with_info">Po prodhohet kyç SSSS prej frazëkalimi (%s)</string> + <string name="bootstrap_progress_generating_ssss_recovery">Po prodhohet kyç SSSS nga kyç rimarrjesh</string> + <string name="bootstrap_progress_storing_in_sss">Po depozitohet e fshehtë kopjeruajtjeje kyçesh në SSSS</string> + <string name="new_session_review_with_info">%1$s (%2$s)</string> + + <string name="bootstrap_migration_enter_backup_password">Që të vazhdohet, jepni Frazëkalim Kopjeruajtje Kyçesh.</string> + <string name="bootstrap_migration_use_recovery_key">përdorni kyçin tuaj të rimarrjeve të Kopjeruajtjes së Kyçeve</string> + <string name="bootstrap_migration_with_passphrase_helper_with_link">S’dihet Frazëkalimi juaj i Kopjeruajtjes së Kyçeve, mundeni të %s.</string> + <string name="bootstrap_migration_backup_recovery_key">Kyç rimarrjesh Kopjeruajtjesh Kyçesh</string> + + <string name="settings_security_prevent_screenshots_title">Pengo foto ekrani të aplikacionit</string> + <string name="settings_security_prevent_screenshots_summary">Aktivizimi i këtij rregullimi shton FLAG_SECURE te krejt Veprimtaritë. Që ndryshimi të hyjë në fuqi, rinisni aplikacionin.</string> + + <string name="media_file_added_to_gallery">Te Galeria u shtua kartelë media</string> + <string name="error_adding_media_file_to_gallery">S’u shtua dot kartelë media te Galeria</string> + <string name="change_password_summary">Caktoni një fjalëkalim të ri llogarie…</string> + + <string name="use_other_session_content_description">Përdorni Riot-in më të ri në pajisjet tuaja të tjera, Riot Web, Riot Desktop, Riot iOS, RiotX për Android, ose ose një tjetër klient Matrix i aftë për <em>cross-signing</em</string> + <string name="riot_desktop_web">Riot Web +\nRiot Desktop</string> + <string name="riot_ios_android">Riot iOS +\nRiot X për Android</string> + <string name="or_other_mx_capabale_client">ose një tjetër klient Matrix i aftë për <em>cross-signing</em</string> + <string name="use_latest_riot">Përdorni Riot-in më të ri në pajisjet tuaja të tjera:</string> + <string name="command_description_discard_session_not_handled">Mbulohet vetëm për dhoma të fshehtëzuara</string> + <string name="enter_secret_storage_passphrase_or_key">Përdorni %1$s tuaj ose përdorni %2$s tuaj që të vazhdohet.</string> + <string name="use_recovery_key">Përdorni Kyçin Rimarrjesh</string> + <string name="enter_secret_storage_input_key">Përzgjidhni Kyçin tuaj të Rimarrjeve, ose jepeni dorazi duke e shtypur ose duke e ngjitur prej të papastrës tuaj</string> + <string name="keys_backup_recovery_key_error_decrypt">Kopjeruajtja s’u shfshehtëzua dot me këtë Kyç Rimarrjesh: ju lutemi, verifikoni se keni dhënë Kyçin e saktë të Rimarrjeve.</string> + <string name="failed_to_access_secure_storage">S’u arrit të hyhet në depozitë të sigurt</string> + + <string name="unencrypted">Të pafshehtëzuara</string> + <string name="encrypted_unverified">Fshehtëzuar nga një pajisje e paverifikuar</string> + <string name="review_logins">Shqyrtojini kur të jeni i futur</string> + <string name="verify_other_sessions">Verifikoni krejt sesionet tuaj që të siguroheni se llogaria & mesazhet tuaja janë të sigurt</string> + <string name="verify_this_session">Verifikoni kredencialet e reja për hyrje te llogaria juaj: %1$s</string> + + <string name="cross_signing_verify_by_text">Verifikojeni Dorazi përmes Teksti</string> + <string name="crosssigning_verify_session">Verifikoni kredenciale hyrjeje</string> + <string name="cross_signing_verify_by_emoji">Verifikojeni Në Mënyrë Interaktive përmes Emoji-sh</string> + <string name="confirm_your_identity">Ripohoni identitetin tuaj duke verifikuar këto kredenciale hyrjesh prej një nga sesionet tuaj të tjerë, duke i akorduar hyrje te mesazhet e fshehtëzuar.</string> + <string name="mark_as_verified">Vërini shenjë si i Besuar</string> + + <string name="error_empty_field_choose_user_name">Ju lutemi, zgjidhni një emër përdoruesi.</string> + <string name="error_empty_field_choose_password">Ju lutemi, zgjidhni një fjalëkalim.</string> + <string name="external_link_confirmation_title">Kontrollojeni edhe një herë këtë lidhje</string> + <string name="external_link_confirmation_message">Lidhja %1$s po ju shpie te një tjetër sajt: %2$s. +\n +\nJeni i sigurt se doni të vazhdohet\?</string> + + <string name="create_room_dm_failure">S’e krijuam dot DM-në tuaj. Ju lutemi, kontrolloni përdoruesit që doni të ftoni dhe riprovoni.</string> </resources> diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml new file mode 100644 index 0000000000..40e2df9870 --- /dev/null +++ b/vector/src/main/res/values-sv/strings.xml @@ -0,0 +1,282 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources> + <string name="resources_language">sv</string> + <string name="resources_country_code">SE</string> + <string name="resources_script">Latn</string> + + <string name="light_theme">Ljust tema</string> + <string name="dark_theme">Mörkt tema</string> + <string name="black_them">Svart tema</string> + <string name="status_theme">Status.im-tema</string> + + <string name="notification_sync_init">Initialiserar tjänst</string> + <string name="notification_sync_in_progress">Synkroniserar…</string> + <string name="notification_listening_for_events">Lyssnar efter händelser</string> + <string name="notification_noisy_notifications">Högljudda notifikationer</string> + <string name="notification_silent_notifications">Tysta notifikationer</string> + + <string name="title_activity_home">Meddelanden</string> + <string name="title_activity_room">Rum</string> + <string name="title_activity_settings">Inställningar</string> + <string name="title_activity_member_details">Medlemsdetaljer</string> + <string name="title_activity_historical">Historisk</string> + <string name="title_activity_bug_report">Buggrapport</string> + <string name="title_activity_group_details">Gemenskapsdetaljer</string> + <string name="title_activity_choose_sticker">Skicka ett klistermärke</string> + <string name="title_activity_keys_backup_setup">Säkerhetskopiering av nycklar</string> + <string name="title_activity_keys_backup_restore">Använd säkerhetskopiering av nycklar</string> + <string name="title_activity_verify_device">Verifiera session</string> + + <string name="keys_backup_is_not_finished_please_wait">Säkerhetskopiering av nycklar är inte färdig, vänligen vänta…</string> + <string name="sign_out_bottom_sheet_warning_no_backup">Du kommer att förlora dina krypterade meddelanden om du loggar ut nu</string> + <string name="sign_out_bottom_sheet_warning_backing_up">Säkerhetskopiering av nycklar pågår. Om du loggar ut nu så kommer du att förlora åtkomst till dina krypterade meddelanden.</string> + <string name="sign_out_bottom_sheet_warning_backup_not_active">Säker säkerhetskopiering av nycklar bör vara aktivt i alla dina sessioner för att förhindra att du förlorar åtkomst till dina krypterade meddelanden.</string> + <string name="sign_out_bottom_sheet_dont_want_secure_messages">Jag vill inte ha mina krypterade meddelanden</string> + <string name="sign_out_bottom_sheet_backing_up_keys">Säkerhetskopierar nycklar…</string> + <string name="keys_backup_activate">Använd säkerhetskopiering av nycklar</string> + <string name="are_you_sure">Är du säker\?</string> + <string name="backup">Säkerhetskopiera</string> + <string name="sign_out_bottom_sheet_will_lose_secure_messages">Du kommer att förlora åtkomst till dina krypterade meddelanden om du inte säkerhetskopierar dina nycklar innan du loggar ut.</string> + + <string name="dialog_title_third_party_licences">Tredjepartslicenser</string> + + <string name="loading">Laddar…</string> + + <string name="ok">OK</string> + <string name="cancel">Avbryt</string> + <string name="save">Spara</string> + <string name="leave">Lämna</string> + <string name="stay">Stanna</string> + <string name="send">Skicka</string> + <string name="copy">Kopiera</string> + <string name="resend">Skicka igen</string> + <string name="redact">Ta bort</string> + <string name="quote">Citat</string> + <string name="download">Ladda ner</string> + <string name="share">Dela</string> + <string name="speak">Prata</string> + <string name="clear">Rensa</string> + <string name="later">Senare</string> + <string name="forward">Vidarebefordra</string> + <string name="permalink">Permanent länk</string> + <string name="view_source">Visa källa</string> + <string name="view_decrypted_source">Visa avkrypterad källa</string> + <string name="delete">Radera</string> + <string name="rename">Döp om</string> + <string name="none">Ingen</string> + <string name="revoke">Återkalla</string> + <string name="disconnect">Koppla ifrån</string> + <string name="report_content">Rapportera innehåll</string> + <string name="active_call">Aktivt samtal</string> + <string name="ongoing_conference_call">Pågående gruppsamtal. +\nAnslut som %1$s eller %2$s</string> + <string name="ongoing_conference_call_voice">Röst</string> + <string name="ongoing_conference_call_video">Video</string> + <string name="cannot_start_call">Kan inte starta samtalet, vänligen försök senare</string> + <string name="missing_permissions_warning">På grund av saknade rättigheter, så kan vissa funktioner saknas…</string> + <string name="missing_permissions_error">På grund av saknade rättigheter så kan denna handling inte utföras.</string> + <string name="missing_permissions_to_start_conf_call">Du behöver ha rätt att bjuda in för att starta ett gruppsamtal i detta rum</string> + <string name="missing_permissions_title_to_start_conf_call">Kan inte starta samtal</string> + <string name="device_information">Sessionsinformation</string> + <string name="room_no_conference_call_in_encrypted_rooms">Gruppsamtal stöds inte i krypterade rum</string> + <string name="call_anyway">Ring ändå</string> + <string name="send_anyway">Skicka ändå</string> + <string name="or">eller</string> + <string name="invite">Bjud in</string> + <string name="offline">Bortkopplad</string> + <string name="accept">Godkänn</string> + <string name="skip">Hoppa över</string> + <string name="done">Färdig</string> + <string name="abort">Avbryt</string> + <string name="ignore">Ignorera</string> + <string name="review">Granska</string> + <string name="decline">Avslå</string> + + <string name="action_exit">Avsluta</string> + <string name="actions">Handlingar</string> + <string name="action_sign_out">Logga ut</string> + <string name="action_sign_out_confirmation_simple">Är du säker på att du vill logga ut\?</string> + <string name="action_voice_call">Röstsamtal</string> + <string name="action_video_call">Videosamtal</string> + <string name="action_global_search">Global sökning</string> + <string name="action_mark_all_as_read">Markera alla som skickade</string> + <string name="action_historical">Historisk</string> + <string name="action_quick_reply">Snabbsvar</string> + <string name="action_mark_room_read">Markera som läst</string> + <string name="action_open">Öppna</string> + <string name="action_close">Stäng</string> + <string name="copied_to_clipboard">Kopierat till klippbordet</string> + <string name="disable">Stäng av</string> + + <string name="dialog_title_confirmation">Bekräftelse</string> + <string name="dialog_title_warning">Varning</string> + <string name="dialog_title_error">Fel</string> + + <string name="bottom_action_home">Hem</string> + <string name="bottom_action_favourites">Favoriter</string> + <string name="bottom_action_people">Personer</string> + <string name="bottom_action_rooms">Rum</string> + <string name="bottom_action_groups">Gemenskaper</string> + + <string name="home_filter_placeholder_home">Filtrera rumsnamn</string> + <string name="home_filter_placeholder_favorites">Filtrera favoriter</string> + <string name="home_filter_placeholder_people">Filtrera personer</string> + <string name="home_filter_placeholder_rooms">Filtrera rumsnamn</string> + <string name="home_filter_placeholder_groups">Filtrera gemenskapsnamn</string> + + <string name="invitations_header">Inbjudningar</string> + <string name="low_priority_header">Låg prioritet</string> + <string name="system_alerts_header">Systemvarningar</string> + + <string name="direct_chats_header">Konversationer</string> + <string name="local_address_book_header">Lokal adressbok</string> + <string name="user_directory_header">Användarkatalog</string> + <string name="matrix_only_filter">Bara Matrix-kontakter</string> + <string name="no_conversation_placeholder">Inga konversationer</string> + <string name="no_contact_access_placeholder">Du gav inte Riot tillgång till dina lokala kontakter</string> + <string name="no_result_placeholder">Inga resultat</string> + <string name="people_no_identity_server">Ingen identitetsserver konfigurerad.</string> + + <string name="rooms_header">Rum</string> + <string name="rooms_directory_header">Rumskatalog</string> + <string name="no_room_placeholder">Inga rum</string> + <string name="no_public_room_placeholder">Inga publika rum tillgängliga</string> + <plurals name="public_room_nb_users"> + <item quantity="one">1 användare</item> + <item quantity="other">%d användare</item> + </plurals> + + <string name="groups_invite_header">Bjud in</string> + <string name="groups_header">Gemenskaper</string> + <string name="no_group_placeholder">Inga grupper</string> + + <string name="send_bug_report_include_logs">Skicka loggar</string> + <string name="send_bug_report_include_crash_logs">Skicka kraschloggar</string> + <string name="send_bug_report_include_screenshot">Skicka skärmdump</string> + <string name="send_bug_report">Rapportera bugg</string> + <string name="send_bug_report_description">Vänligen beskriv buggen. Vad gjorde du\? Vad förväntade du dig skulle hända\? Vad hände istället\?</string> + <string name="send_bug_report_description_in_english">Om möjligt, skriv vänligen beskrivningen på engelska.</string> + <string name="send_bug_report_placeholder">Beskriv ditt problem här</string> + <string name="send_bug_report_logs_description">För att diagnostisera problemet kommer loggar från den här klienten att skickas med den här buggrapporten. Denna buggrapport, inklusive loggarna och skärmdumpen, kommer att vara publikt synlig. Om du skulle föredra att endast skicka texten ovan, vänligen avmarkera:</string> + <string name="send_bug_report_alert_message">Du verkar skaka din telefon i frustration. Vill du skicka en buggrapport\?</string> + <string name="send_bug_report_app_crashed">Appen kraschade senaste gången. Vill du skicka en buggrapport\?</string> + <string name="send_bug_report_rage_shake">Raseriskaka för att rapportera bugg</string> + + <string name="send_bug_report_sent">Buggrapporten har skickats framgångsrikt</string> + <string name="send_bug_report_failed">Sändning av buggrapporten misslyckades (%s)</string> + <string name="send_bug_report_progress">Framsteg (%s%%)</string> + + <string name="send_files_in">Skicka in i</string> + <string name="read_receipt">Läs</string> + + <string name="join_room">Gå med i rummet</string> + <string name="username">Användarnamn</string> + <string name="create_account">Skapa konto</string> + <string name="login">Logga in</string> + <string name="logout">Logga ut</string> + <string name="hs_url">URL för hemserver</string> + <string name="identity_url">URL för identitetsserver</string> + <string name="search">Sök</string> + + <string name="start_new_chat">Starta ny chatt</string> + <string name="start_voice_call">Starta röstsamtal</string> + <string name="start_video_call">Starta videosamtal</string> + + <string name="option_send_voice">Skicka röst</string> + + <string name="start_new_chat_prompt_msg">Är du säker på att du vill skapa en ny chatt med %s\?</string> + <string name="start_voice_call_prompt_msg">Är du säker på att du vill starta ett röstsamtal\?</string> + <string name="start_video_call_prompt_msg">Är du säker på att du vill skapa ett videosamtal\?</string> + <string name="call_failed_no_ice_title">Samtal misslyckades p.g.a. felkonfigurerad server</string> + <string name="call_failed_no_ice_description">Vänligen be administratören för din hemserver (%1$s) att konfigurera en TURN-server för att samtal ska funka pålitligt. +\n +\nAlternativt så kan du försöka använda den publika servern på %2$s, men detta kommer inte att vara lika pålitligt, och kommer att dela din IP-adress med den servern. Du kan också hantera detta i inställningarna.</string> + <string name="call_failed_no_ice_use_alt">Försök med %s</string> + <string name="call_failed_dont_ask_again">Fråga mig inte igen</string> + + <string name="option_send_files">Skicka filer</string> + <string name="option_send_sticker">Skicka klistermärke</string> + <string name="option_take_photo_video">Ta foto eller video</string> + <string name="option_take_photo">Ta ett foto</string> + <string name="option_take_video">Ta en video</string> + + <string name="no_sticker_application_dialog_content">Du har för närvarande inga klistermärkespaket aktiva. +\n +\nLägg till några nu\?</string> + + <string name="go_on_with">fortsätt med…</string> + <string name="error_no_external_application_found">Tyvärr har ingen extern applikation hittats som kan fullfölja denna handling.</string> + + <string name="auth_login">Logga in</string> + <string name="auth_login_sso">Logga in med externt konto</string> + <string name="auth_register">Skapa konto</string> + <string name="auth_submit">Skicka in</string> + <string name="auth_skip">Hoppa över</string> + <string name="auth_send_reset_email">Skicka e-brev för återställning</string> + <string name="auth_return_to_login">Gå tillbaka till inloggningsskärmen</string> + <string name="auth_user_id_placeholder">E-postadress eller användarnamn</string> + <string name="auth_password_placeholder">Lösenord</string> + <string name="auth_new_password_placeholder">Nytt lösenord</string> + <string name="auth_user_name_placeholder">Användarnamn</string> + <string name="auth_add_email_message_2">Sätt en e-postadress för kontoåterförvärv, och senare för att valfritt vara upptäckbar av folk som som känner dig.</string> + <string name="auth_add_phone_message_2">Sätt ett telefonnummer som folk som känner dig kan använda för att hitta dig.</string> + <string name="auth_add_email_phone_message_2">Sätt en e-postadress för kontoåterförvärv. Senare kan e-postadresser och telefonnummer valfritt användas för att vara upptäckbar av folk som känner dig.</string> + <string name="auth_add_email_and_phone_message_2">Sätt en e-postadress för kontoåterförvärv. Senare kan e-postadresser och telefonnummer valfritt användas för att vara upptäckbar av folk som känner dig.</string> + <string name="auth_email_placeholder">E-postadress</string> + <string name="auth_opt_email_placeholder">E-postadress (valfritt)</string> + <string name="auth_phone_number_placeholder">Telefonnummer</string> + <string name="auth_opt_phone_number_placeholder">Telefonnummer (valfritt)</string> + <string name="auth_repeat_password_placeholder">Repetera lösenordet</string> + <string name="auth_repeat_new_password_placeholder">Bekräfta ditt nya lösenord</string> + <string name="auth_invalid_login_param">Fel användarnamn och/eller lösenord</string> + <string name="auth_invalid_user_name">Användarnamn får endast innehålla bokstäver, siffror, punkter, bindestreck och understreck</string> + <string name="auth_invalid_password">Lösenordet är för kort (minst 6 tecken)</string> + <string name="auth_missing_password">Lösenord saknas</string> + <string name="auth_invalid_email">Det här ser inte ut som en giltig e-postadress</string> + <string name="auth_invalid_phone">Det här ser inte ut som ett giltigt telefonnummer</string> + <string name="auth_email_already_defined">Den här e-postadressen är redan definierad.</string> + <string name="auth_missing_email">E-postadress saknas</string> + <string name="auth_missing_phone">Telefonnummer saknas</string> + <string name="auth_missing_email_or_phone">E-postadress eller telefonnummer saknas</string> + <string name="auth_invalid_token">Felaktig token</string> + <string name="auth_password_dont_match">Lösenorden matchar inte</string> + <string name="auth_forgot_password">Glömt lösenordet\?</string> + <string name="auth_use_server_options">Använd anpassade server-inställningar (avancerat)</string> + <string name="auth_email_validation_message">Vänligen kolla din e-post för att fortsätta med registreringen</string> + <string name="auth_threepid_warning_message">Registrering med e-postadress och telefonnummer samtidigt så stöds inte än, fram tills API:t finns. Endast telefonnumret kommer användas. +\n +\nDu kan lägga till ett telefonnummer till din profil i inställningarna.</string> + <string name="auth_recaptcha_message">Denna hemserver skulle vilja verifiera att du inte är en robot</string> + <string name="auth_username_in_use">Användarnamnet är upptagen</string> + <string name="auth_home_server">Hemserver:</string> + <string name="auth_identity_server">Identitetsserver:</string> + <string name="auth_reset_password_next_step_button">Jag har verifierat min e-postadress</string> + <string name="auth_reset_password_message">För att återställa ditt lösenord, skriv in e-postadressen länkad till ditt konto:</string> + <string name="auth_reset_password_missing_email">Du måste skriva in e-postadressen länkad till ditt konto.</string> + <string name="auth_reset_password_missing_password">Du måste skriva in ett nytt lösenord.</string> + <string name="auth_reset_password_email_validation_message">Ett e-brev har skickats till %s. När du har följt länken i det, klicka nedan.</string> + <string name="auth_reset_password_error_unauthorized">Misslyckades att verifiera e-postadressen: se till att du klickade på länken i e-brevet</string> + <string name="auth_reset_password_success_message">Ditt lösenord har blivit återställt. +\n +\nDu har loggats ut ur alla sessioner och kommer inte längre motta pushnotifikationer. För att återaktivera notifikationer, logga in igen på varje enhet.</string> + <string name="auth_accept_policies">Vänligen granska och acceptera villkoren för denna hemserver:</string> + + <string name="login_error_must_start_http">URLen måste börja med http[s]://</string> + <string name="login_error_network_error">Kan inte logga in: Nätverksfel</string> + <string name="login_error_unable_login">Kan inte logga in</string> + <string name="login_error_registration_network_error">Kan inte registrera: Nätverksfel</string> + <string name="login_error_unable_register">Kan inte registrera</string> + <string name="login_error_unable_register_mail_ownership">Kan inte registrera: fel vid med e-postägandeskap</string> + <string name="login_error_invalid_home_server">Vänligen skriv in en giltig URL</string> + <string name="login_error_unknown_host">Den här URLen kan inte nås, vänligen kolla den</string> + <string name="login_error_no_homeserver_found">Det här är inte en giltig Matrixserveradress</string> + <string name="login_error_homeserver_not_found">Kan inte nå en hemserver på den här URLen, vänligen kolla den</string> + <string name="login_error_ssl_handshake">Din enhet använder ett utdaterat TLS-protokoll, sårbart för anfall, så för din säkerhets skull så kommer du inte kunna ansluta</string> + <string name="login_mobile_device">Mobil</string> + + <string name="login_error_forbidden">Felaktigt användarnamn/lösenord</string> + <string name="login_error_unknown_token">Den åtkomsttoken du specificerade kändes inte igen</string> + <string name="login_error_bad_json">Felformaterad JSON</string> + <string name="login_error_not_json">Innehöll inte giltig JSON</string> + <string name="login_error_limit_exceeded">För många förfrågningar har skickats</string> +</resources> diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index 6f6d809624..f460df6d24 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -210,9 +210,7 @@ <string name="auth_recaptcha_message">此主服务器想确认您不是机器人</string> <string name="auth_reset_password_email_validation_message">一封电子邮件已发送至 %s。点击了其中的链接后,请点击下面。</string> <string name="auth_reset_password_error_unauthorized">电子邮箱地址验证失败:请确保您已点击邮件中的链接</string> - <string name="auth_reset_password_success_message">密码已重设。 - -您已从所有设备注销并且不再接受推送通知。要重新启用通知,请重新在相应设备上登录。</string> + <string name="auth_reset_password_success_message">密码已重置。 您已从所有设备注销并且不再接受推送通知。要重新启用通知,请重新在相应设备上登录。</string> <string name="compression_opt_list_original">原始</string> <string name="attachment_remaining_time_seconds">%d 秒</string> @@ -519,7 +517,7 @@ 这只会发生一次。 请谅解由此造成的不便。</string> <string name="read_receipts_list">已读标签清单</string> - <string name="compression_options">"发送为 "</string> + <string name="compression_options">发送为</string> <string name="permissions_rationale_msg_contacts">Riot 需要访问您的通讯录,才能根据电子邮箱地址和手机号码查找其他 Matrix 用户。 请在接下来的弹出窗口中授权允许访问。</string> @@ -888,7 +886,7 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意 <string name="e2e_re_request_encryption_key_sent">已发送密钥共享请求。</string> <string name="e2e_re_request_encryption_key_dialog_title">已请求</string> - <string name="e2e_re_request_encryption_key_dialog_content">请在可解密此消息的设备上启动 Riot,以便其将密钥发送至当前设备。</string> + <string name="e2e_re_request_encryption_key_dialog_content">请在其他可解密此消息的设备上启动 Riot,以便其将密钥发送至当前设备。</string> <string name="lock_screen_hint">在此输入…</string> diff --git a/vector/src/main/res/values/colors_riot.xml b/vector/src/main/res/values/colors_riot.xml index 85e3a6d31a..faecfb66d3 100644 --- a/vector/src/main/res/values/colors_riot.xml +++ b/vector/src/main/res/values/colors_riot.xml @@ -5,6 +5,7 @@ <color name="vector_success_color">#70BF56</color> <color name="vector_warning_color">#ff4b55</color> <color name="vector_error_color">#ff4b55</color> + <color name="vector_info_color">#2f9edb</color> <!-- main app colors --> <color name="vector_fuchsia_color">#ff4b55</color> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ff821f5b95..4f8474760d 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1563,6 +1563,9 @@ Why choose Riot.im? <string name="message_view_reaction">View Reactions</string> <string name="reactions">Reactions</string> + <string name="event_redacted">Message deleted</string> + <string name="settings_show_redacted">Show removed messages</string> + <string name="settings_show_redacted_summary">Show a placeholder for removed messages</string> <string name="event_redacted_by_user_reason">Event deleted by user</string> <string name="event_redacted_by_admin_reason">Event moderated by room admin</string> <string name="last_edited_info_message">Last edited by %1$s on %2$s</string> @@ -1730,14 +1733,16 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming <string name="settings_discovery_disconnect_identity_server_info">Disconnecting from your identity server will mean you won’t be discoverable by other users and you won’t be able to invite others by email or phone.</string> <string name="settings_discovery_msisdn_title">Discoverable phone numbers</string> <string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string> + <string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string> <string name="settings_discovery_mail_pending">Pending</string> - <string name="settings_discovery_enter_identity_server">Enter a new identity server</string> + <string name="settings_discovery_enter_identity_server">Enter an identity server URL</string> <string name="settings_discovery_bad_identity_server">Could not connect to identity server</string> <string name="settings_discovery_please_enter_server">Please enter the identity server url</string> <string name="settings_discovery_no_terms_title">Identity server has no terms of services</string> <string name="settings_discovery_no_terms">The identity server you have chosen does not have any terms of services. Only continue if you trust the owner of the service</string> <string name="settings_text_message_sent">A text message has been sent to %s. Please enter the verification code it contains.</string> + <string name="settings_text_message_sent_wrong_code">The verification code is not correct.</string> <string name="settings_discovery_disconnect_with_bound_pid">You are currently sharing email addresses or phone numbers on the identity server %1$s. You will need to reconnect to %2$s to stop sharing them.</string> <string name="settings_agree_to_terms">Agree to the identity server (%s) Terms of Service to allow yourself to be discoverable by email address or phone number.</string> @@ -1939,6 +1944,14 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming <item quantity="other">Too many requests have been sent. You can retry in %1$d seconds…</item> </plurals> + <string name="login_connect_using_matrix_id_notice">Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method:</string> + <string name="login_connect_using_matrix_id_submit">Sign in with my Matrix identifier</string> + <string name="login_signin_matrix_id_title">Sign in</string> + <string name="login_signin_matrix_id_notice">Enter your identifier and your password</string> + <string name="login_signin_matrix_id_hint">User identifier</string> + <string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string> + <string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string> + <string name="seen_by">Seen by</string> <string name="signed_out_title">You’re signed out</string> @@ -2155,6 +2168,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming <string name="qr_code_scanned_by_other_no">No</string> <string name="no_connectivity_to_the_server_indicator">Connectivity to the server has been lost</string> + <string name="no_connectivity_to_the_server_indicator_airplane">Airplane mode is on</string> <string name="settings_dev_tools">Dev Tools</string> <string name="settings_account_data">Account Data</string> @@ -2384,4 +2398,24 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming </plurals> <string name="invite_users_to_room_failure">We could not invite users. Please check the users you want to invite and try again.</string> -</resources> \ No newline at end of file + <string name="choose_locale_current_locale_title">Current language</string> + <string name="choose_locale_other_locales_title">Other available languages</string> + <string name="choose_locale_loading_locales">Loading available languages…</string> + + <string name="open_terms_of">Open terms of %s</string> + <string name="disconnect_identity_server_dialog_content">Disconnect from the identity server %s?</string> + <string name="identity_server_error_outdated_identity_server">This identity server is outdated. RiotX support only API V2.</string> + <string name="identity_server_error_outdated_home_server">This operation is not possible. The homeserver is outdated.</string> + <string name="identity_server_error_no_identity_server_configured">Please first configure an identity server.</string> + <string name="identity_server_error_terms_not_signed">Please first accepts the terms of the identity server in the settings.</string> + <string name="identity_server_error_bulk_sha256_not_supported">For your privacy, RiotX only supports sending hashed user emails and phone number.</string> + <string name="identity_server_error_binding_error">The association has failed.</string> + <string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string> + + <string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string> + <string name="identity_server_set_default_submit">Use %1$s</string> + <string name="identity_server_set_alternative_notice">Alternatively, you can enter any other identity server URL</string> + <string name="identity_server_set_alternative_notice_no_default">Enter the URL of an identity server</string> + <string name="identity_server_set_alternative_submit">Submit</string> + +</resources> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml deleted file mode 100644 index dd1043819d..0000000000 --- a/vector/src/main/res/values/strings_riotX.xml +++ /dev/null @@ -1,68 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - - <!-- - - - - - - - - - - - - - - PLEASE DO NOT ADD NEW STRINGS HERE, THE FILE WILL BE DELETED, ONCE ALL PR WILL BE MERGED - - - - - - - - - - - - --> - - <!-- Strings not defined in Riot --> - - <!-- Sections has been created to limit merge conflicts. --> - - <!-- BEGIN Strings added by Valere --> - - <!-- END Strings added by Valere --> - - - <!-- BEGIN Strings added by Benoit --> - - <!-- END Strings added by Benoit --> - - - <!-- BEGIN Strings added by Ganfra --> - - <!-- END Strings added by Ganfra --> - - - <!-- BEGIN Strings added by Onuray --> - - <!-- END Strings added by Onuray --> - - - <!-- BEGIN Strings added by Others --> - - <!-- END Strings added by Others --> - - <string name="login_connect_using_matrix_id_notice">Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method:</string> - <string name="login_connect_using_matrix_id_submit">Sign in with my Matrix identifier</string> - <string name="login_signin_matrix_id_title">Sign in</string> - <string name="login_signin_matrix_id_notice">Enter your identifier and your password</string> - <string name="login_signin_matrix_id_hint">User identifier</string> - <string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string> - <string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string> - -</resources> diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 11f16655a7..15d43ddc34 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -43,6 +43,13 @@ android:title="@string/settings_add_phone_number" app:iconTint="?attr/vctr_settings_icon_tint_color" /> + <im.vector.riotx.core.preference.VectorPreference + android:order="1000" + android:persistent="false" + android:summary="@string/settings_discovery_manage" + android:title="@string/settings_discovery_category" + app:fragment="im.vector.riotx.features.discovery.DiscoverySettingsFragment" /> + </im.vector.riotx.core.preference.VectorPreferenceCategory> <im.vector.riotx.core.preference.VectorPreferenceCategory @@ -76,7 +83,7 @@ <im.vector.riotx.core.preference.VectorPreference android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" android:title="@string/settings_identity_server" - app:isPreferenceVisible="@bool/false_not_implemented" + app:fragment="im.vector.riotx.features.discovery.DiscoverySettingsFragment" tools:summary="https://identity.server.url" /> <im.vector.riotx.core.preference.VectorPreference diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index e7217b7394..d290b62825 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -7,9 +7,10 @@ android:title="@string/settings_user_interface"> <im.vector.riotx.core.preference.VectorPreference - android:dialogTitle="@string/settings_select_language" android:key="SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY" - android:title="@string/settings_interface_language" /> + android:persistent="false" + android:title="@string/settings_interface_language" + app:fragment="im.vector.riotx.features.settings.locale.LocalePickerFragment" /> <im.vector.riotx.core.preference.VectorListPreference android:defaultValue="light" @@ -23,6 +24,7 @@ <im.vector.riotx.core.preference.VectorPreference android:dialogTitle="@string/font_size" android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY" + android:persistent="false" android:title="@string/font_size" /> </im.vector.riotx.core.preference.VectorPreferenceCategory> @@ -68,6 +70,12 @@ android:summary="@string/settings_show_read_receipts_summary" android:title="@string/settings_show_read_receipts" /> + <im.vector.riotx.core.preference.VectorSwitchPreference + android:defaultValue="true" + android:key="SETTINGS_SHOW_REDACTED_KEY" + android:summary="@string/settings_show_redacted_summary" + android:title="@string/settings_show_redacted" /> + <im.vector.riotx.core.preference.VectorSwitchPreference android:defaultValue="true" android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"