diff --git a/CHANGES.md b/CHANGES.md index 3d43096e69..869b034f45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,23 @@ +Changes in RiotX 0.20.0 (2020-05-15) +=================================================== + +Features ✨: + - Add Direct Shortcuts (#652) + +Improvements 🙌: + - Invite member(s) to an existing room (#1276) + - Improve notification accessibility with ticker text (#1226) + - Support homeserver discovery from MXID (DISABLED: waiting for design) (#476) + +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) + - Crash | crash while restoring key backup (#1366) + +SDK API changes ⚠️: + - excludedUserIds parameter added to the UserService.getPagedUsersLive() function + Changes in RiotX 0.19.0 (2020-05-04) =================================================== diff --git a/gradle.properties b/gradle.properties index 2e2b110f15..d9d9e57cbc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx8192m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index 193b5c3fbf..469bc514e0 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single @@ -95,6 +96,10 @@ class RxRoom(private val room: Room) { fun liveNotificationState(): Observable { return room.getLiveRoomNotificationState().asObservable() } + + fun invite(userId: String, reason: String? = null): Completable = completableBuilder { + room.invite(userId, reason, it) + } } fun Room.rx(): RxRoom { 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 c2c8978500..e92da1e424 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 @@ -90,8 +90,8 @@ class RxSession(private val session: Session) { return session.getIgnoredUsersLive().asObservable() } - fun livePagedUsers(filter: String? = null): Observable> { - return session.getPagedUsersLive(filter).asObservable() + fun livePagedUsers(filter: String? = null, excludedUserIds: Set? = null): Observable> { + return session.getPagedUsersLive(filter, excludedUserIds).asObservable() } fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt index f10f2fef0e..0270c34a37 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -19,13 +19,13 @@ package im.vector.matrix.android.internal.crypto.keysbackup import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CryptoTestData -import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper +import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2 /** * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword] */ data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, - val aliceKeys: List, + val aliceKeys: List, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session) { fun cleanUp(testHelper: CommonTestHelper) { 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 140d1c259f..5150420de2 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 @@ -23,6 +23,7 @@ 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 import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -30,7 +31,6 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to authenticate or to create an account to a matrix server. */ interface AuthenticationService { - /** * Request the supported login flows for this homeserver. * This is the first method to call to be able to get a wizard to login or the create an account @@ -89,4 +89,20 @@ interface AuthenticationService { fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + callback: MatrixCallback): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index 72affe24bb..d88cd5e74d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -24,16 +24,38 @@ import im.vector.matrix.android.internal.util.md5 * This data class hold credentials user data. * You shouldn't have to instantiate it. * The access token should be use to authenticate user in all server requests. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login */ @JsonClass(generateAdapter = true) data class Credentials( + /** + * The fully-qualified Matrix ID that has been registered. + */ @Json(name = "user_id") val userId: String, - @Json(name = "home_server") val homeServer: String, + /** + * An access token for the account. This access token can then be used to authorize other requests. + */ @Json(name = "access_token") val accessToken: String, + /** + * Not documented + */ @Json(name = "refresh_token") val refreshToken: String?, + /** + * The server_name of the homeserver on which the account has been registered. + * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) + * if they require it. Note also that homeserver is not spelt this way. + */ + @Json(name = "home_server") val homeServer: String, + /** + * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. + */ @Json(name = "device_id") val deviceId: String?, - // Optional data that may contain info to override home server and/or identity server - @Json(name = "well_known") val wellKnown: WellKnown? = null + /** + * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to + * reconfigure themselves, optionally validating the URLs within. + * This object takes the same form as the one returned from .well-known autodiscovery. + */ + @Json(name = "well_known") val discoveryInformation: DiscoveryInformation? = null ) internal fun Credentials.sessionId(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt new file mode 100644 index 0000000000..2aa741bad3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.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.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is a light version of Wellknown model, used for login response + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class DiscoveryInformation( + /** + * Required. Used by clients to discover homeserver information. + */ + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + /** + * Used by clients to discover identity server information. + * Note: matrix.org does not send this field + */ + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt index bdad4702b7..9dd1fa2012 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnown.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict /** * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery @@ -52,7 +53,7 @@ data class WellKnown( val identityServer: WellKnownBaseConfig? = null, @Json(name = "m.integrations") - val integrations: Map? = null + val integrations: JsonDict? = null ) { /** * Returns the list of integration managers proposed diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt index 33ed412a2a..ffdea37afe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/WellKnownManagerConfig.kt @@ -16,6 +16,6 @@ package im.vector.matrix.android.api.auth.data data class WellKnownManagerConfig( - val apiUrl : String, + val apiUrl: String, val uiUrl: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000..58c7cf730e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.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.api.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homeServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 453400bc99..1abda8ec05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -61,9 +61,10 @@ interface UserService { /** * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * @param filter the filter. It will look into userId and displayName. + * @param excludedUserIds userId list which will be excluded from the result list. * @return a Livedata of users */ - fun getPagedUsersLive(filter: String? = null): LiveData> + fun getPagedUsersLive(filter: String? = null, excludedUserIds: Set? = null): LiveData> /** * Get list of ignored users 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 6b6321de36..232cb3f541 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,6 +25,10 @@ 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.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration @@ -59,14 +63,20 @@ internal abstract class AuthModule { } @Binds - abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore @Binds - abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore @Binds - abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService @Binds - abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator + 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 85c2cdbf3d..997cf70e5a 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedByS import im.vector.matrix.android.api.auth.data.isSupportedBySdk 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 import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -38,11 +39,16 @@ 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.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 +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 im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.toCancelable import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -59,7 +65,10 @@ internal class DefaultAuthenticationService @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -148,27 +157,71 @@ internal class DefaultAuthenticationService @Inject constructor( val authAPI = buildAuthAPI(homeServerConnectionConfig) // Ok, try to get the config.json file of a RiotWeb client - val riotConfig = executeRequest(null) { - apiCall = authAPI.getRiotConfig() - } - - if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { - // Ok, good sign, we got a default hs url - val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) - ) - - val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - - val versions = executeRequest(null) { - apiCall = newAuthAPI.versions() + return runCatching { + executeRequest(null) { + apiCall = authAPI.getRiotConfig() } - - return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) - } else { - // Config exists, but there is no default homeserver url (ex: https://riot.im/app) - throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) } + .map { riotConfig -> + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Try with wellknown + getWellknownLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + + private suspend fun getWellknownLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val domain = homeServerConnectionConfig.homeServerUri.host + ?: throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + + // Create a fake userId, for the getWellknown task + val fakeUserId = "@alice:$domain" + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId)) + + return when (wellknownResult) { + is WellknownResult.Prompt -> { + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(wellknownResult.homeServerUrl), + identityServerUri = wellknownResult.identityServerUrl?.let { Uri.parse(it) } + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) + } + else -> throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + }.exhaustive } private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { @@ -260,6 +313,26 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getWellKnownData(matrixId: String, callback: MatrixCallback): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + private suspend fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { sessionCreator.createSession(credentials, homeServerConnectionConfig) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt index 95a9fbb506..74f7cad67d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt @@ -46,14 +46,14 @@ internal class DefaultSessionCreator @Inject constructor( val sessionParams = SessionParams( credentials = credentials, homeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = credentials.wellKnown?.homeServer?.baseURL + homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } ?.also { Timber.d("Overriding homeserver url to $it") } ?.let { Uri.parse(it) } ?: homeServerConnectionConfig.homeServerUri, - identityServerUri = credentials.wellKnown?.identityServer?.baseURL + identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } 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/wellknown/DirectLoginTask.kt new file mode 100644 index 0000000000..01a3ab192d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.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.auth.wellknown + +import dagger.Lazy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionCreator +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } +} 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/auth/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000..8ed2cb3b0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt @@ -0,0 +1,199 @@ +/* + * 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.auth.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import im.vector.matrix.android.api.MatrixPatterns +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.task.Task +import im.vector.matrix.android.internal.util.isValidUrl +import okhttp3.OkHttpClient +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task { + data class Params( + val matrixId: String + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + return findClientConfig(homeServerDomain) + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String): WellknownResult { + val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { + val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) + .create(IdentityPingApi::class.java) + + return try { + executeRequest(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} 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/auth/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000..71928123bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.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.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 1d3c0f4dcd..d529cf4ae5 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor( } override fun getCryptoDeviceInfo(userId: String): List { - return cryptoStore.getUserDevices(userId)?.map { it.value }?.sortedBy { it.deviceId } ?: emptyList() + return cryptoStore.getUserDeviceList(userId) ?: emptyList() } override fun getLiveCryptoDeviceInfo(): LiveData> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt index d6538f041d..d9fa8d5955 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/SetDeviceVerificationAction.kt @@ -48,7 +48,7 @@ internal class SetDeviceVerificationAction @Inject constructor( if (device.trustLevel != trustLevel) { device.trustLevel = trustLevel - cryptoStore.storeUserDevice(userId, device) + cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 888e436ea0..69f0985391 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -164,14 +164,6 @@ internal interface IMXCryptoStore { */ fun saveOlmAccount() - /** - * Store a device for a user. - * - * @param userId the user's id. - * @param device the device to store. - */ - fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) - /** * Retrieve a device for a user. * @@ -415,7 +407,7 @@ internal interface IMXCryptoStore { fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) - fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) + fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) fun clearOtherUserTrust() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index ac7f00b531..7064663995 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -233,29 +233,6 @@ internal class RealmCryptoStore @Inject constructor( return olmAccount!! } - override fun storeUserDevice(userId: String?, deviceInfo: CryptoDeviceInfo?) { - if (userId == null || deviceInfo == null) { - return - } - - doRealmTransaction(realmConfiguration) { realm -> - val user = UserEntity.getOrCreate(realm, userId) - - // Create device info - val deviceInfoEntity = CryptoMapper.mapToEntity(deviceInfo) - realm.insertOrUpdate(deviceInfoEntity) -// val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply { -// deviceId = deviceInfo.deviceId -// identityKey = deviceInfo.identityKey() -// putDeviceInfo(deviceInfo) -// } - - if (!user.devices.contains(deviceInfoEntity)) { - user.devices.add(deviceInfoEntity) - } - } - } - override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? { return doWithRealm(realmConfiguration) { it.where() @@ -1276,7 +1253,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean) { + override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { doRealmTransaction(realmConfiguration) { realm -> realm.where(DeviceInfoEntity::class.java) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) @@ -1289,7 +1266,7 @@ internal class RealmCryptoStore @Inject constructor( deviceInfoEntity.trustLevelEntity = it } } else { - trustEntity.locallyVerified = locallyVerified + locallyVerified?.let { trustEntity.locallyVerified = it } trustEntity.crossSignedVerified = crossSignedVerified } } @@ -1429,7 +1406,7 @@ internal class RealmCryptoStore @Inject constructor( } else { // Just override existing, caller should check and untrust id needed val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) - existing.crossSigningKeys.forEach { it.deleteFromRealm() } + existing.crossSigningKeys.deleteAllFromRealm() existing.crossSigningKeys.addAll( info.crossSigningKeys.map { crossSigningKeysMapper.map(it) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index c604250be7..885abb776d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -45,7 +45,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi // Version 1L added Cross Signing info persistence companion object { - const val CRYPTO_STORE_SCHEMA_VERSION = 5L + const val CRYPTO_STORE_SCHEMA_VERSION = 6L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -56,6 +56,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -255,4 +256,22 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } } } + + // Fixes duplicate devices in UserEntity#devices + private fun migrateTo6(realm: DynamicRealm) { + val userEntities = realm.where("UserEntity").findAll() + userEntities.forEach { + try { + val deviceList = it.getList(UserEntityFields.DEVICES.`$`) + ?: return@forEach + val distinct = deviceList.distinctBy { it.getString(DeviceInfoEntityFields.DEVICE_ID) } + if (distinct.size != deviceList.size) { + deviceList.clear() + deviceList.addAll(distinct) + } + } catch (failure: Throwable) { + Timber.w(failure, "Crypto Data base migration error for migrateTo6") + } + } + } } 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/identity/IdentityPingApi.kt new file mode 100644 index 0000000000..2a0e00704c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.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.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface IdentityPingApi { + + /** + * 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 + */ + @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) + fun ping(): Call +} 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 c6c10d9a8f..ab6745148f 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 @@ -26,4 +26,10 @@ internal object NetworkConstants { // Media private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" 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_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" } 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 b20235448a..0ebfc1c4c5 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.openid.OpenIdModule import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker import im.vector.matrix.android.internal.session.pushers.PushersModule @@ -70,6 +71,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers CacheModule::class, CryptoModule::class, PushersModule::class, + OpenIdModule::class, AccountDataModule::class, ProfileModule::class, SessionAssistedInjectModule::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/GetOpenIdTokenTask.kt new file mode 100644 index 0000000000..c8f394dc47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/GetOpenIdTokenTask.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.openid + +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 org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetOpenIdTokenTask : Task + +internal class DefaultGetOpenIdTokenTask @Inject constructor( + @UserId private val userId: String, + private val openIdAPI: OpenIdAPI, + private val eventBus: EventBus) : GetOpenIdTokenTask { + + override suspend fun execute(params: Unit): RequestOpenIdTokenResponse { + return executeRequest(eventBus) { + apiCall = openIdAPI.openIdToken(userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdAPI.kt new file mode 100644 index 0000000000..ee2e85a33e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdAPI.kt @@ -0,0 +1,38 @@ +/* + * 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.openid + +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.POST +import retrofit2.http.Path + +internal interface OpenIdAPI { + + /** + * Gets a bearer token from the homeserver that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-openid-request-token + * + * @param userId the user id + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") + fun openIdToken(@Path("userId") userId: String, @Body body: JsonDict = emptyMap()): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdModule.kt new file mode 100644 index 0000000000..c6993167e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/OpenIdModule.kt @@ -0,0 +1,38 @@ +/* + * 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.openid + +import dagger.Binds +import dagger.Module +import dagger.Provides +import retrofit2.Retrofit + +@Module +internal abstract class OpenIdModule { + + @Module + companion object { + @JvmStatic + @Provides + fun providesOpenIdAPI(retrofit: Retrofit): OpenIdAPI { + return retrofit.create(OpenIdAPI::class.java) + } + } + + @Binds + abstract fun bindGetOpenIdTokenTask(task: DefaultGetOpenIdTokenTask): GetOpenIdTokenTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/RequestOpenIdTokenResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/RequestOpenIdTokenResponse.kt new file mode 100644 index 0000000000..4beb3fe420 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/openid/RequestOpenIdTokenResponse.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.matrix.android.internal.session.openid + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class RequestOpenIdTokenResponse( + /** + * Required. An access token the consumer may use to verify the identity of the person who generated the token. + * This is given to the federation API GET /openid/userinfo to verify the user's identity. + */ + @Json(name = "access_token") + val openIdToken: String, + + /** + * Required. The string "Bearer". + */ + @Json(name = "token_type") + val tokenType: String, + + /** + * Required. The homeserver domain the consumer should use when attempting to verify the user's identity. + */ + @Json(name = "matrix_server_name") + val matrixServerName: String, + + /** + * Required. The number of seconds before this token expires and a new one must be generated. + */ + @Json(name = "expires_in") + val expiresIn: Int +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index 93b3889455..5a8b302f1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -39,6 +39,8 @@ internal class DefaultInviteTask @Inject constructor( return executeRequest(eventBus) { val body = InviteBody(params.userId, params.reason) apiCall = roomAPI.invite(params.roomId, body) + isRetryable = true + maxRetryCount = 3 } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 761c810b41..7cd2f1b743 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona ) } - override fun getPagedUsersLive(filter: String?): LiveData> { + override fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { realmDataSourceFactory.updateQuery { realm -> val query = realm.where(UserEntity::class.java) if (filter.isNullOrEmpty()) { @@ -104,6 +104,11 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .contains(UserEntityFields.USER_ID, filter) .endGroup() } + excludedUserIds + ?.takeIf { it.isNotEmpty() } + ?.let { + query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) + } query.sort(UserEntityFields.DISPLAY_NAME) } return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt index e57daed617..4adcee88aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.user -import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.user.model.SearchUsersParams import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse import retrofit2.Call @@ -30,6 +30,6 @@ internal interface SearchUserAPI { * * @param searchUsersParams the search params. */ - @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt index 824af2d1c3..65ec05e76e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataAPI.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.user.accountdata import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body -import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path @@ -34,15 +33,4 @@ interface AccountDataAPI { */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call - - /** - * Gets a bearer token from the homeserver that the user can - * present to a third party in order to prove their ownership - * of the Matrix account they are logged into. - * - * @param userId the user id - * @param body the body content - */ - @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token") - fun openIdToken(@Path("userId") userId: String, @Body body: Map): Call> } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt similarity index 61% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt index eeffc1f119..8ad5e89605 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -1,5 +1,5 @@ /* - * 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. @@ -14,12 +14,15 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.matrix.android.internal.util -import im.vector.riotx.core.platform.VectorSharedAction +import java.net.URL -sealed class CreateDirectRoomSharedAction : VectorSharedAction { - object OpenUsersDirectory : CreateDirectRoomSharedAction() - object Close : CreateDirectRoomSharedAction() - object GoBack : CreateDirectRoomSharedAction() +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } } 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 @@ %1$s\'s Einladung. Grund: %2$s %1$s hat %2$s eingeladen. Grund: %3$s %1$s hat dich eingeladen. Grund: %2$s - %1$s beigetreten. Grund: %2$s - %1$s ging. Grund: %2$s + %1$s ist dem Raum beigetreten. Grund: %2$s + %1$s hat den Raum verlassen. Grund: %2$s %1$s hat die Einladung abgelehnt. Grund: %2$s %1$s hat %2$s gekickt. Grund: %3$s %1$s hat Sperre von %2$s aufgehoben. Grund: %3$s 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 @@ %1$s sendis bildon. %1$s sendis glumarkon. - invito de %s - %1$s invitis %2$s + Invito de %s + %1$s invitis uzanton %2$s %1$s invitis vin %1$s alvenis %1$s foriris %1$s malakceptis la inviton - %1$s forpelis %2$s - %1$s malforbaris %2$s - %1$s forbaris %2$s - %1$s malinvitis %2$s + %1$s forpelis uzanton %2$s + %1$s malforbaris uzanton %2$s + %1$s forbaris uzanton %2$s + %1$s nuligis inviton por %2$s %1$s ŝanĝis sian profilbildon ** Ne eblas malĉifri: %s ** La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo. - Respondanta al + Responde al + + %1$s: %2$s + %1$s ŝanĝis sian vidigan nomon al %2$s + %1$s ŝanĝis sian vidigan nomon de %2$s al %3$s + %1$s forigis sian vidigan nomon (%2$s) + %1$s ŝanĝis la temon al: %2$s + %1$s ŝanĝis nomon de la ĉambro al: %2$s + %s vidvokis. + %s voĉvokis. + %s respondis la vokon. + %s finis la vokon. + %1$s videbligis estontan historion de ĉambro al %2$s + ĉiuj ĉambranoj, ekde iliaj invitoj. + ĉiuj ĉambranoj, ekde iliaj aliĝoj. + ĉiuj ĉambranoj. + ĉiu ajn. + nekonata (%s). + %1$s ŝaltis tutvojan ĉifradon (%2$s) + %s gradaltigis la ĉambron. + + Mesaĝo foriĝis + Mesaĝo foriĝis de %1$s + Mesaĝo foriĝis [kialo: %1$s] + Mesaĝo foriĝis de %1$s [kialo: %2$s] + %1$s ĝisdatigis sian profilon %2$s + %1$s sendis aliĝan inviton al %2$s + %1$s nuligis la aliĝan inviton por %2$s + %1$s akceptis la inviton por %2$s + + Ne povis redakti + Ne povas sendi mesaĝon + + Malsukcesis alŝuti bildon + + Reta eraro + Matrix-eraro + + Nun ne eblas re-aliĝi al malplena ĉambro + + Ĉifrita mesaĝo + + Retpoŝtadreso + Telefonnumero + + sendis bildon. + sendis filmon. + sendis sondosieron. + sendis dosieron. + + Invito de %s + Ĉambra invito + + %1$s kaj %2$s + + + %1$s kaj 1 alia + %1$s kaj %2$d aliaj + + + Malplena ĉambro + + + Hundo + Kato + Leono + Ĉevalo + Unukorno + Porko + Elefanto + Kuniklo + Pando + Koko + Pingveno + Testudo + Fiŝo + Polpo + Papilio + Floro + Arbo + Kakto + Fungo + Globo + Luno + Nubo + Fajro + Banano + Pomo + Frago + Maizo + Pico + Kuko + Koro + Mieneto + Roboto + Ĉapelo + Okulvitroj + Boltilo + Kristnaska viro + Dikfingro supren + Ombrelo + Sablohorloĝo + Horloĝo + Donaco + Lampo + Libro + Grifelo + Paperkuntenilo + Tondilo + Seruro + Ŝlosilo + Martelo + Telefono + Flago + Vagonaro + Biciklo + Aviadilo + Raketo + Trofeo + Pilko + Gitaro + Trumpeto + Sonorilo + Ankro + Kapaŭdilo + Dosierujo + Pinglo + + Komenca spegulado: +\nEnportante konton… + Komenca spegulado: +\nEnportante ĉifrilojn + Komenca spegulado: +\nEnportante ĉambrojn + Komenca spegulado: +\nEnportante aliĝitajn ĉambrojn + Komenca spegulado: +\nEnportante ĉambrojn de invitoj + Komenca spegulado: +\nEnportante forlasitajn ĉambrojn + Komenca spegulado: +\nEnportante komunumojn + Komenca spegulado: +\nEnportante datumojn de konto + + Sendante mesaĝon… + Vakigi sendan atendovicon + + %1$s petis grupan vokon + Grupa voko komenciĝis + Grupa voko finiĝis + + (ankaŭ profilbildo ŝanĝiĝis) + %1$s forigis nomon de la ĉambro + %1$s forigis temon de la ĉambro + Invito de %1$s. Kialo: %2$s + %1$s invitis uzanton %2$s. Kialo: %3$s + %1$s invitis vin. Kialo: %2$s + %1$s aliĝis al la ĉambro. Kialo: %2$s + %1$s foriris de la ĉambro. Kialo: %2$s + %1$s rifuzis la inviton. Kialo: %2$s + %1$s forpelis uzanton %2$s. Kialo: %3$s + %1$s malforbaris uzanton %2$s. Kialo: %3$s + %1$s forbaris uzanton %2$s. Kialo: %3$s + %1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s akceptis la inviton por %2$s. Kialo: %3$s + %1$s nuligis la inviton al %2$s. Kialo: %3$s + + + %1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro. + %1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro. + + + + %1$s forigis %2$s kiel adreson por ĉi tiu ĉambro. + %1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro. + + + %1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro. + + %1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s. + %1$s forigis la ĉefadreson de ĉi tiu ĉambro. + + %1$s permesis al gastoj aliĝi al la ĉambro. + %1$s malpermesis al gastoj aliĝi al la ĉambro. + + %1$s ŝaltis tutvojan ĉifradon. + %1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s). + + %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj. 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 @@ + + + %1$s: %2$s + %1$s saatis pildi. + %1$s saatis kleepsu. + + Kasutaja %s kutse + %1$s kutsus kasutajat %2$s + %1$s kutsus sind + %1$s liitus jututoaga + %1$s lahkus jututoast + %1$s lükkas tagasi kutse + %1$s müksas kasutajat %2$s + %1$s võttis tagasi kutse kasutajale %2$s + %1$s muutis oma avatari + %1$s määras oma kuvatavaks nimeks %2$s + %1$s muutis senise kuvatava nime %2$s uueks nimeks %3$s + %1$s eemaldas oma kuvatava nime (%2$s) + %1$s muutis uueks teemaks %2$s + %1$s muutis jututoa uueks nimeks %2$s + %s alustas videokõnet. + %s alustas häälkõnet. + %s vastas kõnele. + %s lõpetas kõne. + %1$s seadistas, et tulevane jututoa ajalugu on nähtav kasutajale %2$s + kõikidele jututoa liikmetele alates kutsumise hetkest. + kõikidele jututoa liikmetele alates liitumise hetkest. + kõikidele jututoa liikmetele. + kõikidele. + teadmata (%s). + %1$s lülitas sisse läbiva krüptimise (%2$s) + %s uuendas seda jututuba. + + %1$s saatis VoIP konverentsi kutse + VoIP-konverents algas + VoIP-konverents lõppes + + (samuti sai avatar muudetud) + %1$s eemaldas jututoa nime + %1$s eemaldas jututoa teema + Sõnum on eemaldatud + Sõnum on eemaldatud %1$s poolt + Sõnum on eemaldatud [põhjus: %1$s] + Sõnum on eemaldatud %1$s poolt [põhjus: %2$s] + %1$s uuendas oma profiili %2$s + %1$s saatis jututoaga liitumiseks kutse kasutajale %2$s + %1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s + %1$s võttis vastu kutse %2$s nimel + + ** Ei õnnestu dekrüptida: %s ** + Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid. + + Vastuseks kasutajale + + Ei saanud muuta sõnumit + Sõnumi saatmine ei õnnestunud + + Faili üles laadimine ei õnnestunud + + Võrguühenduse viga + Matrix\'i viga + + Hetkel ei ole võimalik uuesti liituda tühja jututoaga. + + Krüptitud sõnum + + E-posti aadress + Telefoninumber + + saatis pildi. + saatis video. + saatis helifaili. + saatis faili. + + Kutse kasutajalt %s + Kutse jututuppa + + %1$s ja %2$s + + + %1$s ja üks muu + %1$s ja %2$d muud + + + Tühi jututuba + + + Koer + Kass + Lõvi + Hobune + Ükssarvik + Siga + Elevant + Jänes + Panda + Kukk + Pingviin + Kilpkonn + Kala + Kaheksajalg + Liblikas + Lill + Puu + Kaktus + Seen + Maakera + Kuu + Pilv + Tuli + Banaan + Õun + Maasikas + Mais + Pitsa + Kook + Süda + Smaili + Robot + Müts + Prillid + Mutrivõti + Jõuluvana + Pöidlad püsti + Vihmavari + Liivakell + Kell + Kingitus + Lambipirn + Raamat + Pliiats + Kirjaklamber + Käärid + Lukk + Võti + Haamer + Telefon + Lipp + Rong + Jalgratas + Lennuk + Rakett + Auhind + Pall + Kitarr + Trompet + Kelluke + Ankur + Kõrvaklapid + Kaust + Knopka + + Alglaadimine: +\nImpordin kontot… + Alglaadimine: +\nImpordin krüptoseadistusi + Alglaadimine: +\nImpordin jututubasid + Alglaadimine: +\nImpordin liitutud jututubasid + Alglaadimine: +\nImpordin kutsutud jututubasid + Alglaadimine: +\nImpordin lahkutud jututubasid + Alglaadimine: +\nImpordin kogukondi + Alglaadimine: +\nImpordin kontoandmeid + + Saadan sõnumit… + Tühjenda saatmisjärjekord + + Kasutaja %1$s kutse. Põhjus: %2$s + %1$s kutsus kasutajat %2$s. Põhjus: %3$s + %1$s kutsus sind. Põhjus: %2$s + %1$s liitus jututoaga. Põhjus: %2$s + %1$s lahkus jututoast. Põhjus: %2$s + %1$s lükkas kutse tagasi. Põhjus: %2$s + %1$s müksas välja kasutaja %2$s. Põhjus: %3$s + %1$s saatis kasutajale %2$s kutse jututoaga liitumiseks. Põhjus: %3$s + %1$s tühistas kasutajale %2$s saadetud kutse jututoaga liitumiseks. Põhjus: %3$s + %1$s võttis vastu kutse %2$s jututoaga liitumiseks. Põhjus: %3$s + %1$s võttis tagasi kasutajale %2$s saadetud kutse. Põhjus: %3$s + + %1$s lülitas sisse läbiva krüptimise. + %1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s). + + 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 @@ %1$s laittoi päälle osapuolten välisen salauksen. %1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s). + %s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa. + 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 @@ %1$s dërgoi një figurë. %1$s ftoi %2$s %1$s ju ftoi - %1$s u bë pjesë - %1$s iku + %1$s hyri në dhomë + %1$s doli nga dhoma %1$s hodhi tej ftesën %1$s përzuri %2$s %1$s dëboi %2$s @@ -172,8 +172,8 @@ Ftesë e %1$s. Arsye: %2$s %1$s ftoi %2$s. Arsye: %3$s %1$s ju ftoi. Arsye: %2$s - %1$s erdhi. Arsye: %2$s - %1$s iku. Arsye: %2$s + %1$s erdhi në dhomë. Arsye: %2$s + %1$s doli nga dhoma. Arsye: %2$s %1$s hodhi poshtë ftesën. Arsye: %2$s %1$s përzuri %2$s. Arsye: %3$s %1$s hoqi dëbimin për %2$s. Arsye: %3$s 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 @@ %s 的邀请 %1$s 邀请了 %2$s %1$s 邀请了您 - %1$s 加入了 - %1$s 离开了 + %1$s 加入了聊天室 + %1$s 离开了聊天室 %1$s 拒绝了邀请 %1$s 移除了 %2$s %1$s 解封了 %2$s @@ -173,8 +173,8 @@ %1$s 的邀请。理由:%2$s %1$s 邀请了 %2$s。理由:%3$s %1$s 邀请了您。理由:%2$s - %1$s 已加入。理由:%2$s - %1$s 已离开。理由:%2$s + %1$s 加入了聊天室。理由:%2$s + %1$s 离开了聊天室。理由:%2$s %1$s 已拒绝邀请。理由:%2$s %1$s 踢走了 %2$s。理由:%3$s %1$s 取消封锁了 %2$s。理由:%3$s diff --git a/tools/release/sign_apk_unsafe.sh b/tools/release/sign_apk_unsafe.sh index 022f3618eb..bf021e8345 100755 --- a/tools/release/sign_apk_unsafe.sh +++ b/tools/release/sign_apk_unsafe.sh @@ -23,7 +23,7 @@ PARAM_KS_PASS=$3 PARAM_KEY_PASS=$4 # Other params -BUILD_TOOLS_VERSION="28.0.3" +BUILD_TOOLS_VERSION="29.0.3" MIN_SDK_VERSION=19 echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." diff --git a/vector/build.gradle b/vector/build.gradle index 69608cf712..459b297fd6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 19 +ext.versionMinor = 20 ext.versionPatch = 0 static def getGitTimestamp() { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 092817a6cc..ae0ffa1f91 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -85,6 +85,7 @@ + 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 db62ddc2d8..01709efcac 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 @@ -23,8 +23,6 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment -import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment -import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment @@ -63,6 +61,8 @@ 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 @@ -226,13 +226,13 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(CreateDirectRoomDirectoryUsersFragment::class) - fun bindCreateDirectRoomDirectoryUsersFragment(fragment: CreateDirectRoomDirectoryUsersFragment): Fragment + @FragmentKey(UserDirectoryFragment::class) + fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment @Binds @IntoMap - @FragmentKey(CreateDirectRoomKnownUsersFragment::class) - fun bindCreateDirectRoomKnownUsersFragment(fragment: CreateDirectRoomKnownUsersFragment): Fragment + @FragmentKey(KnownUsersFragment::class) + fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment @Binds @IntoMap 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 af49b00b59..c38c0c99e6 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 @@ -39,6 +39,7 @@ import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReaction import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet +import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity @@ -116,6 +117,7 @@ interface ScreenComponent { fun inject(activity: DebugMenuActivity) fun inject(activity: SharedSecureStorageActivity) fun inject(activity: BigImageViewerActivity) + fun inject(activity: InviteUsersToRoomActivity) /* ========================================================================================== * BottomSheets 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 4bb0adb9f0..8046f67668 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 @@ -22,7 +22,6 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.core.platform.ConfigurationViewModel -import im.vector.riotx.features.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel @@ -31,10 +30,10 @@ 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 import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel -import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @Module @@ -87,8 +86,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(CreateDirectRoomSharedActionViewModel::class) - fun bindCreateDirectRoomSharedActionViewModel(viewModel: CreateDirectRoomSharedActionViewModel): ViewModel + @ViewModelKey(UserDirectorySharedActionViewModel::class) + fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel @Binds @IntoMap @@ -110,11 +109,6 @@ interface ViewModelModule { @ViewModelKey(RoomDirectorySharedActionViewModel::class) fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel - @Binds - @IntoMap - @ViewModelKey(LoginSharedActionViewModel::class) - fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(RoomDetailSharedActionViewModel::class) 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 58ec4b22c6..e8e8f21259 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,6 +26,7 @@ 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 @@ -107,4 +108,15 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { } super.onBackPressed() } + + protected fun 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/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt index 0e74ff71fd..f995f82ff7 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.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, @@ -20,10 +20,5 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.core.platform.VectorViewModelAction sealed class CreateDirectRoomAction : VectorViewModelAction { - object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() - data class FilterKnownUsers(val value: String) : CreateDirectRoomAction() - data class SearchDirectoryUsers(val value: String) : CreateDirectRoomAction() - object ClearFilterKnownUsers : CreateDirectRoomAction() - data class SelectUser(val user: User) : CreateDirectRoomAction() - data class RemoveSelectedUser(val user: User) : CreateDirectRoomAction() + data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt index 3ae206cd21..ef3e9bdeff 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt @@ -37,6 +37,12 @@ import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs +import im.vector.riotx.features.userdirectory.UserDirectoryFragment +import im.vector.riotx.features.userdirectory.UserDirectorySharedAction +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectoryViewModel import kotlinx.android.synthetic.main.activity.* import java.net.HttpURLConnection import javax.inject.Inject @@ -44,7 +50,8 @@ import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { private val viewModel: CreateDirectRoomViewModel by viewModel() - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel + @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter @@ -56,26 +63,40 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - sharedActionViewModel = viewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) + sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - CreateDirectRoomSharedAction.OpenUsersDirectory -> - addFragmentToBackstack(R.id.container, CreateDirectRoomDirectoryUsersFragment::class.java) - CreateDirectRoomSharedAction.Close -> finish() - CreateDirectRoomSharedAction.GoBack -> onBackPressed() + UserDirectorySharedAction.OpenUsersDirectory -> + addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) + UserDirectorySharedAction.Close -> finish() + UserDirectorySharedAction.GoBack -> onBackPressed() + is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) } } .disposeOnDestroy() if (isFirstCreation()) { - addFragment(R.id.container, CreateDirectRoomKnownUsersFragment::class.java) + addFragment( + R.id.container, + KnownUsersFragment::class.java, + KnownUsersFragmentArgs( + title = getString(R.string.fab_menu_create_chat), + menuResId = R.menu.vector_create_direct_room + ) + ) } viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) } } + private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { + if (action.itemId == R.id.action_create_direct_room) { + viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers)) + } + } + private fun renderCreateAndInviteState(state: Async) { when (state) { is Loading -> renderCreationLoading() diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt index 0ed584ac6b..5ea344115a 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt @@ -18,7 +18,4 @@ package im.vector.riotx.features.createdirect import im.vector.riotx.core.platform.VectorViewEvents -/** - * Transient events for create direct room screen - */ sealed class CreateDirectRoomViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt index 71fae11486..1800759da6 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt @@ -1,42 +1,31 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect -import arrow.core.Option import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay 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.room.model.create.CreateRoomParams -import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx -import im.vector.riotx.core.extensions.toggle import im.vector.riotx.core.platform.VectorViewModel -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -private typealias KnowUsersFilter = String -private typealias DirectoryUsersSearch = String class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, @@ -48,9 +37,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel } - private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) - private val directoryUsersSearch = BehaviorRelay.create() - companion object : MvRxViewModelFactory { @JvmStatic @@ -60,25 +46,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } - init { - observeKnownUsers() - observeDirectoryUsers() - } - override fun handle(action: CreateDirectRoomAction) { when (action) { - is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() - is CreateDirectRoomAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) - is CreateDirectRoomAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) - is CreateDirectRoomAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) - is CreateDirectRoomAction.SelectUser -> handleSelectUser(action) - is CreateDirectRoomAction.RemoveSelectedUser -> handleRemoveSelectedUser(action) + is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers) } } - private fun createRoomAndInviteSelectedUsers() = withState { currentState -> + private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) { val roomParams = CreateRoomParams( - invitedUserIds = currentState.selectedUsers.map { it.userId } + invitedUserIds = selectedUsers.map { it.userId } ) .setDirectMessage() .enableEncryptionIfInvitedUsersSupportIt() @@ -89,52 +65,4 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted copy(createAndInviteState = it) } } - - private fun handleRemoveSelectedUser(action: CreateDirectRoomAction.RemoveSelectedUser) = withState { state -> - val selectedUsers = state.selectedUsers.minus(action.user) - setState { copy(selectedUsers = selectedUsers) } - } - - private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state -> - // Reset the filter asap - directoryUsersSearch.accept("") - val selectedUsers = state.selectedUsers.toggle(action.user) - setState { copy(selectedUsers = selectedUsers) } - } - - private fun observeDirectoryUsers() { - directoryUsersSearch - .debounce(300, TimeUnit.MILLISECONDS) - .switchMapSingle { search -> - val stream = if (search.isBlank()) { - Single.just(emptyList()) - } else { - session.rx() - .searchUsersDirectory(search, 50, emptySet()) - .map { users -> - users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } - } - } - stream.toAsync { - copy(directoryUsers = it, directorySearchTerm = search) - } - } - .subscribe() - .disposeOnClear() - } - - private fun observeKnownUsers() { - knownUsersFilter - .throttleLast(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { - session.rx().livePagedUsers(it.orNull()) - } - .execute { async -> - copy( - knownUsers = async, - filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() - ) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt index dcf86ef6f1..8bb8c3ce58 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt @@ -1,41 +1,25 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect -import androidx.paging.PagedList -import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.user.model.User data class CreateDirectRoomViewState( - val knownUsers: Async> = Uninitialized, - val directoryUsers: Async> = Uninitialized, - val selectedUsers: Set = emptySet(), - val createAndInviteState: Async = Uninitialized, - val directorySearchTerm: String = "", - val filterKnownUsersValue: Option = Option.empty() -) : MvRxState { - - enum class DisplayMode { - KNOWN_USERS, - DIRECTORY_USERS - } -} + val createAndInviteState: Async = Uninitialized +) : MvRxState 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/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 6d85dd8a3e..2edb2c4edf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -17,11 +17,13 @@ package im.vector.riotx.features.home import android.content.Context +import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget @@ -72,6 +74,28 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .into(target) } + @AnyThread + fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { + return glideRequest + .asBitmap() + .apply { + val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) + if (resolvedUrl != null) { + load(resolvedUrl) + } else { + val avatarColor = avatarColor(matrixItem, context) + load(TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) + .toBitmap(width = iconSize, height = iconSize)) + } + } + .submit(iconSize, iconSize) + .get() + } + @AnyThread fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable { return buildGlideRequest(glideRequest, matrixItem.avatarUrl) @@ -82,10 +106,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @AnyThread fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { - val avatarColor = when (matrixItem) { - is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) - else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) - } + val avatarColor = avatarColor(matrixItem, context) return TextDrawable.builder() .beginConfig() .bold() @@ -96,11 +117,21 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active // PRIVATE API ********************************************************************************* private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { - val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() - ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) - + val resolvedUrl = resolvedUrl(avatarUrl) return glideRequest .load(resolvedUrl) .apply(RequestOptions.circleCropTransform()) } + + private fun resolvedUrl(avatarUrl: String?): String? { + return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() + ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + } + + private fun avatarColor(matrixItem: MatrixItem, context: Context): Int { + return when (matrixItem) { + is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) + else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 60c974c291..b6e3cbcd76 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -65,6 +65,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var popupAlertManager: PopupAlertManager + @Inject lateinit var shortcutsHandler: ShortcutsHandler private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -144,6 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { && activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) { promptCompleteSecurityIfNeeded() } + + shortcutsHandler.observeRoomsAndBuildShortcuts() + .disposeOnDestroy() } private fun promptCompleteSecurityIfNeeded() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt new file mode 100644 index 0000000000..657942457e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt @@ -0,0 +1,88 @@ +/* + * 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.home + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.home.room.detail.RoomDetailActivity +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O +private const val adaptiveIconSizeDp = 108 +private const val adaptiveIconOuterSidesDp = 18 + +class ShortcutsHandler @Inject constructor( + private val context: Context, + private val homeRoomListStore: HomeRoomListDataSource, + private val avatarRenderer: AvatarRenderer, + private val dimensionConverter: DimensionConverter +) { + private val adaptiveIconSize = dimensionConverter.dpToPx(adaptiveIconSizeDp) + private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp) + private val iconSize by lazy { + if (useAdaptiveIcon) { + adaptiveIconSize - adaptiveIconOuterSides + } else { + dimensionConverter.dpToPx(72) + } + } + + fun observeRoomsAndBuildShortcuts(): Disposable { + return homeRoomListStore + .observe() + .distinct() + .observeOn(Schedulers.computation()) + .subscribe { rooms -> + val shortcuts = rooms + .filter { room -> room.tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } } + .take(n = 4) // Android only allows us to create 4 shortcuts + .map { room -> + val intent = RoomDetailActivity.shortcutIntent(context, room.roomId) + val bitmap = avatarRenderer.shortcutDrawable(context, GlideApp.with(context), room.toMatrixItem(), iconSize) + + ShortcutInfoCompat.Builder(context, room.roomId) + .setShortLabel(room.displayName) + .setIcon(bitmap.toProfileImageIcon()) + .setIntent(intent) + .build() + } + + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) + } + } + + // PRIVATE API ********************************************************************************* + + private fun Bitmap.toProfileImageIcon(): IconCompat { + return if (useAdaptiveIcon) { + IconCompat.createWithAdaptiveBitmap(this) + } else { + IconCompat.createWithBitmap(this) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index fe4d0ae1f7..6507bf6030 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -44,8 +44,14 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { super.onCreate(savedInstanceState) waitingView = waiting_view if (isFirstCreation()) { - val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) - ?: return + val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { + RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) + } else { + intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) + } + + if (roomDetailArgs == null) return + currentRoomId = roomDetailArgs.roomId replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) @@ -110,11 +116,20 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" + const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" + const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent { return Intent(context, RoomDetailActivity::class.java).apply { putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs) } } + + fun shortcutIntent(context: Context, roomId: String): Intent { + return Intent(context, RoomDetailActivity::class.java).apply { + action = ACTION_ROOM_DETAILS_FROM_SHORTCUT + putExtra(EXTRA_ROOM_ID, roomId) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt new file mode 100644 index 0000000000..8a62935bdd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.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.invite + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class InviteUsersToRoomAction : VectorViewModelAction { + data class InviteSelectedUsers(val selectedUsers: Set) : InviteUsersToRoomAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt new file mode 100644 index 0000000000..839a0767d8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt @@ -0,0 +1,140 @@ +/* + * 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.invite + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.failure.Failure +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.addFragment +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.platform.SimpleFragmentActivity +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs +import im.vector.riotx.features.userdirectory.UserDirectoryFragment +import im.vector.riotx.features.userdirectory.UserDirectorySharedAction +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectoryViewModel +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.activity.* +import java.net.HttpURLConnection +import javax.inject.Inject + +@Parcelize +data class InviteUsersToRoomArgs(val roomId: String) : Parcelable + +class InviteUsersToRoomActivity : SimpleFragmentActivity() { + + private val viewModel: InviteUsersToRoomViewModel by viewModel() + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel + @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory + @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + toolbar.visibility = View.GONE + sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + UserDirectorySharedAction.OpenUsersDirectory -> + addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) + UserDirectorySharedAction.Close -> finish() + UserDirectorySharedAction.GoBack -> onBackPressed() + is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) + } + } + .disposeOnDestroy() + if (isFirstCreation()) { + addFragment( + R.id.container, + KnownUsersFragment::class.java, + KnownUsersFragmentArgs( + title = getString(R.string.invite_users_to_room_title), + menuResId = R.menu.vector_invite_users_to_room, + excludedUserIds = viewModel.getUserIdsOfRoomMembers() + ) + ) + } + + viewModel.observeViewEvents { renderInviteEvents(it) } + } + + private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { + if (action.itemId == R.id.action_invite_users_to_room_invite) { + viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selectedUsers)) + } + } + + private fun renderInviteEvents(viewEvent: InviteUsersToRoomViewEvents) { + when (viewEvent) { + is InviteUsersToRoomViewEvents.Loading -> renderInviteLoading() + is InviteUsersToRoomViewEvents.Success -> renderInvitationSuccess(viewEvent.successMessage) + is InviteUsersToRoomViewEvents.Failure -> renderInviteFailure(viewEvent.throwable) + } + } + + private fun renderInviteLoading() { + updateWaitingView(WaitingViewData(getString(R.string.inviting_users_to_room))) + } + + private fun renderInviteFailure(error: Throwable) { + hideWaitingView() + val message = if (error is Failure.ServerError && error.httpCode == HttpURLConnection.HTTP_INTERNAL_ERROR /*500*/) { + // This error happen if the invited userId does not exist. + getString(R.string.invite_users_to_room_failure) + } else { + errorFormatter.toHumanReadable(error) + } + AlertDialog.Builder(this) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun renderInvitationSuccess(successMessage: String) { + toast(successMessage) + finish() + } + + companion object { + + fun getIntent(context: Context, roomId: String): Intent { + return Intent(context, InviteUsersToRoomActivity::class.java).also { + it.putExtra(MvRx.KEY_ARG, InviteUsersToRoomArgs(roomId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewEvents.kt new file mode 100644 index 0000000000..a76d4a4077 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewEvents.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.invite + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class InviteUsersToRoomViewEvents : VectorViewEvents { + object Loading : InviteUsersToRoomViewEvents() + data class Failure(val throwable: Throwable) : InviteUsersToRoomViewEvents() + data class Success(val successMessage: String) : InviteUsersToRoomViewEvents() +} 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 new file mode 100644 index 0000000000..fc2f34b7a0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt @@ -0,0 +1,89 @@ +/* + * 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.invite + +import com.airbnb.mvrx.ActivityViewModelContext +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.session.Session +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.rx.rx +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import io.reactivex.Observable + +class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted + initialState: InviteUsersToRoomViewState, + session: Session, + val stringProvider: StringProvider) + : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + + @AssistedInject.Factory + interface Factory { + fun create(initialState: InviteUsersToRoomViewState): InviteUsersToRoomViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: InviteUsersToRoomViewState): InviteUsersToRoomViewModel? { + val activity: InviteUsersToRoomActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.inviteUsersToRoomViewModelFactory.create(state) + } + } + + override fun handle(action: InviteUsersToRoomAction) { + when (action) { + is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selectedUsers) + } + } + + private fun inviteUsersToRoom(selectedUsers: Set) { + _viewEvents.post(InviteUsersToRoomViewEvents.Loading) + + Observable.fromIterable(selectedUsers).flatMapCompletable { user -> + room.rx().invite(user.userId, null) + }.subscribe( + { + val successMessage = when (selectedUsers.size) { + 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, + selectedUsers.first().getBestName()) + 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, + selectedUsers.first().getBestName(), + selectedUsers.last().getBestName()) + else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, + selectedUsers.size - 1, + selectedUsers.first().getBestName(), + selectedUsers.size - 1) + } + _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) + }, + { + _viewEvents.post(InviteUsersToRoomViewEvents.Failure(it)) + }) + .disposeOnClear() + } + + fun getUserIdsOfRoomMembers(): Set { + return room.roomSummary()?.otherMemberIds?.toSet() ?: emptySet() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewState.kt new file mode 100644 index 0000000000..e0c3ec24a3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewState.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.invite + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class InviteUsersToRoomViewState( + val roomId: String, + val inviteState: Async = Uninitialized +) : MvRxState { + + constructor(args: InviteUsersToRoomArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 83263d05a2..8fceaad07f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -38,7 +38,6 @@ import javax.net.ssl.HttpsURLConnection abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { protected val loginViewModel: LoginViewModel by activityViewModel() - protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private var isResetPasswordStarted = false @@ -57,8 +56,6 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 90d6754448..3403760136 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -58,4 +58,6 @@ sealed class LoginAction : VectorViewModelAction { // For the soft logout case data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() + + data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index c67e45d9e7..99d8da490d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -38,6 +38,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.HomeActivity @@ -54,7 +55,6 @@ import javax.inject.Inject open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private val loginViewModel: LoginViewModel by viewModel() - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -98,14 +98,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { loginViewModel.handle(LoginAction.InitWith(loginConfig)) } - loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) - loginSharedActionViewModel - .observe() - .subscribe { - handleLoginNavigation(it) - } - .disposeOnDestroy() - loginViewModel .subscribe(this) { updateWithState(it) @@ -124,65 +116,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) } - private fun handleLoginNavigation(loginNavigation: LoginNavigation) { - // Assigning to dummy make sure we do not forget a case - @Suppress("UNUSED_VARIABLE") - val dummy = when (loginNavigation) { - is LoginNavigation.OpenServerSelection -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginServerSelectionFragment::class.java, - option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // TODO Disabled because it provokes a flickering - // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) - }) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginSignUpSignInSelectionFragment::class.java, - option = commonOption) - is LoginNavigation.OnWebLoginError -> onWebLoginError(loginNavigation) - is LoginNavigation.OnForgetPasswordClicked -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordFragment::class.java, - option = commonOption) - is LoginNavigation.OnResetPasswordSendThreePidDone -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordMailConfirmationFragment::class.java, - option = commonOption) - } - is LoginNavigation.OnResetPasswordMailConfirmationSuccess -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordSuccessFragment::class.java, - option = commonOption) - } - is LoginNavigation.OnResetPasswordMailConfirmationSuccessDone -> { - // Go back to the login fragment - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - } - is LoginNavigation.OnSendEmailSuccess -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWaitForEmailFragment::class.java, - LoginWaitForEmailFragmentArgument(loginNavigation.email), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is LoginNavigation.OnSendMsisdnSuccess -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - } - } - private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { when (loginViewEvents) { - is LoginViewEvents.RegistrationFlowResult -> { + is LoginViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback @@ -203,15 +139,64 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } } } - is LoginViewEvents.OutdatedHomeserver -> + is LoginViewEvents.OutdatedHomeserver -> AlertDialog.Builder(this) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_content) .setPositiveButton(R.string.ok, null) .show() - is LoginViewEvents.Failure -> + is LoginViewEvents.Failure -> // This is handled by the Fragments Unit + is LoginViewEvents.OpenServerSelection -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerSelectionFragment::class.java, + option = { ft -> + findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + }) + is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone() + is LoginViewEvents.OnSignModeSelected -> onSignModeSelected() + is LoginViewEvents.OnLoginFlowRetrieved -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginSignUpSignInSelectionFragment::class.java, + option = commonOption) + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordFragment::class.java, + option = commonOption) + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordSuccessFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents.OnSendEmailSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWaitForEmailFragment::class.java, + LoginWaitForEmailFragmentArgument(loginViewEvents.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is LoginViewEvents.OnSendMsisdnSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) } } @@ -230,7 +215,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { loginLoading.isVisible = loginViewState.isLoading() } - private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { + private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { // Pop the backstack supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) @@ -254,11 +239,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private fun onSignModeSelected() = withState(loginViewModel) { state -> when (state.signMode) { - SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> { // This is managed by the LoginViewEvents } - SignMode.SignIn -> { + SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { LoginMode.Unknown -> error("Developer error") @@ -272,7 +257,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) } } - } + SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive } private fun onRegistrationStageNotSupported() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 8b89aeda2a..c2bd02b817 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -31,6 +31,7 @@ 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.riotx.R +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.extensions.toReducedUrl @@ -73,16 +74,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { private fun setupAutoFill(state: LoginViewState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } - SignMode.SignIn -> { + SignMode.SignIn, + SignMode.SignInWithMatrixId -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) } - } + }.exhaustive } } @@ -116,35 +118,44 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } private fun setupUi(state: LoginViewState) { - val resId = when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_to - SignMode.SignIn -> R.string.login_connect_to - } - loginFieldTil.hint = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_username_hint - SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_username_hint + SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint }) - when (state.serverType) { - ServerType.MatrixOrg -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_matrix_org_text) + // Handle direct signin first + if (state.signMode == SignMode.SignInWithMatrixId) { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_signin_matrix_id_title) + loginNotice.text = getString(R.string.login_signin_matrix_id_notice) + } else { + val resId = when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to + SignMode.SignInWithMatrixId -> R.string.login_connect_to } - ServerType.Modular -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - loginTitle.text = getString(resId, "Modular") - loginNotice.text = getString(R.string.login_server_modular_text) - } - ServerType.Other -> { - loginServerIcon.isVisible = false - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_other_text) + + when (state.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + loginTitle.text = getString(resId, "Modular") + loginNotice.text = getString(R.string.login_server_modular_text) + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_other_text) + } } } } @@ -153,9 +164,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn loginSubmit.text = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_submit - SignMode.SignIn -> R.string.login_signin + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn, + SignMode.SignInWithMatrixId -> R.string.login_signin }) } @@ -178,7 +190,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { @OnClick(R.id.forgetPasswordButton) fun forgetPasswordClicked() { - loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 3ee1cd6d64..5203e60b26 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -217,7 +217,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra TextInputFormFragmentMode.SetEmail -> { if (throwable.is401()) { // This is normal use case, we go to the mail waiting screen - loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendEmailSuccess(loginViewModel.currentThreePid ?: ""))) } else { loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } @@ -225,7 +225,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra TextInputFormFragmentMode.SetMsisdn -> { if (throwable.is401()) { // This is normal use case, we go to the enter code screen - loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: ""))) } else { loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt deleted file mode 100644 index 79c6409a3f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.riotx.features.login - -import im.vector.riotx.core.platform.VectorSharedAction - -// Supported navigation actions for LoginActivity -sealed class LoginNavigation : VectorSharedAction { - object OpenServerSelection : LoginNavigation() - object OnServerSelectionDone : LoginNavigation() - object OnLoginFlowRetrieved : LoginNavigation() - object OnSignModeSelected : LoginNavigation() - object OnForgetPasswordClicked : LoginNavigation() - object OnResetPasswordSendThreePidDone : LoginNavigation() - object OnResetPasswordMailConfirmationSuccess : LoginNavigation() - object OnResetPasswordMailConfirmationSuccessDone : LoginNavigation() - - data class OnSendEmailSuccess(val email: String) : LoginNavigation() - data class OnSendMsisdnSuccess(val msisdn: String) : LoginNavigation() - - data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() -} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index d3a86ef769..d90cfc77e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -149,7 +149,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment() resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) } is Success -> { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone) + Unit } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt index cace48b7f2..f340fb8f9f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt @@ -64,7 +64,7 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor() : Abstrac .show() } is Success -> { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess) + Unit } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt index 4faeef1269..fd3c5e6377 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -29,7 +29,7 @@ class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFra @OnClick(R.id.resetPasswordSuccessSubmit) fun submit() { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 9050ea2688..0e234d3da8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -95,10 +95,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Request login flow here loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) } else { - loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnServerSelectionDone)) } } + @OnClick(R.id.loginServerIKnowMyIdSubmit) + fun loginWithMatrixId() { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignInWithMatrixId)) + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetHomeServerType) } @@ -108,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } 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 898ee97656..92dcfcc8aa 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 @@ -126,7 +126,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 9f084299b7..f09053c883 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -78,7 +78,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 53de8c2c43..c860d02fec 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -29,7 +29,7 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { @OnClick(R.id.loginSplashSubmit) fun getStarted() { - loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OpenServerSelection)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt index 25747df3d4..c7c2ee6273 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -23,10 +23,26 @@ import im.vector.riotx.core.platform.VectorViewEvents /** * Transient events for Login */ -sealed class LoginViewEvents: VectorViewEvents { +sealed class LoginViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : LoginViewEvents() data class Failure(val throwable: Throwable) : LoginViewEvents() data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents() object OutdatedHomeserver : LoginViewEvents() + + // Navigation event + + object OpenServerSelection : LoginViewEvents() + object OnServerSelectionDone : LoginViewEvents() + object OnLoginFlowRetrieved : LoginViewEvents() + object OnSignModeSelected : LoginViewEvents() + object OnForgetPasswordClicked : LoginViewEvents() + object OnResetPasswordSendThreePidDone : LoginViewEvents() + object OnResetPasswordMailConfirmationSuccess : LoginViewEvents() + object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents() + + data class OnSendEmailSuccess(val email: String) : LoginViewEvents() + data class OnSendMsisdnSuccess(val msisdn: String) : LoginViewEvents() + + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 80b04fe062..81dcfcea9f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login import android.content.Context +import android.net.Uri import androidx.fragment.app.FragmentActivity import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail @@ -29,19 +30,24 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService +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.login.LoginWizard import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart +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.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.signout.soft.SoftLogoutActivity @@ -51,14 +57,16 @@ import java.util.concurrent.CancellationException /** * */ -class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, - private val applicationContext: Context, - private val authenticationService: AuthenticationService, - private val activeSessionHolder: ActiveSessionHolder, - private val pushRuleTriggerListener: PushRuleTriggerListener, - private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, - private val sessionListener: SessionListener, - private val reAuthHelper: ReAuthHelper) +class LoginViewModel @AssistedInject constructor( + @Assisted initialState: LoginViewState, + private val applicationContext: Context, + private val authenticationService: AuthenticationService, + private val activeSessionHolder: ActiveSessionHolder, + private val pushRuleTriggerListener: PushRuleTriggerListener, + private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, + private val sessionListener: SessionListener, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -108,7 +116,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) - } + is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + }.exhaustive } private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { @@ -320,11 +329,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - if (action.signMode == SignMode.SignUp) { - startRegistrationFlow() - } else if (action.signMode == SignMode.SignIn) { - startAuthenticationFlow() - } + when (action.signMode) { + SignMode.SignUp -> startRegistrationFlow() + SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected) + SignMode.Unknown -> Unit + }.exhaustive } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { @@ -365,6 +375,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi resetPasswordEmail = action.email ) } + + _viewEvents.post(LoginViewEvents.OnResetPasswordSendThreePidDone) } override fun onFailure(failure: Throwable) { @@ -405,6 +417,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi resetPasswordEmail = null ) } + + _viewEvents.post(LoginViewEvents.OnResetPasswordMailConfirmationSuccess) } override fun onFailure(failure: Throwable) { @@ -421,10 +435,78 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state -> when (state.signMode) { - SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(action) - else -> error("Developer error, invalid sign mode") + SignMode.Unknown -> error("Developer error, invalid sign mode") + SignMode.SignIn -> handleLogin(action) + SignMode.SignUp -> handleRegisterWith(action) + SignMode.SignInWithMatrixId -> handleDirectLogin(action) + }.exhaustive + } + + private fun handleDirectLogin(action: LoginAction.LoginOrRegister) { + setState { + copy( + asyncLoginAction = Loading() + ) } + + authenticationService.getWellKnownData(action.username, object : MatrixCallback { + override fun onSuccess(data: WellknownResult) { + when (data) { + is WellknownResult.Prompt -> + onWellknownSuccess(action, data) + is WellknownResult.InvalidMatrixId -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)))) + } + else -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + } + }.exhaustive + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + + private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) { + val homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + authenticationService.directAuthentication( + homeServerConnectionConfig, + action.username, + action.password, + action.initialDeviceName, + object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) } private fun handleLogin(action: LoginAction.LoginOrRegister) { @@ -477,6 +559,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun startAuthenticationFlow() { // Ensure Wizard is ready loginWizard + + _viewEvents.post(LoginViewEvents.OnSignModeSelected) } private fun onFlowResponse(flowResult: FlowResult) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index f9b0b98f29..cf3b39ebb0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -173,7 +173,7 @@ class LoginWebFragment @Inject constructor( override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) - loginSharedActionViewModel.post(LoginNavigation.OnWebLoginError(errorCode, description, failingUrl)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnWebLoginError(errorCode, description, failingUrl))) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt index b793a0fe1d..ad06f1f4a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -21,5 +21,7 @@ enum class SignMode { // Account creation SignUp, // Login - SignIn + SignIn, + // Login directly with matrix Id + SignInWithMatrixId } 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 2e2814cb78..ac725eb850 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 @@ -41,6 +41,7 @@ import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity +import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity @@ -163,6 +164,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } + override fun openInviteUsersToRoom(context: Context, roomId: String) { + val intent = InviteUsersToRoomActivity.getIntent(context, roomId) + context.startActivity(intent) + } + override fun openRoomsFiltering(context: Context) { val intent = FilteredRoomsActivity.newIntent(context) context.startActivity(intent) 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 bf99643912..cc8e7cac34 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 @@ -46,6 +46,8 @@ interface Navigator { fun openCreateDirectRoom(context: Context) + fun openInviteUsersToRoom(context: Context, roomId: String) + fun openRoomDirectory(context: Context, initialFilter: String = "") fun openRoomsFiltering(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index ae8e7f23fc..6fc396b264 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -348,12 +348,19 @@ class NotificationDrawerManager @Inject constructor(private val context: Context globalLastMessageTimestamp = lastMessageTimestamp } + val tickerText = if (roomEventGroupInfo.isDirect) { + stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + } else { + stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + } + val notification = notificationUtils.buildMessagesListNotification( style, roomEventGroupInfo, largeBitmap, lastMessageTimestamp, - myUserDisplayName) + myUserDisplayName, + tickerText) // is there an id for this room? notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 50fb5b70de..178235ab5f 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -381,7 +381,8 @@ class NotificationUtils @Inject constructor(private val context: Context, roomInfo: RoomEventGroupInfo, largeIcon: Bitmap?, lastMessageTimestamp: Long, - senderDisplayNameForReplyCompat: String?): Notification { + senderDisplayNameForReplyCompat: String?, + tickerText: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) @@ -478,6 +479,7 @@ class NotificationUtils @Inject constructor(private val context: Context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) setDeleteIntent(pendingIntent) } + .setTicker(tickerText) .build() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt index e6e54d6771..8a08cbae8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.roomprofile.members import android.os.Bundle +import android.view.MenuItem import android.view.View import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel @@ -43,6 +44,18 @@ class RoomMemberListFragment @Inject constructor( override fun getLayoutResId() = R.layout.fragment_room_setting_generic + override fun getMenuRes() = R.menu.menu_room_member_list + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_room_member_list_add_member -> { + navigator.openInviteUsersToRoom(requireContext(), roomProfileArgs.roomId) + return true + } + } + return super.onOptionsItemSelected(item) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this 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 cd5e53b7c3..d0369e7707 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 @@ -30,7 +30,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback -import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod @@ -48,6 +47,7 @@ import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.launch +import timber.log.Timber import java.util.concurrent.TimeUnit data class DevicesViewState( @@ -65,6 +65,7 @@ data class DeviceFullInfo( val deviceInfo: DeviceInfo, val cryptoDeviceInfo: CryptoDeviceInfo? ) + class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, private val session: Session @@ -215,8 +216,13 @@ class DevicesViewModel @AssistedInject constructor( private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state -> viewModelScope.launch { if (state.hasAccountCrossSigning) { - awaitCallback { - tryThis { session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) } + try { + awaitCallback { + session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) + } + } catch (failure: Throwable) { + Timber.e("Failed to manually cross sign device ${action.cryptoDeviceInfo.deviceId} : ${failure.localizedMessage}") + _viewEvents.post(DevicesViewEvents.Failure(failure)) } } else { // legacy diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt index d3288c5b2e..13b90f26e8 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt @@ -30,7 +30,7 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginMode -import im.vector.riotx.features.login.LoginNavigation +import im.vector.riotx.features.login.LoginViewEvents import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject @@ -94,7 +94,7 @@ class SoftLogoutFragment @Inject constructor( } override fun signinFallbackSubmit() { - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected)) } override fun clearData() { @@ -124,7 +124,7 @@ class SoftLogoutFragment @Inject constructor( } override fun forgetPasswordClicked() { - loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) } override fun revealPasswordClicked() { diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt index 1c38e6f723..9d11387fe8 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt @@ -1,22 +1,20 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect +package im.vector.riotx.features.userdirectory import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Fail @@ -41,7 +39,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter) : EpoxyController() { - private var state: CreateDirectRoomViewState? = null + private var state: UserDirectoryViewState? = null var callback: Callback? = null @@ -49,7 +47,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, requestModelBuild() } - fun setData(state: CreateDirectRoomViewState) { + fun setData(state: UserDirectoryViewState) { this.state = state requestModelBuild() } @@ -110,7 +108,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, continue } val isSelected = selectedUsers.contains(user.userId) - createDirectRoomUserItem { + userDirectoryUserItem { id(user.userId) selected(isSelected) matrixItem(user.toMatrixItem()) diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt similarity index 92% rename from vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt index a0e20b45f5..7a1ad49b8c 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.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,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController @@ -49,7 +49,7 @@ class KnownUsersController @Inject constructor(private val session: Session, requestModelBuild() } - fun setData(state: CreateDirectRoomViewState) { + fun setData(state: UserDirectoryViewState) { this.isFiltering = !state.filterKnownUsersValue.isEmpty() val newSelection = state.selectedUsers.map { it.userId } this.users = state.knownUsers @@ -65,7 +65,7 @@ class KnownUsersController @Inject constructor(private val session: Session, EmptyItem_().id(currentPosition) } else { val isSelected = selectedUsers.contains(item.userId) - CreateDirectRoomUserItem_() + UserDirectoryUserItem_() .id(item.userId) .selected(isSelected) .matrixItem(item.toMatrixItem()) @@ -84,13 +84,13 @@ class KnownUsersController @Inject constructor(private val session: Session, } else { var lastFirstLetter: String? = null for (model in models) { - if (model is CreateDirectRoomUserItem) { + if (model is UserDirectoryUserItem) { if (model.matrixItem.id == session.myUserId) continue val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName() val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter - CreateDirectRoomLetterHeaderItem_() + UserDirectoryLetterHeaderItem_() .id(currentFirstLetter) .letter(currentFirstLetter) .addIf(showLetter, this) diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt similarity index 60% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt index 24b5394e5c..78482e0b54 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt @@ -1,29 +1,29 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect +package im.vector.riotx.features.userdirectory import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ScrollView +import androidx.core.view.forEach import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args import com.airbnb.mvrx.withState import com.google.android.material.chip.Chip import com.jakewharton.rxbinding3.widget.textChanges @@ -35,30 +35,36 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter -import kotlinx.android.synthetic.main.fragment_create_direct_room.* +import kotlinx.android.synthetic.main.fragment_known_users.* import javax.inject.Inject -class CreateDirectRoomKnownUsersFragment @Inject constructor( +class KnownUsersFragment @Inject constructor( + val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory, private val knownUsersController: KnownUsersController, private val dimensionConverter: DimensionConverter ) : VectorBaseFragment(), KnownUsersController.Callback { - override fun getLayoutResId() = R.layout.fragment_create_direct_room + private val args: KnownUsersFragmentArgs by args() - override fun getMenuRes() = R.menu.vector_create_direct_room + override fun getLayoutResId() = R.layout.fragment_known_users - private val viewModel: CreateDirectRoomViewModel by activityViewModel() - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + override fun getMenuRes() = args.menuResId + + private val viewModel: UserDirectoryViewModel by activityViewModel() + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) - vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar) + sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + + knownUsersTitle.text = args.title + + vectorBaseActivity.setSupportActionBar(knownUsersToolbar) setupRecyclerView() setupFilterView() setupAddByMatrixIdView() setupCloseView() - viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { + viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) { renderSelectedUsers(it) } } @@ -71,27 +77,22 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( override fun onPrepareOptionsMenu(menu: Menu) { withState(viewModel) { - val createMenuItem = menu.findItem(R.id.action_create_direct_room) val showMenuItem = it.selectedUsers.isNotEmpty() - createMenuItem.setVisible(showMenuItem) + menu.forEach { menuItem -> + menuItem.isVisible = showMenuItem + } } super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_create_direct_room -> { - viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) - true - } - else -> - super.onOptionsItemSelected(item) - } + override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { + sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.selectedUsers)) + return@withState true } private fun setupAddByMatrixIdView() { addByMatrixId.setOnClickListener { - sharedActionViewModel.post(CreateDirectRoomSharedAction.OpenUsersDirectory) + sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory) } } @@ -102,26 +103,26 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( } private fun setupFilterView() { - createDirectRoomFilter + knownUsersFilter .textChanges() - .startWith(createDirectRoomFilter.text) + .startWith(knownUsersFilter.text) .subscribe { text -> val filterValue = text.trim() val action = if (filterValue.isBlank()) { - CreateDirectRoomAction.ClearFilterKnownUsers + UserDirectoryAction.ClearFilterKnownUsers } else { - CreateDirectRoomAction.FilterKnownUsers(filterValue.toString()) + UserDirectoryAction.FilterKnownUsers(filterValue.toString()) } viewModel.handle(action) } .disposeOnDestroyView() - createDirectRoomFilter.setupAsSearch() - createDirectRoomFilter.requestFocus() + knownUsersFilter.setupAsSearch() + knownUsersFilter.requestFocus() } private fun setupCloseView() { - createDirectRoomClose.setOnClickListener { + knownUsersClose.setOnClickListener { requireActivity().finish() } } @@ -157,12 +158,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( chip.isCloseIconVisible = true chipGroup.addView(chip) chip.setOnCloseIconClickListener { - viewModel.handle(CreateDirectRoomAction.RemoveSelectedUser(user)) + viewModel.handle(UserDirectoryAction.RemoveSelectedUser(user)) } } override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(CreateDirectRoomAction.SelectUser(user)) + viewModel.handle(UserDirectoryAction.SelectUser(user)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragmentArgs.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragmentArgs.kt new file mode 100644 index 0000000000..9e87633608 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragmentArgs.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.userdirectory + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class KnownUsersFragmentArgs( + val title: String, + val menuResId: Int, + val excludedUserIds: Set? = null +) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt new file mode 100644 index 0000000000..1df3c02736 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.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.userdirectory + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class UserDirectoryAction : VectorViewModelAction { + data class FilterKnownUsers(val value: String) : UserDirectoryAction() + data class SearchDirectoryUsers(val value: String) : UserDirectoryAction() + object ClearFilterKnownUsers : UserDirectoryAction() + data class SelectUser(val user: User) : UserDirectoryAction() + data class RemoveSelectedUser(val user: User) : UserDirectoryAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt similarity index 64% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt index ecfe054767..28aa2d433b 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.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,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import android.os.Bundle import android.view.View @@ -29,22 +29,22 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.platform.VectorBaseFragment -import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* +import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.recyclerView +import kotlinx.android.synthetic.main.fragment_user_directory.* import javax.inject.Inject -class CreateDirectRoomDirectoryUsersFragment @Inject constructor( +class UserDirectoryFragment @Inject constructor( private val directRoomController: DirectoryUsersController ) : VectorBaseFragment(), DirectoryUsersController.Callback { - override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users + override fun getLayoutResId() = R.layout.fragment_user_directory + private val viewModel: UserDirectoryViewModel by activityViewModel() - private val viewModel: CreateDirectRoomViewModel by activityViewModel() - - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) + sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) setupRecyclerView() setupSearchByMatrixIdView() setupCloseView() @@ -62,19 +62,19 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( } private fun setupSearchByMatrixIdView() { - createDirectRoomSearchById.setupAsSearch(searchIconRes = 0) - createDirectRoomSearchById + userDirectorySearchById.setupAsSearch(searchIconRes = 0) + userDirectorySearchById .textChanges() .subscribe { - viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) + viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString())) } .disposeOnDestroyView() - createDirectRoomSearchById.showKeyboard(andRequestFocus = true) + userDirectorySearchById.showKeyboard(andRequestFocus = true) } private fun setupCloseView() { - createDirectRoomClose.setOnClickListener { - sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) + userDirectoryClose.setOnClickListener { + sharedActionViewModel.post(UserDirectorySharedAction.GoBack) } } @@ -84,12 +84,12 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(CreateDirectRoomAction.SelectUser(user)) - sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) + viewModel.handle(UserDirectoryAction.SelectUser(user)) + sharedActionViewModel.post(UserDirectorySharedAction.GoBack) } override fun retryDirectoryUsersRequest() { - val currentSearch = createDirectRoomSearchById.text.toString() - viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(currentSearch)) + val currentSearch = userDirectorySearchById.text.toString() + viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryLetterHeaderItem.kt similarity index 70% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryLetterHeaderItem.kt index e512337c64..e7e9183ada 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryLetterHeaderItem.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,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -23,8 +23,8 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header) -abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_user_directory_letter_header) +abstract class UserDirectoryLetterHeaderItem : VectorEpoxyModel() { @EpoxyAttribute var letter: String = "" @@ -33,6 +33,6 @@ abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel(R.id.createDirectRoomLetterView) + val letterView by bind(R.id.userDirectoryLetterView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt new file mode 100644 index 0000000000..7d1987aa4b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.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.userdirectory + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorSharedAction + +sealed class UserDirectorySharedAction : VectorSharedAction { + object OpenUsersDirectory : UserDirectorySharedAction() + object Close : UserDirectorySharedAction() + object GoBack : UserDirectorySharedAction() + data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set) : UserDirectorySharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedActionViewModel.kt similarity index 76% rename from vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedActionViewModel.kt index 625208b682..e7081ea969 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedActionViewModel.kt @@ -1,5 +1,5 @@ /* - * 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package im.vector.riotx.features.login +package im.vector.riotx.features.userdirectory import im.vector.riotx.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryUserItem.kt similarity index 63% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryUserItem.kt index f2f517fd6e..7ea0709ce8 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryUserItem.kt @@ -1,22 +1,20 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * 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. + * 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.createdirect +package im.vector.riotx.features.userdirectory import android.view.View import android.widget.ImageView @@ -31,8 +29,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_create_direct_room_user) -abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_known_user) +abstract class UserDirectoryUserItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem @@ -66,9 +64,9 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel(R.id.createDirectRoomUserID) - val nameView by bind(R.id.createDirectRoomUserName) - val avatarImageView by bind(R.id.createDirectRoomUserAvatar) - val avatarCheckedImageView by bind(R.id.createDirectRoomUserAvatarChecked) + val userIdView by bind(R.id.knownUserID) + val nameView by bind(R.id.knownUserName) + val avatarImageView by bind(R.id.knownUserAvatar) + val avatarCheckedImageView by bind(R.id.knownUserAvatarChecked) } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewEvents.kt similarity index 60% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewEvents.kt index 91c21378d2..435fce8b16 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewEvents.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,9 +14,11 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory -import im.vector.riotx.core.platform.VectorSharedActionViewModel -import javax.inject.Inject +import im.vector.riotx.core.platform.VectorViewEvents -class CreateDirectRoomSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +/** + * Transient events for invite users to room screen + */ +sealed class UserDirectoryViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt new file mode 100644 index 0000000000..3111a86bf7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt @@ -0,0 +1,134 @@ +/* + * 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.userdirectory + +import androidx.fragment.app.FragmentActivity +import arrow.core.Option +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay +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.util.toMatrixItem +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.toggle +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.createdirect.CreateDirectRoomActivity +import im.vector.riotx.features.invite.InviteUsersToRoomActivity +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +private typealias KnowUsersFilter = String +private typealias DirectoryUsersSearch = String + +class UserDirectoryViewModel @AssistedInject constructor(@Assisted + initialState: UserDirectoryViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel + } + + private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) + private val directoryUsersSearch = BehaviorRelay.create() + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? { + return when (viewModelContext) { + is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state) + is ActivityViewModelContext -> { + when (viewModelContext.activity()) { + is CreateDirectRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state) + is InviteUsersToRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state) + else -> error("Wrong activity or fragment") + } + } + else -> error("Wrong activity or fragment") + } + } + } + + init { + observeKnownUsers() + observeDirectoryUsers() + } + + override fun handle(action: UserDirectoryAction) { + when (action) { + is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) + is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) + is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) + is UserDirectoryAction.SelectUser -> handleSelectUser(action) + is UserDirectoryAction.RemoveSelectedUser -> handleRemoveSelectedUser(action) + } + } + + private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemoveSelectedUser) = withState { state -> + val selectedUsers = state.selectedUsers.minus(action.user) + setState { copy(selectedUsers = selectedUsers) } + } + + private fun handleSelectUser(action: UserDirectoryAction.SelectUser) = withState { state -> + // Reset the filter asap + directoryUsersSearch.accept("") + val selectedUsers = state.selectedUsers.toggle(action.user) + setState { copy(selectedUsers = selectedUsers) } + } + + private fun observeDirectoryUsers() = withState { state -> + directoryUsersSearch + .debounce(300, TimeUnit.MILLISECONDS) + .switchMapSingle { search -> + val stream = if (search.isBlank()) { + Single.just(emptyList()) + } else { + session.rx() + .searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet()) + .map { users -> + users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } + } + } + stream.toAsync { + copy(directoryUsers = it, directorySearchTerm = search) + } + } + .subscribe() + .disposeOnClear() + } + + private fun observeKnownUsers() = withState { state -> + knownUsersFilter + .throttleLast(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + session.rx().livePagedUsers(it.orNull(), state.excludedUserIds) + } + .execute { async -> + copy( + knownUsers = async, + filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt new file mode 100644 index 0000000000..52f92a9994 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.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.riotx.features.userdirectory + +import androidx.paging.PagedList +import arrow.core.Option +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.user.model.User + +data class UserDirectoryViewState( + val excludedUserIds: Set? = null, + val knownUsers: Async> = Uninitialized, + val directoryUsers: Async> = Uninitialized, + val selectedUsers: Set = emptySet(), + val createAndInviteState: Async = Uninitialized, + val directorySearchTerm: String = "", + val filterKnownUsersValue: Option = Option.empty() +) : MvRxState { + + constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds) +} diff --git a/vector/src/main/res/drawable/ic_invite_users.xml b/vector/src/main/res/drawable/ic_invite_users.xml new file mode 100644 index 0000000000..64e5a3788d --- /dev/null +++ b/vector/src/main/res/drawable/ic_invite_users.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/vector/src/main/res/layout/fragment_known_users.xml b/vector/src/main/res/layout/fragment_known_users.xml new file mode 100644 index 0000000000..915d27bdf7 --- /dev/null +++ b/vector/src/main/res/layout/fragment_known_users.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index c97b32bd21..ba74ce26f8 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -16,7 +16,9 @@ style="@style/LoginFormScrollView" tools:ignore="MissingConstraints"> - + + + + + diff --git a/vector/src/main/res/layout/fragment_user_directory.xml b/vector/src/main/res/layout/fragment_user_directory.xml new file mode 100644 index 0000000000..e10f5bcaa9 --- /dev/null +++ b/vector/src/main/res/layout/fragment_user_directory.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_known_user.xml b/vector/src/main/res/layout/item_known_user.xml new file mode 100644 index 0000000000..e90b2c6256 --- /dev/null +++ b/vector/src/main/res/layout/item_known_user.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_user_directory_letter_header.xml b/vector/src/main/res/layout/item_user_directory_letter_header.xml new file mode 100644 index 0000000000..0cb2faf9bc --- /dev/null +++ b/vector/src/main/res/layout/item_user_directory_letter_header.xml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_room_member_list.xml b/vector/src/main/res/menu/menu_room_member_list.xml new file mode 100644 index 0000000000..ef452de70f --- /dev/null +++ b/vector/src/main/res/menu/menu_room_member_list.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_invite_users_to_room.xml b/vector/src/main/res/menu/vector_invite_users_to_room.xml new file mode 100755 index 0000000000..2e799b5c03 --- /dev/null +++ b/vector/src/main/res/menu/vector_invite_users_to_room.xml @@ -0,0 +1,10 @@ + + + + + + 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? Er du sikker på, at du ønsker at starte en ny chat med %s? Er du sikker på, at du ønsker at starte et opkald? - Er du sikker på, at du ønsker at du ønsker at starte et videoopkald? + Er du sikker på, at du ønsker at starte et videoopkald\? Inviter Liste over grupper 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 @@ Sprache Video Das Gespräch kann nicht gestartet werden, bitte später erneut versuchen - Aufgrund fehlender Berechtigungen stehen eventuell einige Funktionen vielleicht nicht zur Verfügung… - Dir fehlt das Recht ein Konferenzgespräch in diesem Raum zu starten + Aufgrund fehlender Berechtigungen stehen einige Funktionen eventuell nicht zur Verfügung… + Ihnen fehlt die Berechtigung, ein Konferenzgespräch in diesem Raum zu starten Kann Gespräch nicht starten Sitzungsinformationen Konferenzgespräche werden in verschlüsselten Räumen nicht unterstützt @@ -84,7 +84,7 @@ Lokales Adressbuch Nur Matrix-Kontakte Keine Konversationen - Du hast Riot nicht erlaubt auf deine lokalen Kontakte zuzugreifen + Sie haben Riot nicht erlaubt, auf lokale Kontakte zuzugreifen Keine Ergebnisse @@ -101,10 +101,10 @@ Absturzberichte übermitteln Screenshot übermitteln Problem melden - Bitte beschreibe das Problem. Was hast du genau gemacht? Was sollte passieren? Was passierte tatsächlich? + Bitte beschreiben Sie das Problem. Was haben Sie genau gemacht\? Was sollte passieren\? Was passierte tatsächlich\? Problembeschreibung - 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: - Du scheinst dein Telefon frustriert zu schütteln. Möchtest du das Fenster zum Senden eines Fehlerberichts öffnen? + 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: + Sie scheinen Ihr Telefon frustriert zu schütteln. Möchten Sie das Fenster zum Senden eines Fehlerberichts öffnen\? Der Fehlerbericht wurde erfolgreich übermittelt Der Fehlerbericht konnte nicht übermittelt werden (%s) Fortschritt (%s%%) @@ -124,7 +124,7 @@ Neuen Chat starten Sprachanruf starten - Video-Anruf starten + Videoanruf starten Dateien senden Foto oder Video aufnehmen @@ -136,7 +136,7 @@ Überspringen Rücksetz-E-Mail senden Zur Anmeldemaske zurückkehren - E-Mail oder Nutzername + E-Mail oder Benutzername Passwort Neues Passwort Benutzername @@ -147,11 +147,11 @@ Passwort wiederholen Neues Passwort bestätigen Benutzername und/oder Passwort falsch - Nutzernamen dürfen nur Buchstaben, Nummern, Punkte, Binde- und Unterstriche enthalten + Benutzernamen dürfen nur Buchstaben, Nummern, Punkte, Binde- und Unterstriche enthalten Passwort zu kurz (min. 6 Zeichen) Passwort fehlt - Dies sieht nicht nach einer gültigen E-Mail-Adresse aus - Dies sieht nicht nach einer gültigen Telefonnummer aus + Dies scheint keine gültige E-Mail-Adresse zu sein + Dies scheint keine gültige Telefonnummer zu sein Diese E-Mail-Adresse wird bereits verwendet. E-Mail-Adresse fehlt Telefonnummer fehlt @@ -247,13 +247,13 @@ Information 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. Riot benötigt die Berechtigung, auf deine Kamera zugreifen zu können, um Bilder aufzunehmen und Video-Anrufe durchzuführen. - - -Bitte erlaube den Zugriff im nächsten Dialog, um den Anruf zu durchzuführen. + " +\n +\nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf zu durchzuführen." Riot benötigt die Berechtigung, auf dein Mikrofon zugreifen zu können, um (Sprach-)Anrufe tätigen zu können. - - -Bitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen. + " +\n +\nBitte erlaube den Zugriff im nächsten Dialog, um den Anruf durchzuführen." 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. @@ -633,7 +633,7 @@ Achtung: Diese Datei wird vielleicht gelöscht, wenn die App deinstalliert wird. Nur zu verifizierten Sitzungen verschlüsseln Von dieser Sitzung aus keine verschlüsselten Nachrichten an nicht-verifizierte Sitzungen senden. - NICHT verifiziert + Nicht verifiziert Verifiziert Auf der Blockierliste @@ -646,8 +646,8 @@ Achtung: Diese Datei wird vielleicht gelöscht, wenn die App deinstalliert wird. Zulassen Sitzung verifizieren - 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: - 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. + Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des/der anderen Nutzer!n und bestätige: + Falls sie nicht übereinstimmen, wurde die Kommunikation vielleicht kompromittiert. Ich bestätige, dass die Schlüssel übereinstimmen 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ü.Communities Keine Gruppen - Bist du sicher, dass du einen neuen Chat mit %s starten möchtest? - Bist du sicher, dass du einen Sprachanruf starten möchtest? - Bist du sicher, dass du einen Videoanruf starten möchtest? + Sind Sie sicher, dass Sie einen neuen Chat mit %s starten möchten\? + Sind Sie sicher, dass Sie einen Sprachanruf starten möchten\? + Sind Sie sicher, dass Sie einen Videoanruf starten möchten\? Gruppenliste @@ -828,7 +828,7 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.Flair - Schüttele um einen Fehler zu melden + Schüttele, um einen Fehler zu melden Aktionen Mitglieder auflisten @@ -902,9 +902,7 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.Sende einen Sticker Sende Sticker - Du hast aktuell keine Stickerpackete aktiviert. - -Willst du welche hinzufügen? + Sie haben aktuell keine Stickerpakete aktiviert. Möchten Sie welche hinzufügen\? Deaktiviere Account Deaktivere meinen Account @@ -952,7 +950,7 @@ Willst du welche hinzufügen? Bitte gib dein Passwort ein. - Wenn möglich, beschreibe bitte in Englisch. + Wenn möglich, schreiben Sie bitte auf Englisch. Sende verschlüsselte Antwort… Sende unverschlüsselte Antwort… Zeige Medien vor dem Senden @@ -982,7 +980,7 @@ Willst du welche hinzufügen? Zeige alle Nachrichten dieses Benutzers? Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen. - Nicht berechtigt diese Aktion durchzuführen. + Nicht berechtigt, diese Aktion durchzuführen. 1s %ds @@ -1045,7 +1043,7 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.Entschuldige, ein Fehler trat auf Version %s - Status.im-Thema + Status.im-Design Bitte eine Passphrase erstellen um exportierte Schlüssel zu verschlüsseln. Du musst dieselbe Passphrase eingeben um die Schlüssel importieren zu können. Erzeuge Passphrase @@ -1212,7 +1210,7 @@ Um sicherzustellen, dass du nichts verpasst, lass deine Updates einfach aktivier Schlüsselsicherung ist nicht abgeschlossen. Bitte warten… Überspringen - Erledigt + Fertig Erweiterte Benachrichtigungseinstellungen Angepasste Einstellungen. @@ -1246,7 +1244,7 @@ Um sicherzustellen, dass du nichts verpasst, lass deine Updates einfach aktivier Wiederherstellungsschlüssel Unerwarteter Fehler Sicherung gestartet - Bist du sicher? + Sind Sie sicher\? Wiederherstellungsschlüssel eingeben Nachrichtenwiederherstellung @@ -1284,26 +1282,26 @@ Dieser Fehler ist außerhalb von Riot passiert. Es gibt kein Google-Konto auf de Deine Verschlüsselungsschlüssel werden nun im Hintergrund auf deinem Heimserver gesichert. Die initiale Sicherung kann mehrere Minuten dauern. - Du kannst den Zugang zu deinen Nachrichten verlieren, wenn du dich abmeldest oder das Gerät verlierst. + Sie verlieren möglicherweise den Zugang zu Ihren Nachrichten, wenn Sie sich abmelden oder das Gerät verlieren. - Rufe Backupversion ab… - Nutze deine Wiederherstellungspassphrase um deinen sicheren Chatverlauf zu entschlüsseln - nutze deinen Wiederherstellungsschlüssel - Wen du deine Wiederherstellungspassphrase nicht weißt, kannst du %s. + Rufe Backup-Version ab… + Nutzen Sie Ihre Wiederherstellungspassphrase, um Ihren verschlüsselten Chatverlauf lesen zu können + nutzen Sie Ihren Wiederherstellungsschlüssel + Wenn Sie Ihre Wiederherstellungspassphrase nicht wissen, können Sie %s. - Nutze deinen Wiederherstellungsschlüssel um deinen verschlüsselten Chatverlauf zu entschlüsseln - Wiederherstellungsschlüssel verloren\? Du kannst einen neuen in den Einstellungen einrichten. - Sicherung konnte mit dieser Passphrase nicht entschlüsselt werden. Bitte stelle sicher, dass du die korrekte Wiederherstellungspassphrase eingegeben hast. - Netzwerkfehler: Bitte überprüfe deine Verbindung und versuche es erneut. + Nutzen Sie Ihren Wiederherstellungsschlüssel, um Ihren verschlüsselten Chatverlauf lesen zu können + Haben Sie Ihren Wiederherstellungsschlüssel verloren\? Sie können einen neuen in den Einstellungen einrichten. + Sicherung konnte mit dieser Passphrase nicht entschlüsselt werden. Bitte stellen Sie sicher, dass Sie die korrekte Wiederherstellungspassphrase eingegeben haben. + Netzwerkfehler: Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut. - Bitte gib deinen Wiederherstellungsschlüssel ein - Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stelle sicher, dass du den korrekten Wiederherstellungsschlüssel eingegeben hast. + Bitte geben Sie Ihren Wiederherstellungsschlüssel ein + Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stellen Sie sicher, dass Sie den korrekten Wiederherstellungsschlüssel eingegeben haben. Sicherung wiederhergestellt %s ! %1$d Sitzungsschlüssel wurde(n) wiederhergestellt und %2$d vorher unbekannte(r) Schlüssel wurde(n) hinzugefügt Backup mit %d Schlüssel wiederhergestellt. - Backup mit %d Schlüssel wiederhergestellt. + Backup mit %d Schlüsseln wiederhergestellt. %d neuer Schlüssel wurde dieser Sitzung hinzugefügt. @@ -1330,20 +1328,20 @@ Dieser Fehler ist außerhalb von Riot passiert. Es gibt kein Google-Konto auf de Um die Schlüsselsicherung für diese Sitzung zu verwenden, stelle sie jetzt mit deiner Passphrase oder deinem Wiederherstellungsschlüssel wieder her. 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. - Beim Ausloggen gehen ihre verschlüsselten Nachrichten verloren - Schlüssel-Sicherung wird durchgeführt. Wenn sie sich jetzt ausloggen, dann gehen ihre verschlüsselten Nachrichten verloren. - Schlüssel-Sicherung sollte bei all deinen Sitzungen aktiviert sein um einen Verlust deiner verschlüsselten Nachrichten zu verhindern. + Beim Ausloggen gehen Ihre verschlüsselten Nachrichten verloren + Schlüssel-Sicherung wird durchgeführt. Wenn Sie sich jetzt ausloggen, dann gehen Ihre verschlüsselten Nachrichten verloren. + Schlüssel-Sicherung sollte bei all Ihren Sitzungen aktiviert sein, um einen Verlust Ihrer verschlüsselten Nachrichten zu verhindern. Ich möchte meine verschlüsselten Nachrichten nicht Sichere Schlüssel… Schlüssel-Sicherung verwenden - Bist du sicher\? + Sind Sie sicher\? Sicherung - Alle verschlüsselten Nachrichten gehen verloren wenn du dich ausloggst ohne diese vorher zu sichern. + Alle verschlüsselten Nachrichten gehen verloren, wenn Sie sich ausloggen, ohne diese vorher zu sichern. Bleiben Abbrechen - Bist du sicher, dass du dich ausloggen möchtest\? + Sind Sie sicher, dass Sie sich ausloggen möchten\? Wiederherstellung verschlüsselter Nachrichten Bitte gib einen Benutzernamen ein. @@ -1398,7 +1396,7 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine Importiere Schlüssel… Ignorieren - Mit single-sign-on anmelden + Mit Single-Sign-On anmelden Diese URL ist nicht erreichbar, bitte prüfen 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 Schicke Nachricht mit Eingabetaste @@ -1660,7 +1658,7 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine Kein Integrationsserver konfiguriert. Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen - Versuche es mit %s + Versuchen Sie es mit %s Nicht erneut fragen Richte eine E-Mail für die Kontowiederherstellung ein. Optional, kannst du später einrichten, dass Personen dich über diese Adresse finden. @@ -1708,9 +1706,9 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine Konnte keine Verbindung zum Heimserver herstellen Latn - Bitte frage die Administration deines Heimservers (%1$s) um einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren. + 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. +\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. Dies ist keine Adresse eines Matrixservers Kann Home-Server nicht bei dieser URL erreichen. Bitte überprüfen Wir nutzen %s als Assistenten wenn dein Home-Server keinen anbietet (Deine IP-Adresse wird während des Anrufs geteilt) @@ -2315,7 +2313,7 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine Dies könnte einige Sekunden dauern, gedulde dich bitte. Wiederherstellung einrichten. Dein Wiederherstellungsschlüssel - Du bist fertig! + Geschafft! Bewahre es sicher auf Abschließen @@ -2354,4 +2352,96 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine Kopier es in deinen persönlichen Cloud-Speicher Verschlüsselung ist nicht aktiviert + Dies kann nicht von einem mobilen Gerät erfolgen + + Wenn Räume verbessert werden + Verschlüsselung aktiviert + Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt. Erfahre mehr & verifiziere Benutzer in deren Profil. + Die Verschlüsselung in diesem Raum wird nicht unterstützt + + Warte auf %s… + + %s setzen + Fehlerbehebung + %s hat den Raum erstellt und konfiguriert. + + Fast geschafft! Zeigt das andere Gerät das gleiche Schild an\? + Fast geschafft! Warte auf Bestätigung… + Verschlüsselte Nachrichten in 1:1 Chats + Nachricht… + + Verifiziere dich & andere, um eure Chats zu schützen + + Geben Sie zum Fortfahren Ihren %s ein + Datei benutzen + + Dies ist kein gültiger Wiederherstellungsschlüssel + Bitte gib deinen Wiederherstellungsschlüssel ein + + Verschlüsselte Nachrichten und Verifizierungen mit einer %s absichern und entsperren. + 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. + Das setzen eines Wiederherstellungspassworts ermöglicht das sichern & entsperren von verschlüsselten Nachrichten und Verifizierungen. + + + Verschlüsselungsupgrade verfügbar + %s eingeben + Wiederherstellungspasswort + Überprüfe Wiederherstellungsschlüssel + Überprüfe Sicherungsstatus (%s) + Erzeuge Kurvenschlüssel + Generiere SSSS Schlüssel aus dem Passwort + Generiere SSSS Schlüssel aus dem Passwort (%s) + Generiere SSSS Schlüssel aus dem Wiederherstellungsschlüssel + Speichere Schlüsselbackup Schlüssel in SSSS + %1$s (%2$s) + + Gib dein Passwort für das Schlüsselbackup ein, um fortzufahren. + nutze deinen Schlüsselbackup Wiederherstellungsschlüssel + Wenn du dein Schlüsselbackup Passwort nicht weißt, kannst du %s. + Schlüsselbackup Wiederherstellungsschlüssel + + Verhindere Screenshots innerhalb der Anwendung + Das Aktivieren dieser Einstellung setzt das FLAG_SECURE in allen Aktivitäten. Starte die Anwendung neu, damit die Änderung wirksam wird. + + Datei wurde der Galerie hinzugefügt + Datei konnte nicht zur Galerie hinzugefügt werden + Neues Benutzerpasswort festlegen… + + 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 + Riot Web +\nRiot Desktop + Riot iOS +\nRiot X für Android + oder einen anderen cross-signing fähigen Matrix Client + Nutze die neueste Version von Riot auf deinen anderen Geräten: + Erzwingt das Verferfen der aktuell ausgehende Gruppensitzung in einem verschlüsseltem Raum + Wird nur in verschlüsselten Räumen unterstützt + Benutze dein %1$s oder deinen %2$s um fortzufahren. + Wiederherstellungsschlüssel verwenden + Wähle deinen Wiederherstellungsschüssel, gib ihn ein oder füge ihn aus der Zwischenablage ein + Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden. Bitte stelle sicher, dass du den korrekten Wiederherstellungsschlüssel eingegeben hast. + Konnte nicht auf gesicherten Speicher zugreifen + + Unverschlüsselt + Verschlüsselt von einem unbekannten Gerät + Überprüfe, wo du eingeloggt bist + Verifiziere alle deine Sitzungen, um sicher zu stellen, dass dein Konto & deine Nachrichten sicher sind + Bestätige neue Anmeldung für dein Konto: %1$s + + Verifiziere manuell mit einem Text + Verifiziere Anmeldung + Verifiziere interaktiv mit Emojis + Bestätige deine Identität, indem du diesen Login von einer deiner anderen Sitzungen verifizierst, um Zugriff auf deine verschlüsselten Nachrichten zu erhalten. + Als vertraut markieren + + Bitte wähle einen Benutzernamen. + Bitte wähle ein Passwort. + Überprüfe diesen Link genau + Dieser Link %1$s bringt dich zu einer anderen Seite: %2$s. +\n +\nWillst du wirklich fortfahren\? + + Konnte Direktnachricht nicht erzeugen. Prüfe die Nutzer, die du einladen willst und versuche es erneut. 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 @@ Malhela haŭto Nigra haŭto - Sinkroniganta - Atentanta pri eventoj + Spegulante… + Atentante eventojn Laŭtaj sciigoj Silentaj sciigoj Mesaĝoj - Babilejo + Ĉambro EO Agordoj - Cimraporto - Komunumaj detaloj + Erar-raporto + Detaloj pri komunumo Sendi glumarkon Permesiloj de eksteraj liverantoj - Ŝarganta… + Enlegante… Bone Nuligi @@ -33,7 +33,7 @@ Forigi Citi Elŝuti - Diskonigi + Kunhavigi Paroli Forviŝi Poste @@ -43,40 +43,41 @@ Vidi malĉifritan fonton Forigi Renomi - Membro-Detaloj + Detaloj pri ano Historia Raporti enhavon - Aktiva alvoko - Cimraporto - Daŭranta telekonferenco.\nAliĝi %1$s aŭ %2$s. - voĉe - videe - Ne eblas ekigi la alvokon, bonvolu provi poste - Ne eblas ekigi alvokon - Pro mankantaj permesoj, kelkaj ebloj eble mankos… + Aktiva voko + Raporti eraron + Daŭranta grupa voko. +\nAliĝi kiel %1$s aŭ %2$s. + Voĉe + Vide + Ne eblas ekigi la vokon, bonvolu provi poste + Ne eblas ekigi vokon + Pro mankantaj permesoj, kelkaj funkcioj eble mankos… Pro mankantaj permesoj, tiu ago ne eblas. - Aparata informo + Informoj pri salutaĵo Tamen sendi Inviti - Malinterrete + Eksterrete Eliri Agoj - Telefono - videotelefono + Voĉvoko + Vidvoko Universala serĉo - Tamen alvoki + Tamen voki Rapida respondo Malfermi Fermi Sendi ĉifritan respondon… Sendi respondon (neĉifritan)… Homoj - Babilejoj + Ĉambroj Komunumoj - Filtri nomojn de babilejoj + Filtri nomojn de ĉambroj Filtri homojn Filtri nomojn de babilejoj Homoj @@ -93,9 +94,422 @@ Babilejoj Salti al unua nelegita mesaĝo. - Ŝlosilo-Sekukopio - Usi Ŝlosilo-Sekukopion - Kontroli aparaton + Savkopiado de ŝlosiloj + Uzi savkopiadon de ŝlosiloj + Kontroli salutaĵon Kontroli aparaton + Latn + + Haŭto de Status.im + + Savkopio de ŝlosiloj ne finiĝis; bonvolu atendi… + Vi perdos viajn ĉifritajn mesaĝojn se vi nun adiaŭos + Savkopio de ŝlosiloj progresas. Se vi nun adiaŭos, vi perdos aliron al viaj ĉifritaj mesaĝoj. + Sekura savkopiado de ŝlosiloj devus esti aktiva en ĉiuj viaj salutaĵoj por eviti perdon de aliro al viaj ĉifritaj mesaĝoj. + Mi ne volas miajn ĉifritajn mesaĝojn + Savkopiante ŝlosilojn… + Uzi savkopiadon de ŝolsiloj + Ĉu vi certas\? + Savkopii + Vi perdos aliron al viaj ĉifritaj mesaĝoj malse vi savkopios viajn ŝlosilojn antaŭ adiaŭo. + + Resti + Neniu + Senvalidigi + Malkonekti + Vi bezonas permeson inviti por komenci grupan vokon en ĉi tiu ĉambro + Grupaj vokoj ne estas subtenataj de ĉifrataj ĉambroj + Akcepti + Preterpasi + Finite + Ĉesigi + Malatenti + Kontroli + Rifuzi + + Adiaŭi + Ĉu vi certe volas adiaŭi\? + Marki ĉion legita + Historia + Marki legita + Kopiiĝis al tondujo + Malŝalti + + Konfirmo + Averto + Eraro + + Hejmo + Elstarigitaj + Filtri elstarigitajn + Filtri nomojn de komunumoj + + Invitoj + Malalta prioritato + Sistemaj avertoj + + Interparoloj + Loka adresaro + Neniuj interparoloj + Vi ne permesis al Riot aliron al viaj lokaj kontaktoj + Neniuj rezultoj + Neniu identiga servilo estas agordita. + + Ĉambroj + Neniuj ĉambroj + Neniuj publikaj ĉambroj disponeblas + + 1 uzanto + %d uzantoj + + + Inviti + Komunumoj + Neniuj grupoj + + Sendi protokolon + Sendi protokolon pri fiasko + Sendi ekrankopion + Bonvolu priskribi la eraron. Kion vi faris\? Kion vi atendis\? Kio tamen vere okazis\? + Se tio eblas, bonvolu priskribi per la angla lingvo. + Priskribu vian problemon ĉi tie + Sukcese sendis la erar-raporton + Malsukcesis sendi la erar-raporton (%s) + Progreso (%s%%) + + Sendi al + Legite + + Aliĝi al ĉambro + Uzantonomo + Krei konton + Saluti + Adiaŭi + URL de hejmservilo + URL de identiga servilo + Serĉi + + Komenci novan babilon + Komenci voĉvokon + Komenci vidvokon + + Sendi voĉon + + Ĉu vi certe volas komenci novan babilon kun %s\? + Ĉu vi certe volas komenci novan voĉvokon\? + Ĉu vi certe volas komenci novan vidvokon\? + Voko malsukcesis pro misagordita servilo + 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. + Provu uzon de %s + Ne demandi ree + + Sendi dosierojn + Sendi glumarkon + Foti aŭ filmi + Foti + Filmi + + Vi havas neniujn ŝaltitajn glumarkarojn. +\n +\nĈu vi volas iujn aldoni nun\? + + pluiĝi per… + Pardonu, troviĝis neniu ekstera aplikaĵo por ĉi tiu ago. + + Saluti + Saluti per ununura saluto + Krei konton + Preterpasi + Sendi restarigan retleteron + Reiri al salutejo + Retpoŝtadreso aŭ uzantonomo + Pasvorto + Nova pasvorto + Uzantonomo + Retpoŝtadreso + Retpoŝtadreso (malnepra) + Telefonnumero + Telefonnumero (malnepra) + Ripetu pasvorton + Konfirmu vian novan pasvorton + Malĝusta uzantonomo kaj/aŭ pasvorto + Uzantonomoj povas enhavi nur literojn, ciferojn, punktojn, streketojn, kaj substrekojn + Pasvorto estas tro mallonga (almenaŭ 6 signoj) + Mankas pasvorto + Ĉi tio ne ŝajnas esti valida retpoŝtadreso + Ĉi tio ne ŝajnas esti valida telefonnumero + Ĉi tiu retpoŝtadreso jam estas difinita. + Mankas retpoŝtadreso + Mankas telefonnumero + Mankas retpoŝtadreso aŭ telefonnumero + Pasvortoj ne akordas + Ĉu vi forgesis pasvorton\? + Uzi proprajn agordojn pri servilo (altnivela) + Bonvolu kontroli vian retpoŝton por daŭrigi la registriĝon + 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. + Ĉi tiu hejmservilo volas certiĝi, ke vi ne estas roboto + Uzantonomo jam uziĝas + Hejmservilo: + Identiga servilo: + Mi kontrolis mian retpoŝtadreson + Por restarigi vian pasvorton, enigu la retpoŝtadreson ligitan al via konto: + Necesas enigi la retpoŝtadreson ligitan al via konto. + Necesas enigi novan pasvorton. + Retletero sendiĝis al %s. Vizitinte la enhavitan ligilon, klaku sube. + Malsukcesis kontroli retpoŝtadreson: certiĝu, ke vi klakis la ligilon en la retletero + 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. + Bonvolu tralegi kaj akcepti la politikojn de ĉi tiu hejmservilo: + + URL komenciĝu per http[s]:// + Ne povas asluti: reta eraro + Ne povas saluti + Ne povas registriĝi: reta eraro + Ne povas registriĝi + Ne povas registriĝi: fiasko pri posedo de retpoŝtadreso + Nevalida uzantonomo/pasvorto + Listo de legokonfirmoj + + Listo de grupoj + + Sendi en formato + Originala + Granda + Meza + Malgranda + + Ĉu nuligi la elŝuton\? + Ĉu nuligi la alŝuton\? + %d s + %1$dm %2$ds + + Hieraŭ + Hodiaŭ + + Nomo de ĉambro + Temo de ĉambro + + Vokoj + Pravalorigante servon + + 1 elektita + %d elektitaj + + Misformita identigilo. Ĝi devus esti retpoŝtadreso aŭ identigilo de Matrix, kiel «@lokaparto:mallokaparto» + INVITITA + ALIĜINTA + + Kialo pro raporto de ĉi tiu enhavo + Ĉ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. + Nuligi alŝuton + Nuligi elŝuton + + Serĉi + Filtri ĉambranojn + Neniuj rezultoj + ĈAMBROJ + MESAĜOJ + ELSTARIGITAJ + ĈAMBROJ + INVITOJ + Komenci babilon + Krei ĉambron + Aliĝi al ĉambro + Aliĝi al ĉambro + + 1 ĉambro + %d ĉambroj + + Ĉiuj mesaĝoj + Nur mencioj + Silentigi + Elstarigi + Forgesi + Mesaĝoj + Agordoj + Versio + Versio %s + Kiam mi estas invitita al ĉambro + Invitoj al vokoj + Mesaĝoj senditaj de roboto + + Fona spegulado + Reĝimo de fona spegulado (eksperimenta) + Optimumigita por baterio + 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. + Optimumigita por tujeco + 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. + Neniu fona spegulado + Vi sciiĝos pri envenaj mesaĝoj dum la aplikaĵo estas fone. + Malsukcesis ĝisdatigi agordojn. + + + Ruliĝi je eko de sistemo + Ŝalti fonan speguladon + Tempolimo de petoj por spegulado + Preferata intertempo de spegulado + %s +\nLa spegulado povas esti prokrastita, depende de la rimedoj (baterio) aŭ la stato de la aparato (dormeto). + Prokrasto inter ĉiu spegulado + sekundo + sekundoj + + Versio + Versio de olm + Uzokondiĉoj + Kopirajto + Privateca politiko + Vakigi kaŝmemoron + Vakigi kaŝmemoron de vidaŭdaĵoj + + Agordoj de uzanto + Sciigoj + Malatentataj uzantoj + Aliaj + Altnivelaj + Kunigoj + 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. + Ĉifroteĥnikaro + Administrado de ĉifraj ŝlasiloj + Celoj de sciigoj + Lokaj kontaktoj + Salutaĵoj + Sendi sciigojn pri tajpado + MarkDown-formatado + Formati mesaĝojn per la sintakso de MarkDown antaŭ sendo. Tio ebligas altnivelan formatadon, ekzemple uzon de steletoj por montri kursivan tekston. + Averto! + Grupa vokado ankoraŭ estas programata kaj eble ne estos dependebla. + + MarkDown ŝaltiĝis. + MarkDown malŝaltiĝis. + + Silente + Laŭte + + Ĉifrita mesaĝo + + Krei + Krei komunumon + Nomo de komunumo + Ekzemplo + Identigilo de komunumo + ekzemplo + + Hejmo + Aliĝintaj + Invititaj + Registriĝi + Saluti + Pluiĝi per ununura saluto + + Via pasvorto estas restarigita. + Vi adiaŭis ĉiujn viajn salutaĵojn kaj ne plu ricevados pasivajn sciigojn. Por reŝalti sciigojn, resalutu per ĉiu via aparato. + Reen al salutejo + + Averto + Via pasvorto ankoraŭ ne ŝanĝiĝis. +\n +\nĈu haltigi la ŝanĝan procedon\? + + Agordi retpoŝtadreson + Neĉifrita + Ĉifrita de nekonata aparato + Mane kontroli per teksto + Kontroli saluton + Interage kontroli per bildsignoj + 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. + Marki fidata + + Bonvolu elekti uzantonomon. + Bonvolu elekti pasvorton. + Bone kontrolu ĉi tiun ligilon + La ligilo %1$s kondukas vin al alia retejo: %2$s. +\n +\nĈu vi certe volas daŭrigi\? + + Ni ne povis krei vian rektan babilon. Bonvolu kontroli la invitotajn uzantojn kaj reprovi. + 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: + Ŝajnas, ke vi kolere skuas la telefonon. Ĉu vi volas malfermi la erar-raportilon\? + La aplikaĵo lastatempe fiaskis. Ĉu vi volas malfermi la fiasko-raportilon\? + Kolere skuu por raporti eraron + + Bonvolu enigi validan URL-on + La URL ne estas atingebla, bonvolu kontroli ĝin + Ĉi tio ne estas valida adreso de servilo de Matrix + Ne povas atingi hejmservilon je ĉi tiu URL, bonvolu kontroli ĝin + Via aparato uzas eksdatan sekurecan protokolon TLS, neŝirmatan kontraŭ atakoj; por via sekureco vi ne povos konektiĝi + Misformita JSON + Ne enhavis valdiajn JSON-datumojn + Tro multaj petoj sendiĝis + Ĉi tiu uzantonomo jam estas uzata + La retpoŝt-ligilo, kiu ankoraŭ ne estas klakita + + "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." + + Repeti ĉifrajn ŝlosilojn de aliaj viaj salutaĵoj. + + Peto de ŝlosilo sendiĝis. + + Peto sendiĝis. + 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. + + Envena voko + Envena vidvoko + Envena voĉvoko + Voko progresas… + Vidvoko progresas… + + Ne povas iniciati la filmilon + Foti aŭ filmi + Ne povas filmi + + Informoj + 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. + Riot bezonas permeson aliri vian filmilon por foti kaj vidvoki. + " +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por ebligi la vokon." + Riot bezonas permeson aliri vian mikrofonon por fari voĉvokojn. + " +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por ebligi la vokon." + Riot bezonsa premeson aliri viajn filmilon kaj mikrofonon por fari vidvokojn. +\n +\nBonvolu permesi aliron per la sekva ŝprucpeto, por ebligi la vokon. + 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. + 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\? + + Pardonu. Ago ne efektiviĝis, pro mankantaj permesoj + + Konservite + Ĉu konservi al elŝutujo\? + JES + NE + Daŭrigi + + Forigi + Aliĝi + Antaŭrigardi + Rifuzi + + Listigi ĉambranojn + Spegulante… + Vi estis invitita al ĉi tiu ĉambro de %s 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 @@ + + + et + EE + Latn + + Hele teema + Tume teema + Must teema + Status.im teema + + Käivitan teenuse + Sünkroniseerin… + Kuulan, kas leidub sündmusi + Lärmakad teavitused + Vaiksed teavitused + + Sõnumid + Jututuba + Seadistused + Jututoa liikme üksikasjad + Ajalooline + Veateade + Kogukonna üksikasjad + Saada kleeps + Võtmete varundus + Kasuta võtmete varundust + Verifitseeri sessioon + + Võtmete varundus pole veel valmis, oota natuke… + Krüptitud sõnum + + 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 Ei turvallinen Video. Kuva. + Pyyntö annetun käyttäjä-ID:n vahvistamiseksi + 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 + + Äänitiedosto + Tiedosto + + Varmennus lähetetty + Varmennuspyyntö + + + Varmenna istunto + Varmenna manuaalisesti + + Lue koodi toisen käyttäjän laitteesta varmentaaksenne toisenne tietoturvallisesti + Lue toisen käyttäjän koodi + Lukeminen ei onnistu + Vertailkaa emojeilla jos et ole toisen käyttäjän luona + + Varmenna vertaamalla emojeja + + Varmenna emojeilla + Jos koodin lukeminen ei onnistu, varmenna vertaamalla lyhyttä sarjaa emojeja. + + QR-koodi + + Varmenna %s + Varmennettu %s + Odotetaan käyttäjää %s… + Varmenna %s tarkistamalla teidän kummankin laitteella näkyvä koodi. +\n +\nJos mahdollista, tehkää tämä kummankin ollessa paikalla parhaan tietoturvan saavuttamiseksi. + Huoneessa olevat viesti eivät ole salattu osapuolten välisellä salauksella. + Huoneen viestit ovat salattu osapuolten välisellä salauksella. +\n +\nViestisi salataan niin, että vain sinä ja vastaanottaja pystytte avaamaan ne henkilökohtaisilla salausavaimillanne. + Tietoturva + Lisää + Huoneen asetukset + Tiedostojen lähetys + Ylläpitäjät + Moderaattorit + Käyttäjät + + Ylläpitäjä %1$s:ssä + Moderaattori %1$s:ssä + Siirry lukukuittaukseen + + RiotX ei osaa käsitellä tapahtumia joiden tyyppi on \'%1$s\' + RiotX ei osaa käsitellä viestejä joiden tyyppi on \'%1$s\' + RiotX ei osannut piirtää tapahtuman jonka tunniste on \'%1$s\' sisältöä + + Viimeaikaiset huoneet + Muut huoneet + + Lähettää annetun viestin väritettynä sateenkaaren väreillä + Ota käyttöön osapuolten välinen salaus + Salausta ei voi enää poistaa käytöstä sen jälkeen kun se on otettu käyttöön. + + Otetaanko salaus käyttöön\? + Salausta ei voi ottaa pois käytöstä sen jälkeen kun se on otettu käyttöön. Salattuja viestejä ei pysty lukemaan edes palvelin, vain ainoastaan huoneessa olijat. Salauksen käyttöönotto voi estää bottien ja siltojen toiminnan huoneessa. + Ota salaus käyttöön + + Salauksen mahdollistamiseksi varmenna %s tarkastamalla kertakäyttöinen koodi. + Tee tämä toisen käyttäjän ollessa läsnä tai käyttäkää toista viestintävälinettä tietoturvan varmistamiseksi. + + Verratkaa emojeja ja varmistakaa että ne ovat samassa järjestyksessä kummallakin. + Vertaa koodia joka näkyy toisen käyttäjän ruudulla. + 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. + Istuntosi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi ja muut käyttäjät näkevät sen luotettuna. + + + Aktiiviset istunnot + Näytä kaikki istunnot + Istuntojen hallinta + Kirjaudu ulos tästä istunnosta + + Ei salaukseen liittyvää tietoa + + Istunto on luotettu, koska olet vahvistanut sen: + 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: + + + %d käynnissä oleva istunto + %d käynnissä olevaa istuntoa + + + Varmenna tämä kirjautuminen + Muut käyttäjät eivät välttämättä luota siihen + Käytä olemassaolevaa istuntoa tämän istunnon varmentamiseksi jotta se saa oikeudet salattuihin viesteihin. + + + Varmenna + Varmennettu + Varoitus + + Ei saatu istuntoja + Istunnot + Luotettu + Ei luotettu + + "Tämä istunto on luotettu salattuun viestintään koska %1$s (%2$s) varmisti sen:" + %1$s (%2$s) kirjautui sisään uuteen istuntoon: + 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. + + + Nollaa avaimet + + QR-koodi + + Melkein valmis! Näkyykä %s:lla sama kilven kuva\? + Kyllä + Ei + + Yhteys kotipalvelimeen on poikki + + Kehittäjän työkalut + + %d ääni + %d ääntä + + + %d ääni - lopulliset tulokset + %d ääntä - lopulliset tulokset + + Luo yksinkertaisen äänestyksen + Jos et pääse käsiksi olemassaolevaan istuntoon + + Uusi sisäänkirjautuminen + + Varoitus: + Poista… + Haluatko lähettää tämän liitteen %1$s\?:lle\? + + Lähetä kuva alkuperäisessä koossa + Lähetä kuvat alkuperäisessä koossa + + + Vahvista poisto + Haluatko varmasti poistaa tämän tapahtuman\? Huomaa, että jos poistat huoneen nimen tai otsikon muutostapahtuman, se voi perua muutoksen. + Anna syy + Käyttäjä poistanut tapahtuman, syynä: %1$s + Tapahtuma moderoitu huoneen ylläpitäjän toimesta, syynä: %1$s + + Avaimet ovat jo ajan tasalla! + + RiotX Android + + Avainpyynnöt + + Päivitä + + Uusi kirjautuminen. Olitko se sinä\? + Paina tarkastellaksesi ja varmentaaksesi + En ollut + Tilillesi saatetaan olla murtauduttu + + Jos perut, et voi lukea salattuja viestejäsi tällä laitteella eivätkä muut käyttäjät luota siihen + Jos perut, et voi lukea salattuja viestejäsi uudella laitteellasi eivätkä muut käyttäjät luota siihen + Varmenna laitteesi ohjelman asetuksista. + Vahvistus peruttu + + Palautussalasana + Tilin salasana + + Aseta %s + Vahvista %s + + Anna %s jatkaaksesi. + + Pura ja salaa salatut viestit ja luottamukset asettamalla %s + Anna %s uudestaan vahvistaaksesi sen. + Älä käytä tilisi salasanaa muualla. + + + Odota hetki, kiitos. + Alustetaan palautusta. + Palautusavaimesi + Valmista! + Pidä se turvassa + Lopeta + + Käytä %1$s:tä turvaverkkona jos onnistut unohtamaan %2$s:n. + + Julkaistaan luodut identiteettiavaimet + Luodaan salausavain salasanasta + Määritetään SSSS-oletusavain + Synkronoidaan pääavain + Synkronoidaan käyttäjän avain + Synkronoidaan allekirjoitusavain + Alustetaan avainten varmuuskopiointi + + + %2$s ja %1$s asetettu. +\n +\nPidä ne tallessa. Tarvitset ne salattujen viestiesi ja tietojesi avaamiseen, jos suljet kaikki istuntosi. + + Tulosta se jos mahdollista ja säilytä tuloste turvallisessa paikassa + Tallenna se muistitikulle tai varmuuskopiolevylle talteen + Kopioi se henkilökohtaiseen pilvitallennustilaasi + + Tätä ei pysty tekemään kännykällä + + Mukautettu + Mukautettu (%1$d) %2$s:ssä + + Syy poistoon + + Viestin avain 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 Vous n’aurez aucune adresse principale spécifiée pour ce salon. Le chiffrement est activé sur ce salon. Le chiffrement est désactivé sur ce salon. - Activer le chiffrement -(attention : ne peut pas être désactivé ensuite !) + Activer le chiffrement +\n(attention : ne peut pas être désactivé ensuite !) %s a essayé de charger un point précis dans l’historique du salon mais ne l’a pas trouvé. @@ -1560,7 +1560,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Merci, la suggestion a bien été envoyée Échec d’envoi de la suggestion (%s) - Afficher les évènements cachés dans l’historique + Afficher les évènements cachés dans les discussions RiotX − Client Matrix nouvelle génération Un client pour Matrix plus rapide et plus léger utilisant les derniers frameworks Android @@ -1600,7 +1600,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Nom ou identifiant (#exemple:matrix.org) - Activer le balayement pour répondre dans l’historique + Activer le balayement pour répondre dans les discussions Lien copié dans le presse-papiers @@ -2102,7 +2102,7 @@ Si vous n’avez pas configuré de nouvelle méthode de récupération, un attaq Envoie le message fourni coloré comme un arc-en-ciel Envoie la réaction fournie colorée comme un arc-en-ciel - Historique + Discussions Éditeur de messages 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ó Kérlek válassz jelszót. Ezt a hivatkozást ellenőrizd le még egyszer 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. + 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 + 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. + %1$s hivatkozás egy másik oldalra visz: %2$s. +\n +\nFolytatod\? + 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 @@ Send inn i Hjemmetjener URL Start ny chat + Start lydsamtale + Start videosamtale + + Send lydopptak + + Er du sikker på du vil starte en samtale med %s\? + Er du sikker på du vil starte en lydsamtale\? + Er du sikker på du vil starte en videosamtale\? 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 @@ U gebruikt geen identiteitsserver Er is geen identiteitsserver geconfigureerd. Dit is vereist om uw wachtwoord opnieuw in te stellen. + 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 + + Het lijkt er op dat je probeert verbinding te maken met een andere thuisserver. Wil je uitloggen\? + + Bewerken + Beantwoorden + + Opnieuw proberen + Betreed een kamer om de applicatie te gebruiken. + Heeft je een uitnodiging gestuurd + Uitgenodigd door %s + + Je bent helemaal bij! + Je hebt geen ongelezen berichten meer + Welkom thuis! + Ongelezen berichten inhalen + Gesprekken + Je directe gesprekken zullen hier worden weergegeven + Kamers + Je kamers zullen hier worden weergegeven + + Reacties + Bevestigen + Leuk vinden + Reactie Toevoegen + Reacties Bekijken + Reacties + + Gebeurtenis verwijderd door gebruiker + Gebeurtenis gemodereerd door gesprek beheerder + Laatst bewerkt door %1$s op %2$s + + + Niet correcte gebeurtenis, kan niet weergeven + Maak een nieuw gesprek aan + Geen netwerk. Controleer uw internet verbinding. + Wijzigen + Wijzig netwerk + Even wachten… + Alle Gemeenschappen + + Dit gesprek kan niet worden voorvertoond + De voorvertoning van wereld-leesbare gesprekken zijn nog niet ondersteund in RiotX + + Gesprekken + Directe Berichten + + Nieuw Gesprek + AANMAKEN + Gespreksnaam + Publiek + Iedereen zal dit gesprek kunnen toetreden 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ę. Tryb programisty aktywuje ukryte funkcje i może również spowodować, że aplikacja będzie mniej stabilna. Tylko dla programistów! Wiadomości w tym pokoju nie są szyfrowane end-to-end. Przesłane pliki + Prośby o klucze + + Odblokuj historię zaszyfrowanych wiadomości + + RiotX Android + + Odśwież + + Użyj tej sesji do weryfikacji nowej, nadając jej dostęp do zaszyfrowanych wiadomości. + To nie ja 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 @@ Копировать Повторить отправку Удалить - Цитата + Цитировать Поделиться Позже Переслать - Постоянная ссылка + Копировать ссылку Просмотр исходного кода Просмотр расшифрованного исходного кода Удалить @@ -80,7 +80,7 @@ Маловажные - Диалоги + Беседы Локальные контакты Только Matrix контакты Нет диалогов @@ -116,7 +116,7 @@ Прочитан Войти в Комнату - Логин + Имя пользователя Создать аккаунт Войти Выйти @@ -145,7 +145,7 @@ Адрес электронной почты Адрес электронной почты (не обязательно) Номер телефона - Номер телефона (дополнительный) + Номер телефона (по желанию) Повтор пароля Подтвердите ваш новый пароль Неверный логин и/или пароль @@ -208,7 +208,7 @@ Оригинал Крупный Средний - Маленький + Мелкий "Отменить загрузку? @@ -290,7 +290,7 @@ 1 пользователь - Покинуть чат + Покинуть комнату Вы уверены, что хотите покинуть чат? Вы уверены, что хотите исключить %s из чата? Создать @@ -460,7 +460,7 @@ Параметры пользователя Уведомления Игнорируемые - Другое + Другой Дополнительно Криптография Отправлять уведомления на @@ -703,8 +703,8 @@ Сохранить медиа Светлая тема - Темная тема - Черная тема + Тёмная тема + Чёрная тема Звук уведомлений Показывать метки времени в 12-часовом формате @@ -1245,7 +1245,7 @@ Резервное копирование ключей не завершено, пожалуйста, подождите… Пропустить - Skip + Готово Расширенные настройки уведомлений Установка важности уведомления по событию, Настроить звук, светодиод, вибрацию @@ -1364,7 +1364,7 @@ [%1$s] Эта ошибка вне контроля Riot. Причины могут быть разными. Возможно, это будет работать, если вы повторите попытку позже, вы также можете проверить, что службы Google Play не ограничены в использовании данных в настройках системы, или что часы вашего устройства установлены правильно, или это может произойти на модифицированных прошивках. [%1$s] Эта ошибка вне контроля Riot, и, по словам Google, эта ошибка означает, что на устройстве слишком много приложений, зарегистрированных в FCM. Ошибка возникает только в тех случаях, когда существует огромное количество приложений, поэтому она не должна влиять на обычного пользователя. - Ваши зашифрованные сообщения будут потеряны если выйдете сейчас + Ваши зашифрованные сообщения будут потеряны, если выйдете сейчас Выполняется резервное копирование ключа. Если выйти сейчас, Вы потеряете доступ к Вашим зашифрованным сообщениям. Мне не нужны мои зашифрованные сообщения Выполняется резервное копирование ключей… @@ -1550,7 +1550,7 @@ Вы вышли из системы из-за недействительных или истекших учетных данных. Редактировать - Ответ + Ответить Повторить Присоединитесь к комнате, чтобы начать использовать приложение. @@ -1566,19 +1566,19 @@ Комнаты Ваши комнаты будут отображаться здесь - Реакция - Соглашаться + Отсебятины + Принять Нравиться - Добавить действие - Посмотреть реакции + Добавить отсебятину + Просмотреть отсебятины Менеджер интеграции Менеджер интеграции не настроен. - Реакции + Отсебятины - Удаленное пользователем событие + Событие удалено пользователем Мероприятие, модерируемое администратором помещения - Последний раз редактировался %1$s на %2$s + Последний раз редактировалось %1$s на %2$s Некорректное событие, не может быть отображено @@ -1591,7 +1591,7 @@ Эту комнату нельзя предварительно просмотреть Комнаты - Прямые сообщения + Личная переписка Новая комната СОЗДАТЬ @@ -1656,7 +1656,7 @@ Предварительный просмотр открытой комнаты в RiotX пока не поддерживается - Прямые сообщения + Личная переписка Ждите… Шифрование миниатюры… @@ -1839,7 +1839,7 @@ Заблокировать пользователя Все сообщения - Только упоминания + Только при упоминаниях Настройки Покинуть комнату %1$s сделал комнату доступной для всех, у кого есть ссылка. @@ -1847,4 +1847,178 @@ Подробные логи помогут разработчикам, предоставив больше информации, когда вы отправляете RageShake. Даже когда они разрешены, приложение не логирует ваши сообщения и другие приватные данные. + Закройте меню создания комнаты… + Вниз + + Контакт + Стикер + Причина отчёта о контенте + ОТЧЁТ + Заблокировать пользователя + + Все сообщения (громко) + Без звука + Отправить данное сообщение под спойлером + Спойлер + Введите ключевые слова, чтобы найти отсебятину. + + Длительный щелчок по комнате открывает опции + + + Непрочитанные сообщения + + Развяжите своё общение + Общайтесь с людьми напрямую или в группах + Начать + + Выберите сервер + Как и электронная почта, учетные записи имеют один дом, хотя вы можете общаться с кем угодно + Премиум-хостинг для организаций + Узнать больше + Другое + Пользовательские и расширенные настройки + + Продолжить + Подключиться к %1$s + Подключиться к Modular + Подключиться к пользовательскому серверу + Зарегистрироваться + Войти в систему + Продолжить с SSO + + Модульный адрес + Адрес + Премиум-хостинг для организаций + Введите адрес Modular Riot или сервера, который вы хотите использовать. + Произошла ошибка при загрузке страницы: %1$s (%2$d) + Приложение не может войти на этот сервер, так как он поддерживает следующие типы входа: %1$s. +\nВы хотите войти с помощью веб-клиента\? + Извините, этот сервер не принимает новые учётные записи. + Приложение не может создать учётную запись на сём сервере. +\n +\nЖелаете зарегистрироваться через веб-клиент\? + + Сей адрес электронной почты не связан ни с одной учетной записью. + + Сбросить пароль на %1$s + Далее + Email + Новый пароль + + Внимание! + Смена пароля приведёт к сбросу всех сквозных ключей шифрования во всех ваших сеансах, что сделает зашифрованную историю разговоров нечитаемой. Настройте резервное копирование ключей или экспортируйте ключи от комнаты из другого сеанса, прежде чем сбрасывать пароль. + Продолжить + + Данный email не связан ни с одним аккаунтом + + Проверьте свою почту + Письмо с подтверждением было отправлено на %1$s. + Нажмите на ссылку, чтобы подтвердить свой новый пароль. Как только вы перейдете по ссылке, которую он содержит, нажмите ниже. + Успешно! + Ваш пароль был сброшен. + Вы вышли из всех сеансов и больше не будете получать push-уведомления. Чтобы снова иметь возможность получать уведомления, необходимо повторно войти в систему. + Назад, чтобы войти в систему + + Предупреждение + Ваш пароль еще не изменен. +\n +\nОстановить процесс смены пароля\? + + Установить адрес электронной почты + Электронная почта + Электронная почта (по желанию) + Далее + + Установить номер телефона + Укажите номер своего телефона, ежели желаете, чтобы люди, которым вы небезразличны, смогли вас найти. + Пожалуйста, используйте международный формат. + Номер телефона + Номер телефона (по желанию) + Далее + + Подтвердить номер телефона + Мы только что отправили код на %1$s. Введите его ниже, чтобы подтвердить, что это вы. + Введите код + Отправить повторно + Далее + + Международные телефонные номера должны начинаться с \'+\' + Номер телефона кажется недействительным. Пожалуйста, проверьте его + + Зарегистрироваться в %1$s + Имя пользователя или email + Имя пользователя + Пароль + Далее + Это имя пользователя занято + Предупреждение + Ваш аккаунт еще не создан. +\n +\nОстановить процесс регистрации\? + + Выбрать matrix.org + Выбрать modular + Выбрать учреждённый сервер + Пожалуйста, пройдите проверку на каптчу + Принять условия для продолжения + + Пожалуйста, проверьте ваш email + Мы только что отправили email на %1$s. +\nПожалуйста, нажмите на содержащуюся в нём ссылку, чтобы продолжить создание аккаунта. + Домашний сервер устарел + Сей домашний сервер использует слишком старую версию для подключения. Попросите вашего администратора обновить версию на домашнем сервере. + + + Слишком много запросов было отправлено. Вы сможете повторить попытку через %1$d секунду… + Слишком много запросов было отправлено. Вы сможете повторить попытку через %1$d секунды… + Слишком много запросов было отправлено. Вы сможете повторить попытку через %1$d секунд… + + + Вы вышли из системы + Сие может быть обусловлено различными причинами: +\n +\n- Вы сменили пароль в другом сеансе. +\n +\n- Вы удалили сей сеанс из иного сеанса. +\n +\n- Администратор вашего сервера заблокировал вам доступ из соображений безопасности. + Войти снова + + Вы вышли из системы + Войти + Администратор вашего домашнего сервера (%1$s) вывел вас из вашего аккаунта %2$s (%3$s). + Войдите, чтобы восстановить ключи шифрования, хранящиеся исключительно на сём устройстве. Они нужны вам для чтения всех ваших защищённых сообщений на любом устройстве. + Войти + Пароль + Очистить личные данные + Внимание: Ваши личные данные (включая ключи шифрования) всё ещё хранятся на сём устройстве. +\n +\nОчистите его, если вы закончили использовать сие устройство или хотите войти в другую учётную запись. + Очистить все данные + + Очистить данные + Очистить все данные, хранящиеся в данный момент на сём устройстве\? +\nВойдите ещё раз, чтобы получить доступ к данным своего аккаунта и сообщениям. + Вы потеряете доступ к защищённым сообщениям, если не войдёте в систему для восстановления ключей шифрования. + Очистить данные + Текущая сессия предназначена для пользователя %1$s, а вы предоставляете учётные данные для пользователя %2$s. Такая безалаберность не поддерживается в RiotX. +\nПожалуйста, сначала очистите данные, а затем снова войдите под другим аккаунтом. + + Ваша ссылка на matrix.to неверна + Описание слишком короткое + + Посмотреть все мои сеансы + Дополнительные настройки + Режим разработчика + Режим разработчика активирует скрытые функции, а также может сделать приложение менее стабильным. Только для разработчиков! + Настройки + Текущий сеанс + Другие сеансы + + Включено шифрование + Недоверительный вход + Отправленные файлы + Интерактивная проверка по отсебятинам + Пожалуйста, выберите имя пользователя. + Пожалуйста, выберите пароль. 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 @@ Importo Jepni frazëkalimin Fshehtëzoje vetëm për sesione të verifikuar - JO i verifikuar + Jo i Verifikuar I verifikuar Në Listë të Zezë @@ -916,8 +916,8 @@ Hiqi verifikimin Hiqe nga listë e zezë - 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ë: - 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. + Ripohojeni duke krahasuar sa vijon me Rregullimet e Përdoruesit te sesioni juaj tjetër: + Nëse s’përputhen, siguria e komunikimeve tuaja mund të jetë komprometuar. URL Shërbyesi Home 1 dhomë @@ -2147,7 +2147,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Mundësi e Përzgjedhur Krijoni një pyetësor të thjeshtë - Përdorni metodë rimarrjesh + Përdorni Frazëkalim Rikthimesh ose Kyç Nëse s’hyni dot në një sesion ekzistues Hyrje e Re @@ -2182,7 +2182,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Rifreskoje - Sesion i Ri + Hyrje e re. Ju qetë\? Prekeni, që ta shqyrtoni & verifikoni Përdoreni këtë sesion që të verifikoni atë të riun tuaj, duke i akorduar hyrje te mesazhe të fshehtëzuar. Ky s’qeshë unë @@ -2204,7 +2204,7 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Verifikoni pajisjet tuaja që prej Rregullimeve. Verifikimi u Anulua - Fjalëkalim Mesazhesh + Frazëkalim Rikthimesh Kyç Mesazhesh Fjalëkalim Llogarie @@ -2278,4 +2278,73 @@ Që të garantoni se s’ju shpëton gjë, thjesht mbajeni të aktivizuar mekani Emër përdoruesi dhe/ose fjalëkalim i pasaktë. Fjalëkalimi i dhënë fillon ose mbaron me hapësirë, ju lutemi, kontrollojeni. + + Po publikohen kyçe të krijuar identiteti + Mesazh… + + Ka të gatshëm përmirësim fshehtëzimi + Verifikoni veten & të tjerët, që t’i mbani bisedat tuaja të sigurta + + Që të vazhdohet, jepni %s tuaj + Përdor Kartelë + + Jepni %s + Frazëkalim Rikthimesh + S’është kyç rimarrjesh i vlefshëm + Ju lutemi, jepni një kyç rimarrjesh + + Po kontrollohet Kyç kopjeruajtjeje + Po kontrollohet Kyç kopjeruajtjeje (%s) + Po prodhohet kyç SSSS nga frazëkalim + Po prodhohet kyç SSSS prej frazëkalimi (%s) + Po prodhohet kyç SSSS nga kyç rimarrjesh + Po depozitohet e fshehtë kopjeruajtjeje kyçesh në SSSS + %1$s (%2$s) + + Që të vazhdohet, jepni Frazëkalim Kopjeruajtje Kyçesh. + përdorni kyçin tuaj të rimarrjeve të Kopjeruajtjes së Kyçeve + S’dihet Frazëkalimi juaj i Kopjeruajtjes së Kyçeve, mundeni të %s. + Kyç rimarrjesh Kopjeruajtjesh Kyçesh + + Pengo foto ekrani të aplikacionit + Aktivizimi i këtij rregullimi shton FLAG_SECURE te krejt Veprimtaritë. Që ndryshimi të hyjë në fuqi, rinisni aplikacionin. + + Te Galeria u shtua kartelë media + S’u shtua dot kartelë media te Galeria + Caktoni një fjalëkalim të ri llogarie… + + 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 + Riot Web +\nRiot Desktop + Riot iOS +\nRiot X për Android + ose një tjetër klient Matrix i aftë për <em>cross-signing</em + Përdorni Riot-in më të ri në pajisjet tuaja të tjera: + Mbulohet vetëm për dhoma të fshehtëzuara + Përdorni %1$s tuaj ose përdorni %2$s tuaj që të vazhdohet. + Përdorni Kyçin Rimarrjesh + Përzgjidhni Kyçin tuaj të Rimarrjeve, ose jepeni dorazi duke e shtypur ose duke e ngjitur prej të papastrës tuaj + Kopjeruajtja s’u shfshehtëzua dot me këtë Kyç Rimarrjesh: ju lutemi, verifikoni se keni dhënë Kyçin e saktë të Rimarrjeve. + S’u arrit të hyhet në depozitë të sigurt + + Të pafshehtëzuara + Fshehtëzuar nga një pajisje e paverifikuar + Shqyrtojini kur të jeni i futur + Verifikoni krejt sesionet tuaj që të siguroheni se llogaria & mesazhet tuaja janë të sigurt + Verifikoni kredencialet e reja për hyrje te llogaria juaj: %1$s + + Verifikojeni Dorazi përmes Teksti + Verifikoni kredenciale hyrjeje + Verifikojeni Në Mënyrë Interaktive përmes Emoji-sh + 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. + Vërini shenjë si i Besuar + + Ju lutemi, zgjidhni një emër përdoruesi. + Ju lutemi, zgjidhni një fjalëkalim. + Kontrollojeni edhe një herë këtë lidhje + Lidhja %1$s po ju shpie te një tjetër sajt: %2$s. +\n +\nJeni i sigurt se doni të vazhdohet\? + + S’e krijuam dot DM-në tuaj. Ju lutemi, kontrolloni përdoruesit që doni të ftoni dhe riprovoni. 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 @@ + + + sv + SE + Latn + + Ljust tema + Mörkt tema + Svart tema + Status.im-tema + + Initialiserar tjänst + Synkroniserar… + Lyssnar efter händelser + Högljudda notifikationer + Tysta notifikationer + + Meddelanden + Rum + Inställningar + Medlemsdetaljer + Historisk + Buggrapport + Gemenskapsdetaljer + Skicka ett klistermärke + Säkerhetskopiering av nycklar + Använd säkerhetskopiering av nycklar + Verifiera session + + Säkerhetskopiering av nycklar är inte färdig, vänligen vänta… + Du kommer att förlora dina krypterade meddelanden om du loggar ut nu + Säkerhetskopiering av nycklar pågår. Om du loggar ut nu så kommer du att förlora åtkomst till dina krypterade meddelanden. + 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. + Jag vill inte ha mina krypterade meddelanden + Säkerhetskopierar nycklar… + Använd säkerhetskopiering av nycklar + Är du säker\? + Säkerhetskopiera + Du kommer att förlora åtkomst till dina krypterade meddelanden om du inte säkerhetskopierar dina nycklar innan du loggar ut. + + Tredjepartslicenser + + Laddar… + + OK + Avbryt + Spara + Lämna + Stanna + Skicka + Kopiera + Skicka igen + Ta bort + Citat + Ladda ner + Dela + Prata + Rensa + Senare + Vidarebefordra + Permanent länk + Visa källa + Visa avkrypterad källa + Radera + Döp om + Ingen + Återkalla + Koppla ifrån + Rapportera innehåll + Aktivt samtal + Pågående gruppsamtal. +\nAnslut som %1$s eller %2$s + Röst + Video + Kan inte starta samtalet, vänligen försök senare + På grund av saknade rättigheter, så kan vissa funktioner saknas… + På grund av saknade rättigheter så kan denna handling inte utföras. + Du behöver ha rätt att bjuda in för att starta ett gruppsamtal i detta rum + Kan inte starta samtal + Sessionsinformation + Gruppsamtal stöds inte i krypterade rum + Ring ändå + Skicka ändå + eller + Bjud in + Bortkopplad + Godkänn + Hoppa över + Färdig + Avbryt + Ignorera + Granska + Avslå + + Avsluta + Handlingar + Logga ut + Är du säker på att du vill logga ut\? + Röstsamtal + Videosamtal + Global sökning + Markera alla som skickade + Historisk + Snabbsvar + Markera som läst + Öppna + Stäng + Kopierat till klippbordet + Stäng av + + Bekräftelse + Varning + Fel + + Hem + Favoriter + Personer + Rum + Gemenskaper + + Filtrera rumsnamn + Filtrera favoriter + Filtrera personer + Filtrera rumsnamn + Filtrera gemenskapsnamn + + Inbjudningar + Låg prioritet + Systemvarningar + + Konversationer + Lokal adressbok + Användarkatalog + Bara Matrix-kontakter + Inga konversationer + Du gav inte Riot tillgång till dina lokala kontakter + Inga resultat + Ingen identitetsserver konfigurerad. + + Rum + Rumskatalog + Inga rum + Inga publika rum tillgängliga + + 1 användare + %d användare + + + Bjud in + Gemenskaper + Inga grupper + + Skicka loggar + Skicka kraschloggar + Skicka skärmdump + Rapportera bugg + Vänligen beskriv buggen. Vad gjorde du\? Vad förväntade du dig skulle hända\? Vad hände istället\? + Om möjligt, skriv vänligen beskrivningen på engelska. + Beskriv ditt problem här + 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: + Du verkar skaka din telefon i frustration. Vill du skicka en buggrapport\? + Appen kraschade senaste gången. Vill du skicka en buggrapport\? + Raseriskaka för att rapportera bugg + + Buggrapporten har skickats framgångsrikt + Sändning av buggrapporten misslyckades (%s) + Framsteg (%s%%) + + Skicka in i + Läs + + Gå med i rummet + Användarnamn + Skapa konto + Logga in + Logga ut + URL för hemserver + URL för identitetsserver + Sök + + Starta ny chatt + Starta röstsamtal + Starta videosamtal + + Skicka röst + + Är du säker på att du vill skapa en ny chatt med %s\? + Är du säker på att du vill starta ett röstsamtal\? + Är du säker på att du vill skapa ett videosamtal\? + Samtal misslyckades p.g.a. felkonfigurerad server + 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. + Försök med %s + Fråga mig inte igen + + Skicka filer + Skicka klistermärke + Ta foto eller video + Ta ett foto + Ta en video + + Du har för närvarande inga klistermärkespaket aktiva. +\n +\nLägg till några nu\? + + fortsätt med… + Tyvärr har ingen extern applikation hittats som kan fullfölja denna handling. + + Logga in + Logga in med externt konto + Skapa konto + Skicka in + Hoppa över + Skicka e-brev för återställning + Gå tillbaka till inloggningsskärmen + E-postadress eller användarnamn + Lösenord + Nytt lösenord + Användarnamn + 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. + Sätt ett telefonnummer som folk som känner dig kan använda för att hitta dig. + 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. + 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. + E-postadress + E-postadress (valfritt) + Telefonnummer + Telefonnummer (valfritt) + Repetera lösenordet + Bekräfta ditt nya lösenord + Fel användarnamn och/eller lösenord + Användarnamn får endast innehålla bokstäver, siffror, punkter, bindestreck och understreck + Lösenordet är för kort (minst 6 tecken) + Lösenord saknas + Det här ser inte ut som en giltig e-postadress + Det här ser inte ut som ett giltigt telefonnummer + Den här e-postadressen är redan definierad. + E-postadress saknas + Telefonnummer saknas + E-postadress eller telefonnummer saknas + Felaktig token + Lösenorden matchar inte + Glömt lösenordet\? + Använd anpassade server-inställningar (avancerat) + Vänligen kolla din e-post för att fortsätta med registreringen + 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. + Denna hemserver skulle vilja verifiera att du inte är en robot + Användarnamnet är upptagen + Hemserver: + Identitetsserver: + Jag har verifierat min e-postadress + För att återställa ditt lösenord, skriv in e-postadressen länkad till ditt konto: + Du måste skriva in e-postadressen länkad till ditt konto. + Du måste skriva in ett nytt lösenord. + Ett e-brev har skickats till %s. När du har följt länken i det, klicka nedan. + Misslyckades att verifiera e-postadressen: se till att du klickade på länken i e-brevet + 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. + Vänligen granska och acceptera villkoren för denna hemserver: + + URLen måste börja med http[s]:// + Kan inte logga in: Nätverksfel + Kan inte logga in + Kan inte registrera: Nätverksfel + Kan inte registrera + Kan inte registrera: fel vid med e-postägandeskap + Vänligen skriv in en giltig URL + Den här URLen kan inte nås, vänligen kolla den + Det här är inte en giltig Matrixserveradress + Kan inte nå en hemserver på den här URLen, vänligen kolla den + 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 + Mobil + + Felaktigt användarnamn/lösenord + Den åtkomsttoken du specificerade kändes inte igen + Felformaterad JSON + Innehöll inte giltig JSON + För många förfrågningar har skickats + 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 @@ 此主服务器想确认您不是机器人 一封电子邮件已发送至 %s。点击了其中的链接后,请点击下面。 电子邮箱地址验证失败:请确保您已点击邮件中的链接 - 密码已重设。 - -您已从所有设备注销并且不再接受推送通知。要重新启用通知,请重新在相应设备上登录。 + 密码已重置。 您已从所有设备注销并且不再接受推送通知。要重新启用通知,请重新在相应设备上登录。 原始 %d 秒 @@ -519,7 +517,7 @@ 这只会发生一次。 请谅解由此造成的不便。 已读标签清单 - "发送为 " + 发送为 Riot 需要访问您的通讯录,才能根据电子邮箱地址和手机号码查找其他 Matrix 用户。 请在接下来的弹出窗口中授权允许访问。 @@ -888,7 +886,7 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意 已发送密钥共享请求。 已请求 - 请在可解密此消息的设备上启动 Riot,以便其将密钥发送至当前设备。 + 请在其他可解密此消息的设备上启动 Riot,以便其将密钥发送至当前设备。 在此输入… diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ccf4dfee3d..ff821f5b95 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1089,6 +1089,8 @@ New Invitation Me ** Failed to send - please open room + %1$s: %2$s + %1$s: %2$s %3$s Search for historical @@ -2369,4 +2371,17 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming The link %1$s is taking you to another site: %2$s.\n\nAre you sure you want to continue? "We couldn't create your DM. Please check the users you want to invite and try again." - + + Add members + INVITE + Inviting users… + Invite Users + Invitation sent to %1$s + Invitations sent to %1$s and %2$s + + Invitations sent to %1$s and one more + Invitations sent to %1$s and %2$d more + + We could not invite users. Please check the users you want to invite and try again. + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 585102d46a..dd1043819d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -57,4 +57,12 @@ + Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method: + Sign in with my Matrix identifier + Sign in + Enter your identifier and your password + User identifier + This is not a valid user identifier. Expected format: \'@user:homeserver.org\' + Unable to find a valid homeserver. Please check your identifier +