Merge branch 'release/0.20.0'

This commit is contained in:
Benoit Marty 2020-05-15 15:45:26 +02:00
commit d1d79c0191
125 changed files with 4324 additions and 706 deletions

View file

@ -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) Changes in RiotX 0.19.0 (2020-05-04)
=================================================== ===================================================

View file

@ -8,7 +8,7 @@
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m org.gradle.jvmargs=-Xmx8192m
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # 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 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View file

@ -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.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Completable
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -95,6 +96,10 @@ class RxRoom(private val room: Room) {
fun liveNotificationState(): Observable<RoomNotificationState> { fun liveNotificationState(): Observable<RoomNotificationState> {
return room.getLiveRoomNotificationState().asObservable() return room.getLiveRoomNotificationState().asObservable()
} }
fun invite(userId: String, reason: String? = null): Completable = completableBuilder<Unit> {
room.invite(userId, reason, it)
}
} }
fun Room.rx(): RxRoom { fun Room.rx(): RxRoom {

View file

@ -90,8 +90,8 @@ class RxSession(private val session: Session) {
return session.getIgnoredUsersLive().asObservable() return session.getIgnoredUsersLive().asObservable()
} }
fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> { fun livePagedUsers(filter: String? = null, excludedUserIds: Set<String>? = null): Observable<PagedList<User>> {
return session.getPagedUsersLive(filter).asObservable() return session.getPagedUsersLive(filter, excludedUserIds).asObservable()
} }
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder { fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {

View file

@ -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.api.session.Session
import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestData 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 to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
*/ */
data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData, data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
val aliceKeys: List<OlmInboundGroupSessionWrapper>, val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session) { val aliceSession2: Session) {
fun cleanUp(testHelper: CommonTestHelper) { fun cleanUp(testHelper: CommonTestHelper) {

View file

@ -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.data.SessionParams
import im.vector.matrix.android.api.auth.login.LoginWizard 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.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.session.Session
import im.vector.matrix.android.api.util.Cancelable 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. * This interface defines methods to authenticate or to create an account to a matrix server.
*/ */
interface AuthenticationService { interface AuthenticationService {
/** /**
* Request the supported login flows for this homeserver. * 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 * 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, fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials, credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable callback: MatrixCallback<Session>): Cancelable
/**
* Perform a wellknown request, using the domain from the matrixId
*/
fun getWellKnownData(matrixId: String,
callback: MatrixCallback<WellknownResult>): 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<Session>): Cancelable
} }

View file

@ -24,16 +24,38 @@ import im.vector.matrix.android.internal.util.md5
* This data class hold credentials user data. * This data class hold credentials user data.
* You shouldn't have to instantiate it. * You shouldn't have to instantiate it.
* The access token should be use to authenticate user in all server requests. * 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) @JsonClass(generateAdapter = true)
data class Credentials( data class Credentials(
/**
* The fully-qualified Matrix ID that has been registered.
*/
@Json(name = "user_id") val userId: String, @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, @Json(name = "access_token") val accessToken: String,
/**
* Not documented
*/
@Json(name = "refresh_token") val refreshToken: String?, @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?, @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 { internal fun Credentials.sessionId(): String {

View file

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

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.auth.data
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass 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 * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
@ -52,7 +53,7 @@ data class WellKnown(
val identityServer: WellKnownBaseConfig? = null, val identityServer: WellKnownBaseConfig? = null,
@Json(name = "m.integrations") @Json(name = "m.integrations")
val integrations: Map<String, @JvmSuppressWildcards Any>? = null val integrations: JsonDict? = null
) { ) {
/** /**
* Returns the list of integration managers proposed * Returns the list of integration managers proposed

View file

@ -16,6 +16,6 @@
package im.vector.matrix.android.api.auth.data package im.vector.matrix.android.api.auth.data
data class WellKnownManagerConfig( data class WellKnownManagerConfig(
val apiUrl : String, val apiUrl: String,
val uiUrl: String val uiUrl: String
) )

View file

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

View file

@ -61,9 +61,10 @@ interface UserService {
/** /**
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * 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 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 * @return a Livedata of users
*/ */
fun getPagedUsersLive(filter: String? = null): LiveData<PagedList<User>> fun getPagedUsersLive(filter: String? = null, excludedUserIds: Set<String>? = null): LiveData<PagedList<User>>
/** /**
* Get list of ignored users * Get list of ignored users

View file

@ -22,6 +22,9 @@ package im.vector.matrix.android.api.session.user.model
*/ */
data class User( data class User(
val userId: String, val userId: String,
/**
* For usage in UI, consider using [getBestName]
*/
val displayName: String? = null, val displayName: String? = null,
val avatarUrl: String? = null val avatarUrl: String? = null
) { ) {

View file

@ -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.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore 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.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.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -59,14 +63,20 @@ internal abstract class AuthModule {
} }
@Binds @Binds
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore
@Binds @Binds
abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore
@Binds @Binds
abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService
@Binds @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
} }

View file

@ -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.data.isSupportedBySdk
import im.vector.matrix.android.api.auth.login.LoginWizard 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.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.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable 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.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard 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.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.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest 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.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers 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 im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -59,7 +65,10 @@ internal class DefaultAuthenticationService @Inject constructor(
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val sessionCreator: SessionCreator, 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 { ) : AuthenticationService {
private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData()
@ -148,27 +157,71 @@ internal class DefaultAuthenticationService @Inject constructor(
val authAPI = buildAuthAPI(homeServerConnectionConfig) val authAPI = buildAuthAPI(homeServerConnectionConfig)
// Ok, try to get the config.json file of a RiotWeb client // Ok, try to get the config.json file of a RiotWeb client
val riotConfig = executeRequest<RiotConfig>(null) { return runCatching {
apiCall = authAPI.getRiotConfig() executeRequest<RiotConfig>(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<Versions>(null) {
apiCall = newAuthAPI.versions()
} }
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<Versions>(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<Versions>(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 { 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<WellknownResult>): 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<Session>): Cancelable {
return directLoginTask
.configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
private suspend fun createSessionFromSso(credentials: Credentials, private suspend fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
sessionCreator.createSession(credentials, homeServerConnectionConfig) sessionCreator.createSession(credentials, homeServerConnectionConfig)

View file

@ -46,14 +46,14 @@ internal class DefaultSessionCreator @Inject constructor(
val sessionParams = SessionParams( val sessionParams = SessionParams(
credentials = credentials, credentials = credentials,
homeServerConnectionConfig = homeServerConnectionConfig.copy( homeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = credentials.wellKnown?.homeServer?.baseURL homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL
// remove trailing "/" // remove trailing "/"
?.trim { it == '/' } ?.trim { it == '/' }
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding homeserver url to $it") } ?.also { Timber.d("Overriding homeserver url to $it") }
?.let { Uri.parse(it) } ?.let { Uri.parse(it) }
?: homeServerConnectionConfig.homeServerUri, ?: homeServerConnectionConfig.homeServerUri,
identityServerUri = credentials.wellKnown?.identityServer?.baseURL identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL
// remove trailing "/" // remove trailing "/"
?.trim { it == '/' } ?.trim { it == '/' }
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }

View file

@ -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<DirectLoginTask.Params, Session> {
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<OkHttpClient>,
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<Credentials>(null) {
apiCall = authAPI.login(loginParams)
}
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
}
}

View file

@ -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<GetWellknownTask.Params, WellknownResult> {
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<OkHttpClient>,
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<WellKnown>(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<Unit>(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<Unit>(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<String?>) {
if (homeServerUrl.startsWith("https://")) {
wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length),
object : SimpleApiCallback<WellKnown>(callback) {
override fun onSuccess(info: WellKnown) {
callback.onSuccess(info.identityServer?.baseURL)
}
})
} else {
callback.onUnexpectedError(InvalidParameterException("malformed url"))
}
}
fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback<List<WellKnownManagerConfig>>) {
if (homeServerUrl.startsWith("https://")) {
wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length),
object : SimpleApiCallback<WellKnown>(callback) {
override fun onSuccess(info: WellKnown) {
callback.onSuccess(info.getIntegrationManagers())
}
})
} else {
callback.onUnexpectedError(InvalidParameterException("malformed url"))
}
}
*/
}

View file

@ -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<WellKnown>
}

View file

@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor(
} }
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> { override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDevices(userId)?.map { it.value }?.sortedBy { it.deviceId } ?: emptyList() return cryptoStore.getUserDeviceList(userId) ?: emptyList()
} }
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> { override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {

View file

@ -48,7 +48,7 @@ internal class SetDeviceVerificationAction @Inject constructor(
if (device.trustLevel != trustLevel) { if (device.trustLevel != trustLevel) {
device.trustLevel = trustLevel device.trustLevel = trustLevel
cryptoStore.storeUserDevice(userId, device) cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified)
} }
} }
} }

View file

@ -164,14 +164,6 @@ internal interface IMXCryptoStore {
*/ */
fun saveOlmAccount() 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. * Retrieve a device for a user.
* *
@ -415,7 +407,7 @@ internal interface IMXCryptoStore {
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) 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() fun clearOtherUserTrust()

View file

@ -233,29 +233,6 @@ internal class RealmCryptoStore @Inject constructor(
return olmAccount!! 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? { override fun getUserDevice(userId: String, deviceId: String): CryptoDeviceInfo? {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<DeviceInfoEntity>() it.where<DeviceInfoEntity>()
@ -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 -> doRealmTransaction(realmConfiguration) { realm ->
realm.where(DeviceInfoEntity::class.java) realm.where(DeviceInfoEntity::class.java)
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
@ -1289,7 +1266,7 @@ internal class RealmCryptoStore @Inject constructor(
deviceInfoEntity.trustLevelEntity = it deviceInfoEntity.trustLevelEntity = it
} }
} else { } else {
trustEntity.locallyVerified = locallyVerified locallyVerified?.let { trustEntity.locallyVerified = it }
trustEntity.crossSignedVerified = crossSignedVerified trustEntity.crossSignedVerified = crossSignedVerified
} }
} }
@ -1429,7 +1406,7 @@ internal class RealmCryptoStore @Inject constructor(
} else { } else {
// Just override existing, caller should check and untrust id needed // Just override existing, caller should check and untrust id needed
val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) val existing = CrossSigningInfoEntity.getOrCreate(realm, userId)
existing.crossSigningKeys.forEach { it.deleteFromRealm() } existing.crossSigningKeys.deleteAllFromRealm()
existing.crossSigningKeys.addAll( existing.crossSigningKeys.addAll(
info.crossSigningKeys.map { info.crossSigningKeys.map {
crossSigningKeysMapper.map(it) crossSigningKeysMapper.map(it)

View file

@ -45,7 +45,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// Version 1L added Cross Signing info persistence // Version 1L added Cross Signing info persistence
companion object { 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) { 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 <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm) if (oldVersion <= 4) migrateTo5(realm)
if (oldVersion <= 5) migrateTo6(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { 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")
}
}
}
} }

View file

@ -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<Unit>
}

View file

@ -26,4 +26,10 @@ internal object NetworkConstants {
// Media // Media
private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/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/" 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"
} }

View file

@ -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.GetGroupDataWorker
import im.vector.matrix.android.internal.session.group.GroupModule 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.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.profile.ProfileModule
import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker
import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.pushers.PushersModule
@ -70,6 +71,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
CacheModule::class, CacheModule::class,
CryptoModule::class, CryptoModule::class,
PushersModule::class, PushersModule::class,
OpenIdModule::class,
AccountDataModule::class, AccountDataModule::class,
ProfileModule::class, ProfileModule::class,
SessionAssistedInjectModule::class, SessionAssistedInjectModule::class,

View file

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

View file

@ -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<RequestOpenIdTokenResponse>
}

View file

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

View file

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

View file

@ -39,6 +39,8 @@ internal class DefaultInviteTask @Inject constructor(
return executeRequest(eventBus) { return executeRequest(eventBus) {
val body = InviteBody(params.userId, params.reason) val body = InviteBody(params.userId, params.reason)
apiCall = roomAPI.invite(params.roomId, body) apiCall = roomAPI.invite(params.roomId, body)
isRetryable = true
maxRetryCount = 3
} }
} }
} }

View file

@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
) )
} }
override fun getPagedUsersLive(filter: String?): LiveData<PagedList<User>> { override fun getPagedUsersLive(filter: String?, excludedUserIds: Set<String>?): LiveData<PagedList<User>> {
realmDataSourceFactory.updateQuery { realm -> realmDataSourceFactory.updateQuery { realm ->
val query = realm.where(UserEntity::class.java) val query = realm.where(UserEntity::class.java)
if (filter.isNullOrEmpty()) { if (filter.isNullOrEmpty()) {
@ -104,6 +104,11 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
.contains(UserEntityFields.USER_ID, filter) .contains(UserEntityFields.USER_ID, filter)
.endGroup() .endGroup()
} }
excludedUserIds
?.takeIf { it.isNotEmpty() }
?.let {
query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray())
}
query.sort(UserEntityFields.DISPLAY_NAME) query.sort(UserEntityFields.DISPLAY_NAME)
} }
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session.user 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.SearchUsersParams
import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse
import retrofit2.Call import retrofit2.Call
@ -30,6 +30,6 @@ internal interface SearchUserAPI {
* *
* @param searchUsersParams the search params. * @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<SearchUsersResponse> fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call<SearchUsersResponse>
} }

View file

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.user.accountdata
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
@ -34,15 +33,4 @@ interface AccountDataAPI {
*/ */
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/account_data/{type}") @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<Unit> fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call<Unit>
/**
* 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<Any, Any>): Call<Map<Any, Any>>
} }

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,15 @@
* limitations under the License. * 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 { internal fun String.isValidUrl(): Boolean {
object OpenUsersDirectory : CreateDirectRoomSharedAction() return try {
object Close : CreateDirectRoomSharedAction() URL(this)
object GoBack : CreateDirectRoomSharedAction() true
} catch (t: Throwable) {
false
}
} }

View file

@ -179,8 +179,8 @@
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string> <string name="notice_room_invite_no_invitee_with_reason">%1$s\'s Einladung. Grund: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string> <string name="notice_room_invite_with_reason">%1$s hat %2$s eingeladen. Grund: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s hat dich eingeladen. Grund: %2$s</string> <string name="notice_room_invite_you_with_reason">%1$s hat dich eingeladen. Grund: %2$s</string>
<string name="notice_room_join_with_reason">%1$s beigetreten. Grund: %2$s</string> <string name="notice_room_join_with_reason">%1$s ist dem Raum beigetreten. Grund: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s ging. Grund: %2$s</string> <string name="notice_room_leave_with_reason">%1$s hat den Raum verlassen. Grund: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s hat die Einladung abgelehnt. Grund: %2$s</string> <string name="notice_room_reject_with_reason">%1$s hat die Einladung abgelehnt. Grund: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s hat %2$s gekickt. Grund: %3$s</string> <string name="notice_room_kick_with_reason">%1$s hat %2$s gekickt. Grund: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s hat Sperre von %2$s aufgehoben. Grund: %3$s</string> <string name="notice_room_unban_with_reason">%1$s hat Sperre von %2$s aufgehoben. Grund: %3$s</string>

View file

@ -3,20 +3,210 @@
<string name="summary_user_sent_image">%1$s sendis bildon.</string> <string name="summary_user_sent_image">%1$s sendis bildon.</string>
<string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string> <string name="summary_user_sent_sticker">%1$s sendis glumarkon.</string>
<string name="notice_room_invite_no_invitee">invito de %s</string> <string name="notice_room_invite_no_invitee">Invito de %s</string>
<string name="notice_room_invite">%1$s invitis %2$s</string> <string name="notice_room_invite">%1$s invitis uzanton %2$s</string>
<string name="notice_room_invite_you">%1$s invitis vin</string> <string name="notice_room_invite_you">%1$s invitis vin</string>
<string name="notice_room_join">%1$s alvenis</string> <string name="notice_room_join">%1$s alvenis</string>
<string name="notice_room_leave">%1$s foriris</string> <string name="notice_room_leave">%1$s foriris</string>
<string name="notice_room_reject">%1$s malakceptis la inviton</string> <string name="notice_room_reject">%1$s malakceptis la inviton</string>
<string name="notice_room_kick">%1$s forpelis %2$s</string> <string name="notice_room_kick">%1$s forpelis uzanton %2$s</string>
<string name="notice_room_unban">%1$s malforbaris %2$s</string> <string name="notice_room_unban">%1$s malforbaris uzanton %2$s</string>
<string name="notice_room_ban">%1$s forbaris %2$s</string> <string name="notice_room_ban">%1$s forbaris uzanton %2$s</string>
<string name="notice_room_withdraw">%1$s malinvitis %2$s</string> <string name="notice_room_withdraw">%1$s nuligis inviton por %2$s</string>
<string name="notice_avatar_url_changed">%1$s ŝanĝis sian profilbildon</string> <string name="notice_avatar_url_changed">%1$s ŝanĝis sian profilbildon</string>
<string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
<string name="message_reply_to_prefix">Respondanta al</string> <string name="message_reply_to_prefix">Responde al</string>
<string name="summary_message">%1$s: %2$s</string>
<string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string>
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string>
<string name="notice_display_name_removed">%1$s forigis sian vidigan nomon (%2$s)</string>
<string name="notice_room_topic_changed">%1$s ŝanĝis la temon al: %2$s</string>
<string name="notice_room_name_changed">%1$s ŝanĝis nomon de la ĉambro al: %2$s</string>
<string name="notice_placed_video_call">%s vidvokis.</string>
<string name="notice_placed_voice_call">%s voĉvokis.</string>
<string name="notice_answered_call">%s respondis la vokon.</string>
<string name="notice_ended_call">%s finis la vokon.</string>
<string name="notice_made_future_room_visibility">%1$s videbligis estontan historion de ĉambro al %2$s</string>
<string name="notice_room_visibility_invited">ĉiuj ĉambranoj, ekde iliaj invitoj.</string>
<string name="notice_room_visibility_joined">ĉiuj ĉambranoj, ekde iliaj aliĝoj.</string>
<string name="notice_room_visibility_shared">ĉiuj ĉambranoj.</string>
<string name="notice_room_visibility_world_readable">ĉiu ajn.</string>
<string name="notice_room_visibility_unknown">nekonata (%s).</string>
<string name="notice_end_to_end">%1$s ŝaltis tutvojan ĉifradon (%2$s)</string>
<string name="notice_room_update">%s gradaltigis la ĉambron.</string>
<string name="notice_event_redacted">Mesaĝo foriĝis</string>
<string name="notice_event_redacted_by">Mesaĝo foriĝis de %1$s</string>
<string name="notice_event_redacted_with_reason">Mesaĝo foriĝis [kialo: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Mesaĝo foriĝis de %1$s [kialo: %2$s]</string>
<string name="notice_profile_change_redacted">%1$s ĝisdatigis sian profilon %2$s</string>
<string name="notice_room_third_party_invite">%1$s sendis aliĝan inviton al %2$s</string>
<string name="notice_room_third_party_revoked_invite">%1$s nuligis la aliĝan inviton por %2$s</string>
<string name="notice_room_third_party_registered_invite">%1$s akceptis la inviton por %2$s</string>
<string name="could_not_redact">Ne povis redakti</string>
<string name="unable_to_send_message">Ne povas sendi mesaĝon</string>
<string name="message_failed_to_upload">Malsukcesis alŝuti bildon</string>
<string name="network_error">Reta eraro</string>
<string name="matrix_error">Matrix-eraro</string>
<string name="room_error_join_failed_empty_room">Nun ne eblas re-aliĝi al malplena ĉambro</string>
<string name="encrypted_message">Ĉifrita mesaĝo</string>
<string name="medium_email">Retpoŝtadreso</string>
<string name="medium_phone_number">Telefonnumero</string>
<string name="reply_to_an_image">sendis bildon.</string>
<string name="reply_to_a_video">sendis filmon.</string>
<string name="reply_to_an_audio_file">sendis sondosieron.</string>
<string name="reply_to_a_file">sendis dosieron.</string>
<string name="room_displayname_invite_from">Invito de %s</string>
<string name="room_displayname_room_invite">Ĉambra invito</string>
<string name="room_displayname_two_members">%1$s kaj %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s kaj 1 alia</item>
<item quantity="other">%1$s kaj %2$d aliaj</item>
</plurals>
<string name="room_displayname_empty_room">Malplena ĉambro</string>
<string name="verification_emoji_dog">Hundo</string>
<string name="verification_emoji_cat">Kato</string>
<string name="verification_emoji_lion">Leono</string>
<string name="verification_emoji_horse">Ĉevalo</string>
<string name="verification_emoji_unicorn">Unukorno</string>
<string name="verification_emoji_pig">Porko</string>
<string name="verification_emoji_elephant">Elefanto</string>
<string name="verification_emoji_rabbit">Kuniklo</string>
<string name="verification_emoji_panda">Pando</string>
<string name="verification_emoji_rooster">Koko</string>
<string name="verification_emoji_penguin">Pingveno</string>
<string name="verification_emoji_turtle">Testudo</string>
<string name="verification_emoji_fish">Fiŝo</string>
<string name="verification_emoji_octopus">Polpo</string>
<string name="verification_emoji_butterfly">Papilio</string>
<string name="verification_emoji_flower">Floro</string>
<string name="verification_emoji_tree">Arbo</string>
<string name="verification_emoji_cactus">Kakto</string>
<string name="verification_emoji_mushroom">Fungo</string>
<string name="verification_emoji_globe">Globo</string>
<string name="verification_emoji_moon">Luno</string>
<string name="verification_emoji_cloud">Nubo</string>
<string name="verification_emoji_fire">Fajro</string>
<string name="verification_emoji_banana">Banano</string>
<string name="verification_emoji_apple">Pomo</string>
<string name="verification_emoji_strawberry">Frago</string>
<string name="verification_emoji_corn">Maizo</string>
<string name="verification_emoji_pizza">Pico</string>
<string name="verification_emoji_cake">Kuko</string>
<string name="verification_emoji_heart">Koro</string>
<string name="verification_emoji_smiley">Mieneto</string>
<string name="verification_emoji_robot">Roboto</string>
<string name="verification_emoji_hat">Ĉapelo</string>
<string name="verification_emoji_glasses">Okulvitroj</string>
<string name="verification_emoji_wrench">Boltilo</string>
<string name="verification_emoji_santa">Kristnaska viro</string>
<string name="verification_emoji_thumbsup">Dikfingro supren</string>
<string name="verification_emoji_umbrella">Ombrelo</string>
<string name="verification_emoji_hourglass">Sablohorloĝo</string>
<string name="verification_emoji_clock">Horloĝo</string>
<string name="verification_emoji_gift">Donaco</string>
<string name="verification_emoji_lightbulb">Lampo</string>
<string name="verification_emoji_book">Libro</string>
<string name="verification_emoji_pencil">Grifelo</string>
<string name="verification_emoji_paperclip">Paperkuntenilo</string>
<string name="verification_emoji_scissors">Tondilo</string>
<string name="verification_emoji_lock">Seruro</string>
<string name="verification_emoji_key">Ŝlosilo</string>
<string name="verification_emoji_hammer">Martelo</string>
<string name="verification_emoji_telephone">Telefono</string>
<string name="verification_emoji_flag">Flago</string>
<string name="verification_emoji_train">Vagonaro</string>
<string name="verification_emoji_bicycle">Biciklo</string>
<string name="verification_emoji_airplane">Aviadilo</string>
<string name="verification_emoji_rocket">Raketo</string>
<string name="verification_emoji_trophy">Trofeo</string>
<string name="verification_emoji_ball">Pilko</string>
<string name="verification_emoji_guitar">Gitaro</string>
<string name="verification_emoji_trumpet">Trumpeto</string>
<string name="verification_emoji_bell">Sonorilo</string>
<string name="verification_emoji_anchor">Ankro</string>
<string name="verification_emoji_headphone">Kapaŭdilo</string>
<string name="verification_emoji_folder">Dosierujo</string>
<string name="verification_emoji_pin">Pinglo</string>
<string name="initial_sync_start_importing_account">Komenca spegulado:
\nEnportante konton…</string>
<string name="initial_sync_start_importing_account_crypto">Komenca spegulado:
\nEnportante ĉifrilojn</string>
<string name="initial_sync_start_importing_account_rooms">Komenca spegulado:
\nEnportante ĉambrojn</string>
<string name="initial_sync_start_importing_account_joined_rooms">Komenca spegulado:
\nEnportante aliĝitajn ĉambrojn</string>
<string name="initial_sync_start_importing_account_invited_rooms">Komenca spegulado:
\nEnportante ĉambrojn de invitoj</string>
<string name="initial_sync_start_importing_account_left_rooms">Komenca spegulado:
\nEnportante forlasitajn ĉambrojn</string>
<string name="initial_sync_start_importing_account_groups">Komenca spegulado:
\nEnportante komunumojn</string>
<string name="initial_sync_start_importing_account_data">Komenca spegulado:
\nEnportante datumojn de konto</string>
<string name="event_status_sending_message">Sendante mesaĝon…</string>
<string name="clear_timeline_send_queue">Vakigi sendan atendovicon</string>
<string name="notice_requested_voip_conference">%1$s petis grupan vokon</string>
<string name="notice_voip_started">Grupa voko komenciĝis</string>
<string name="notice_voip_finished">Grupa voko finiĝis</string>
<string name="notice_avatar_changed_too">(ankaŭ profilbildo ŝanĝiĝis)</string>
<string name="notice_room_name_removed">%1$s forigis nomon de la ĉambro</string>
<string name="notice_room_topic_removed">%1$s forigis temon de la ĉambro</string>
<string name="notice_room_invite_no_invitee_with_reason">Invito de %1$s. Kialo: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invitis uzanton %2$s. Kialo: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invitis vin. Kialo: %2$s</string>
<string name="notice_room_join_with_reason">%1$s aliĝis al la ĉambro. Kialo: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s foriris de la ĉambro. Kialo: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rifuzis la inviton. Kialo: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s forpelis uzanton %2$s. Kialo: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s malforbaris uzanton %2$s. Kialo: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s forbaris uzanton %2$s. Kialo: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s akceptis la inviton por %2$s. Kialo: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s nuligis la inviton al %2$s. Kialo: %3$s</string>
<plurals name="notice_room_aliases_added">
<item quantity="one">%1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.</item>
<item quantity="other">%1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
</plurals>
<plurals name="notice_room_aliases_removed">
<item quantity="one">%1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.</item>
<item quantity="other">%1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.</item>
</plurals>
<string name="notice_room_aliases_added_and_removed">%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.</string>
<string name="notice_room_canonical_alias_set">%1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.</string>
<string name="notice_room_canonical_alias_unset">%1$s forigis la ĉefadreson de ĉi tiu ĉambro.</string>
<string name="notice_room_guest_access_can_join">%1$s permesis al gastoj aliĝi al la ĉambro.</string>
<string name="notice_room_guest_access_forbidden">%1$s malpermesis al gastoj aliĝi al la ĉambro.</string>
<string name="notice_end_to_end_ok">%1$s ŝaltis tutvojan ĉifradon.</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).</string>
<string name="key_verification_request_fallback_message">%s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj.</string>
</resources> </resources>

View file

@ -0,0 +1,188 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="summary_user_sent_image">%1$s saatis pildi.</string>
<string name="summary_user_sent_sticker">%1$s saatis kleepsu.</string>
<string name="notice_room_invite_no_invitee">Kasutaja %s kutse</string>
<string name="notice_room_invite">%1$s kutsus kasutajat %2$s</string>
<string name="notice_room_invite_you">%1$s kutsus sind</string>
<string name="notice_room_join">%1$s liitus jututoaga</string>
<string name="notice_room_leave">%1$s lahkus jututoast</string>
<string name="notice_room_reject">%1$s lükkas tagasi kutse</string>
<string name="notice_room_kick">%1$s müksas kasutajat %2$s</string>
<string name="notice_room_withdraw">%1$s võttis tagasi kutse kasutajale %2$s</string>
<string name="notice_avatar_url_changed">%1$s muutis oma avatari</string>
<string name="notice_display_name_set">%1$s määras oma kuvatavaks nimeks %2$s</string>
<string name="notice_display_name_changed_from">%1$s muutis senise kuvatava nime %2$s uueks nimeks %3$s</string>
<string name="notice_display_name_removed">%1$s eemaldas oma kuvatava nime (%2$s)</string>
<string name="notice_room_topic_changed">%1$s muutis uueks teemaks %2$s</string>
<string name="notice_room_name_changed">%1$s muutis jututoa uueks nimeks %2$s</string>
<string name="notice_placed_video_call">%s alustas videokõnet.</string>
<string name="notice_placed_voice_call">%s alustas häälkõnet.</string>
<string name="notice_answered_call">%s vastas kõnele.</string>
<string name="notice_ended_call">%s lõpetas kõne.</string>
<string name="notice_made_future_room_visibility">%1$s seadistas, et tulevane jututoa ajalugu on nähtav kasutajale %2$s</string>
<string name="notice_room_visibility_invited">kõikidele jututoa liikmetele alates kutsumise hetkest.</string>
<string name="notice_room_visibility_joined">kõikidele jututoa liikmetele alates liitumise hetkest.</string>
<string name="notice_room_visibility_shared">kõikidele jututoa liikmetele.</string>
<string name="notice_room_visibility_world_readable">kõikidele.</string>
<string name="notice_room_visibility_unknown">teadmata (%s).</string>
<string name="notice_end_to_end">%1$s lülitas sisse läbiva krüptimise (%2$s)</string>
<string name="notice_room_update">%s uuendas seda jututuba.</string>
<string name="notice_requested_voip_conference">%1$s saatis VoIP konverentsi kutse</string>
<string name="notice_voip_started">VoIP-konverents algas</string>
<string name="notice_voip_finished">VoIP-konverents lõppes</string>
<string name="notice_avatar_changed_too">(samuti sai avatar muudetud)</string>
<string name="notice_room_name_removed">%1$s eemaldas jututoa nime</string>
<string name="notice_room_topic_removed">%1$s eemaldas jututoa teema</string>
<string name="notice_event_redacted">Sõnum on eemaldatud</string>
<string name="notice_event_redacted_by">Sõnum on eemaldatud %1$s poolt</string>
<string name="notice_event_redacted_with_reason">Sõnum on eemaldatud [põhjus: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Sõnum on eemaldatud %1$s poolt [põhjus: %2$s]</string>
<string name="notice_profile_change_redacted">%1$s uuendas oma profiili %2$s</string>
<string name="notice_room_third_party_invite">%1$s saatis jututoaga liitumiseks kutse kasutajale %2$s</string>
<string name="notice_room_third_party_revoked_invite">%1$s võttis tagasi jututoaga liitumise kutse kasutajalt %2$s</string>
<string name="notice_room_third_party_registered_invite">%1$s võttis vastu kutse %2$s nimel</string>
<string name="notice_crypto_unable_to_decrypt">** Ei õnnestu dekrüptida: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string>
<string name="message_reply_to_prefix">Vastuseks kasutajale</string>
<string name="could_not_redact">Ei saanud muuta sõnumit</string>
<string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string>
<string name="message_failed_to_upload">Faili üles laadimine ei õnnestunud</string>
<string name="network_error">Võrguühenduse viga</string>
<string name="matrix_error">Matrix\'i viga</string>
<string name="room_error_join_failed_empty_room">Hetkel ei ole võimalik uuesti liituda tühja jututoaga.</string>
<string name="encrypted_message">Krüptitud sõnum</string>
<string name="medium_email">E-posti aadress</string>
<string name="medium_phone_number">Telefoninumber</string>
<string name="reply_to_an_image">saatis pildi.</string>
<string name="reply_to_a_video">saatis video.</string>
<string name="reply_to_an_audio_file">saatis helifaili.</string>
<string name="reply_to_a_file">saatis faili.</string>
<string name="room_displayname_invite_from">Kutse kasutajalt %s</string>
<string name="room_displayname_room_invite">Kutse jututuppa</string>
<string name="room_displayname_two_members">%1$s ja %2$s</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s ja üks muu</item>
<item quantity="other">%1$s ja %2$d muud</item>
</plurals>
<string name="room_displayname_empty_room">Tühi jututuba</string>
<string name="verification_emoji_dog">Koer</string>
<string name="verification_emoji_cat">Kass</string>
<string name="verification_emoji_lion">Lõvi</string>
<string name="verification_emoji_horse">Hobune</string>
<string name="verification_emoji_unicorn">Ükssarvik</string>
<string name="verification_emoji_pig">Siga</string>
<string name="verification_emoji_elephant">Elevant</string>
<string name="verification_emoji_rabbit">Jänes</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kukk</string>
<string name="verification_emoji_penguin">Pingviin</string>
<string name="verification_emoji_turtle">Kilpkonn</string>
<string name="verification_emoji_fish">Kala</string>
<string name="verification_emoji_octopus">Kaheksajalg</string>
<string name="verification_emoji_butterfly">Liblikas</string>
<string name="verification_emoji_flower">Lill</string>
<string name="verification_emoji_tree">Puu</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Seen</string>
<string name="verification_emoji_globe">Maakera</string>
<string name="verification_emoji_moon">Kuu</string>
<string name="verification_emoji_cloud">Pilv</string>
<string name="verification_emoji_fire">Tuli</string>
<string name="verification_emoji_banana">Banaan</string>
<string name="verification_emoji_apple">Õun</string>
<string name="verification_emoji_strawberry">Maasikas</string>
<string name="verification_emoji_corn">Mais</string>
<string name="verification_emoji_pizza">Pitsa</string>
<string name="verification_emoji_cake">Kook</string>
<string name="verification_emoji_heart">Süda</string>
<string name="verification_emoji_smiley">Smaili</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Müts</string>
<string name="verification_emoji_glasses">Prillid</string>
<string name="verification_emoji_wrench">Mutrivõti</string>
<string name="verification_emoji_santa">Jõuluvana</string>
<string name="verification_emoji_thumbsup">Pöidlad püsti</string>
<string name="verification_emoji_umbrella">Vihmavari</string>
<string name="verification_emoji_hourglass">Liivakell</string>
<string name="verification_emoji_clock">Kell</string>
<string name="verification_emoji_gift">Kingitus</string>
<string name="verification_emoji_lightbulb">Lambipirn</string>
<string name="verification_emoji_book">Raamat</string>
<string name="verification_emoji_pencil">Pliiats</string>
<string name="verification_emoji_paperclip">Kirjaklamber</string>
<string name="verification_emoji_scissors">Käärid</string>
<string name="verification_emoji_lock">Lukk</string>
<string name="verification_emoji_key">Võti</string>
<string name="verification_emoji_hammer">Haamer</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Lipp</string>
<string name="verification_emoji_train">Rong</string>
<string name="verification_emoji_bicycle">Jalgratas</string>
<string name="verification_emoji_airplane">Lennuk</string>
<string name="verification_emoji_rocket">Rakett</string>
<string name="verification_emoji_trophy">Auhind</string>
<string name="verification_emoji_ball">Pall</string>
<string name="verification_emoji_guitar">Kitarr</string>
<string name="verification_emoji_trumpet">Trompet</string>
<string name="verification_emoji_bell">Kelluke</string>
<string name="verification_emoji_anchor">Ankur</string>
<string name="verification_emoji_headphone">Kõrvaklapid</string>
<string name="verification_emoji_folder">Kaust</string>
<string name="verification_emoji_pin">Knopka</string>
<string name="initial_sync_start_importing_account">Alglaadimine:
\nImpordin kontot…</string>
<string name="initial_sync_start_importing_account_crypto">Alglaadimine:
\nImpordin krüptoseadistusi</string>
<string name="initial_sync_start_importing_account_rooms">Alglaadimine:
\nImpordin jututubasid</string>
<string name="initial_sync_start_importing_account_joined_rooms">Alglaadimine:
\nImpordin liitutud jututubasid</string>
<string name="initial_sync_start_importing_account_invited_rooms">Alglaadimine:
\nImpordin kutsutud jututubasid</string>
<string name="initial_sync_start_importing_account_left_rooms">Alglaadimine:
\nImpordin lahkutud jututubasid</string>
<string name="initial_sync_start_importing_account_groups">Alglaadimine:
\nImpordin kogukondi</string>
<string name="initial_sync_start_importing_account_data">Alglaadimine:
\nImpordin kontoandmeid</string>
<string name="event_status_sending_message">Saadan sõnumit…</string>
<string name="clear_timeline_send_queue">Tühjenda saatmisjärjekord</string>
<string name="notice_room_invite_no_invitee_with_reason">Kasutaja %1$s kutse. Põhjus: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s kutsus kasutajat %2$s. Põhjus: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s kutsus sind. Põhjus: %2$s</string>
<string name="notice_room_join_with_reason">%1$s liitus jututoaga. Põhjus: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s lahkus jututoast. Põhjus: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s lükkas kutse tagasi. Põhjus: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s müksas välja kasutaja %2$s. Põhjus: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s saatis kasutajale %2$s kutse jututoaga liitumiseks. Põhjus: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s tühistas kasutajale %2$s saadetud kutse jututoaga liitumiseks. Põhjus: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s võttis vastu kutse %2$s jututoaga liitumiseks. Põhjus: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s võttis tagasi kasutajale %2$s saadetud kutse. Põhjus: %3$s</string>
<string name="notice_end_to_end_ok">%1$s lülitas sisse läbiva krüptimise.</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s lülitas sisse läbiva krüptimise (tundmatu algoritm %2$s).</string>
</resources>

View file

@ -209,4 +209,6 @@
<string name="notice_end_to_end_ok">%1$s laittoi päälle osapuolten välisen salauksen.</string> <string name="notice_end_to_end_ok">%1$s laittoi päälle osapuolten välisen salauksen.</string>
<string name="notice_end_to_end_unknown_algorithm">%1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s).</string> <string name="notice_end_to_end_unknown_algorithm">%1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s).</string>
<string name="key_verification_request_fallback_message">%s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa.</string>
</resources> </resources>

View file

@ -4,8 +4,8 @@
<string name="summary_user_sent_image">%1$s dërgoi një figurë.</string> <string name="summary_user_sent_image">%1$s dërgoi një figurë.</string>
<string name="notice_room_invite">%1$s ftoi %2$s</string> <string name="notice_room_invite">%1$s ftoi %2$s</string>
<string name="notice_room_invite_you">%1$s ju ftoi</string> <string name="notice_room_invite_you">%1$s ju ftoi</string>
<string name="notice_room_join">%1$s u bë pjesë</string> <string name="notice_room_join">%1$s hyri në dhomë</string>
<string name="notice_room_leave">%1$s iku</string> <string name="notice_room_leave">%1$s doli nga dhoma</string>
<string name="notice_room_reject">%1$s hodhi tej ftesën</string> <string name="notice_room_reject">%1$s hodhi tej ftesën</string>
<string name="notice_room_kick">%1$s përzuri %2$s</string> <string name="notice_room_kick">%1$s përzuri %2$s</string>
<string name="notice_room_ban">%1$s dëboi %2$s</string> <string name="notice_room_ban">%1$s dëboi %2$s</string>
@ -172,8 +172,8 @@
<string name="notice_room_invite_no_invitee_with_reason">Ftesë e %1$s. Arsye: %2$s</string> <string name="notice_room_invite_no_invitee_with_reason">Ftesë e %1$s. Arsye: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s ftoi %2$s. Arsye: %3$s</string> <string name="notice_room_invite_with_reason">%1$s ftoi %2$s. Arsye: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s ju ftoi. Arsye: %2$s</string> <string name="notice_room_invite_you_with_reason">%1$s ju ftoi. Arsye: %2$s</string>
<string name="notice_room_join_with_reason">%1$s erdhi. Arsye: %2$s</string> <string name="notice_room_join_with_reason">%1$s erdhi në dhomë. Arsye: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s iku. Arsye: %2$s</string> <string name="notice_room_leave_with_reason">%1$s doli nga dhoma. Arsye: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s hodhi poshtë ftesën. Arsye: %2$s</string> <string name="notice_room_reject_with_reason">%1$s hodhi poshtë ftesën. Arsye: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s përzuri %2$s. Arsye: %3$s</string> <string name="notice_room_kick_with_reason">%1$s përzuri %2$s. Arsye: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s hoqi dëbimin për %2$s. Arsye: %3$s</string> <string name="notice_room_unban_with_reason">%1$s hoqi dëbimin për %2$s. Arsye: %3$s</string>

View file

@ -5,8 +5,8 @@
<string name="notice_room_invite_no_invitee">%s 的邀请</string> <string name="notice_room_invite_no_invitee">%s 的邀请</string>
<string name="notice_room_invite">%1$s 邀请了 %2$s</string> <string name="notice_room_invite">%1$s 邀请了 %2$s</string>
<string name="notice_room_invite_you">%1$s 邀请了您</string> <string name="notice_room_invite_you">%1$s 邀请了您</string>
<string name="notice_room_join">%1$s 加入了</string> <string name="notice_room_join">%1$s 加入了聊天室</string>
<string name="notice_room_leave">%1$s 离开了</string> <string name="notice_room_leave">%1$s 离开了聊天室</string>
<string name="notice_room_reject">%1$s 拒绝了邀请</string> <string name="notice_room_reject">%1$s 拒绝了邀请</string>
<string name="notice_room_kick">%1$s 移除了 %2$s</string> <string name="notice_room_kick">%1$s 移除了 %2$s</string>
<string name="notice_room_unban">%1$s 解封了 %2$s</string> <string name="notice_room_unban">%1$s 解封了 %2$s</string>
@ -173,8 +173,8 @@
<string name="notice_room_invite_no_invitee_with_reason">%1$s 的邀请。理由:%2$s</string> <string name="notice_room_invite_no_invitee_with_reason">%1$s 的邀请。理由:%2$s</string>
<string name="notice_room_invite_with_reason">%1$s 邀请了 %2$s。理由%3$s</string> <string name="notice_room_invite_with_reason">%1$s 邀请了 %2$s。理由%3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s 邀请了您。理由:%2$s</string> <string name="notice_room_invite_you_with_reason">%1$s 邀请了您。理由:%2$s</string>
<string name="notice_room_join_with_reason">%1$s 加入。理由:%2$s</string> <string name="notice_room_join_with_reason">%1$s 加入了聊天室。理由:%2$s</string>
<string name="notice_room_leave_with_reason">%1$s 离开。理由:%2$s</string> <string name="notice_room_leave_with_reason">%1$s 离开了聊天室。理由:%2$s</string>
<string name="notice_room_reject_with_reason">%1$s 已拒绝邀请。理由:%2$s</string> <string name="notice_room_reject_with_reason">%1$s 已拒绝邀请。理由:%2$s</string>
<string name="notice_room_kick_with_reason">%1$s 踢走了 %2$s。理由%3$s</string> <string name="notice_room_kick_with_reason">%1$s 踢走了 %2$s。理由%3$s</string>
<string name="notice_room_unban_with_reason">%1$s 取消封锁了 %2$s。理由%3$s</string> <string name="notice_room_unban_with_reason">%1$s 取消封锁了 %2$s。理由%3$s</string>

View file

@ -23,7 +23,7 @@ PARAM_KS_PASS=$3
PARAM_KEY_PASS=$4 PARAM_KEY_PASS=$4
# Other params # Other params
BUILD_TOOLS_VERSION="28.0.3" BUILD_TOOLS_VERSION="29.0.3"
MIN_SDK_VERSION=19 MIN_SDK_VERSION=19
echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..." echo "Signing APK with build-tools version ${BUILD_TOOLS_VERSION} for min SDK version ${MIN_SDK_VERSION}..."

View file

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 19 ext.versionMinor = 20
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {

View file

@ -85,6 +85,7 @@
</activity> </activity>
<activity android:name=".features.debug.DebugMenuActivity" /> <activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name="im.vector.riotx.features.createdirect.CreateDirectRoomActivity" /> <activity android:name="im.vector.riotx.features.createdirect.CreateDirectRoomActivity" />
<activity android:name="im.vector.riotx.features.invite.InviteUsersToRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" /> <activity android:name=".features.webview.VectorWebViewActivity" />
<activity android:name=".features.link.LinkHandlerActivity"> <activity android:name=".features.link.LinkHandlerActivity">
<intent-filter> <intent-filter>

View file

@ -23,8 +23,6 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment 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.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment 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.LoginWaitForEmailFragment
import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.LoginWebFragment
import im.vector.riotx.features.login.terms.LoginTermsFragment 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.qrcode.QrCodeScannerFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment
@ -226,13 +226,13 @@ interface FragmentModule {
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(CreateDirectRoomDirectoryUsersFragment::class) @FragmentKey(UserDirectoryFragment::class)
fun bindCreateDirectRoomDirectoryUsersFragment(fragment: CreateDirectRoomDirectoryUsersFragment): Fragment fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(CreateDirectRoomKnownUsersFragment::class) @FragmentKey(KnownUsersFragment::class)
fun bindCreateDirectRoomKnownUsersFragment(fragment: CreateDirectRoomKnownUsersFragment): Fragment fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap

View file

@ -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.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.home.room.list.RoomListModule
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet 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.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
@ -116,6 +117,7 @@ interface ScreenComponent {
fun inject(activity: DebugMenuActivity) fun inject(activity: DebugMenuActivity)
fun inject(activity: SharedSecureStorageActivity) fun inject(activity: SharedSecureStorageActivity)
fun inject(activity: BigImageViewerActivity) fun inject(activity: BigImageViewerActivity)
fun inject(activity: InviteUsersToRoomActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets

View file

@ -22,7 +22,6 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.riotx.core.platform.ConfigurationViewModel 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.KeysBackupRestoreFromKeyViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel import im.vector.riotx.features.crypto.keysbackup.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.RoomDetailSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel 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.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.login.LoginSharedActionViewModel
import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.riotx.features.workers.signout.SignOutViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module @Module
@ -87,8 +86,8 @@ interface ViewModelModule {
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(CreateDirectRoomSharedActionViewModel::class) @ViewModelKey(UserDirectorySharedActionViewModel::class)
fun bindCreateDirectRoomSharedActionViewModel(viewModel: CreateDirectRoomSharedActionViewModel): ViewModel fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel
@Binds @Binds
@IntoMap @IntoMap
@ -110,11 +109,6 @@ interface ViewModelModule {
@ViewModelKey(RoomDirectorySharedActionViewModel::class) @ViewModelKey(RoomDirectorySharedActionViewModel::class)
fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(LoginSharedActionViewModel::class)
fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(RoomDetailSharedActionViewModel::class) @ViewModelKey(RoomDetailSharedActionViewModel::class)

View file

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity.* import kotlinx.android.synthetic.main.activity.*
import javax.inject.Inject import javax.inject.Inject
@ -107,4 +108,15 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {
} }
super.onBackPressed() super.onBackPressed()
} }
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
hideWaitingView()
observer(it)
}
.disposeOnDestroy()
}
} }

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * 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 import im.vector.riotx.core.platform.VectorViewModelAction
sealed class CreateDirectRoomAction : VectorViewModelAction { sealed class CreateDirectRoomAction : VectorViewModelAction {
object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set<User>) : 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()
} }

View file

@ -37,6 +37,12 @@ import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData 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 kotlinx.android.synthetic.main.activity.*
import java.net.HttpURLConnection import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
@ -44,7 +50,8 @@ import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() { class CreateDirectRoomActivity : SimpleFragmentActivity() {
private val viewModel: CreateDirectRoomViewModel by viewModel() 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 createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@ -56,26 +63,40 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
sharedActionViewModel sharedActionViewModel
.observe() .observe()
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (sharedAction) { when (sharedAction) {
CreateDirectRoomSharedAction.OpenUsersDirectory -> UserDirectorySharedAction.OpenUsersDirectory ->
addFragmentToBackstack(R.id.container, CreateDirectRoomDirectoryUsersFragment::class.java) addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java)
CreateDirectRoomSharedAction.Close -> finish() UserDirectorySharedAction.Close -> finish()
CreateDirectRoomSharedAction.GoBack -> onBackPressed() UserDirectorySharedAction.GoBack -> onBackPressed()
is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction)
} }
} }
.disposeOnDestroy() .disposeOnDestroy()
if (isFirstCreation()) { 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) { viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) {
renderCreateAndInviteState(it) 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<String>) { private fun renderCreateAndInviteState(state: Async<String>) {
when (state) { when (state) {
is Loading -> renderCreationLoading() is Loading -> renderCreationLoading()

View file

@ -18,7 +18,4 @@ package im.vector.riotx.features.createdirect
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for create direct room screen
*/
sealed class CreateDirectRoomViewEvents : VectorViewEvents sealed class CreateDirectRoomViewEvents : VectorViewEvents

View file

@ -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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session 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.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.matrix.rx.rx
import im.vector.riotx.core.extensions.toggle
import im.vector.riotx.core.platform.VectorViewModel 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 class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState, initialState: CreateDirectRoomViewState,
@ -48,9 +37,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel
} }
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> { companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@JvmStatic @JvmStatic
@ -60,25 +46,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} }
} }
init {
observeKnownUsers()
observeDirectoryUsers()
}
override fun handle(action: CreateDirectRoomAction) { override fun handle(action: CreateDirectRoomAction) {
when (action) { when (action) {
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers)
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)
} }
} }
private fun createRoomAndInviteSelectedUsers() = withState { currentState -> private fun createRoomAndInviteSelectedUsers(selectedUsers: Set<User>) {
val roomParams = CreateRoomParams( val roomParams = CreateRoomParams(
invitedUserIds = currentState.selectedUsers.map { it.userId } invitedUserIds = selectedUsers.map { it.userId }
) )
.setDirectMessage() .setDirectMessage()
.enableEncryptionIfInvitedUsersSupportIt() .enableEncryptionIfInvitedUsersSupportIt()
@ -89,52 +65,4 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
copy(createAndInviteState = it) 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()
)
}
}
} }

View file

@ -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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.createdirect
import androidx.paging.PagedList
import arrow.core.Option
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.user.model.User
data class CreateDirectRoomViewState( data class CreateDirectRoomViewState(
val knownUsers: Async<PagedList<User>> = Uninitialized, val createAndInviteState: Async<String> = Uninitialized
val directoryUsers: Async<List<User>> = Uninitialized, ) : MvRxState
val selectedUsers: Set<User> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val directorySearchTerm: String = "",
val filterKnownUsersValue: Option<String> = Option.empty()
) : MvRxState {
enum class DisplayMode {
KNOWN_USERS,
DIRECTORY_USERS
}
}

View file

@ -51,7 +51,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
try { try {
sharedViewModel.recoverUsingBackupPass(recoveryKey) sharedViewModel.recoverUsingBackupPass(recoveryKey)
} catch (failure: Throwable) { } 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))
} }
} }
} }

View file

@ -17,11 +17,13 @@
package im.vector.riotx.features.home package im.vector.riotx.features.home
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.DrawableImageViewTarget
@ -72,6 +74,28 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.into(target) .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 @AnyThread
fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable { fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
return buildGlideRequest(glideRequest, matrixItem.avatarUrl) return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
@ -82,10 +106,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
@AnyThread @AnyThread
fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
val avatarColor = when (matrixItem) { val avatarColor = avatarColor(matrixItem, context)
is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id))
else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id))
}
return TextDrawable.builder() return TextDrawable.builder()
.beginConfig() .beginConfig()
.bold() .bold()
@ -96,11 +117,21 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() val resolvedUrl = resolvedUrl(avatarUrl)
?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
return glideRequest return glideRequest
.load(resolvedUrl) .load(resolvedUrl)
.apply(RequestOptions.circleCropTransform()) .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))
}
}
} }

View file

@ -65,6 +65,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var shortcutsHandler: ShortcutsHandler
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
@ -144,6 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
&& activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) { && activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) {
promptCompleteSecurityIfNeeded() promptCompleteSecurityIfNeeded()
} }
shortcutsHandler.observeRoomsAndBuildShortcuts()
.disposeOnDestroy()
} }
private fun promptCompleteSecurityIfNeeded() { private fun promptCompleteSecurityIfNeeded() {

View file

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

View file

@ -44,8 +44,14 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
waitingView = waiting_view waitingView = waiting_view
if (isFirstCreation()) { if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) {
?: return RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!)
} else {
intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
}
if (roomDetailArgs == null) return
currentRoomId = roomDetailArgs.roomId currentRoomId = roomDetailArgs.roomId
replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs)
replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java)
@ -110,11 +116,20 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object { companion object {
const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" 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 { fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent {
return Intent(context, RoomDetailActivity::class.java).apply { return Intent(context, RoomDetailActivity::class.java).apply {
putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs) 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)
}
}
} }
} }

View file

@ -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<User>) : InviteUsersToRoomAction()
}

View file

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

View file

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

View file

@ -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<InviteUsersToRoomViewState, InviteUsersToRoomAction, InviteUsersToRoomViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
@AssistedInject.Factory
interface Factory {
fun create(initialState: InviteUsersToRoomViewState): InviteUsersToRoomViewModel
}
companion object : MvRxViewModelFactory<InviteUsersToRoomViewModel, InviteUsersToRoomViewState> {
@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<User>) {
_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<String> {
return room.roomSummary()?.otherMemberIds?.toSet() ?: emptySet()
}
}

View file

@ -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<Unit> = Uninitialized
) : MvRxState {
constructor(args: InviteUsersToRoomArgs) : this(roomId = args.roomId)
}

View file

@ -38,7 +38,6 @@ import javax.net.ssl.HttpsURLConnection
abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
protected val loginViewModel: LoginViewModel by activityViewModel() protected val loginViewModel: LoginViewModel by activityViewModel()
protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
private var isResetPasswordStarted = false private var isResetPasswordStarted = false
@ -57,8 +56,6 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java)
loginViewModel.observeViewEvents { loginViewModel.observeViewEvents {
handleLoginViewEvents(it) handleLoginViewEvents(it)
} }

View file

@ -58,4 +58,6 @@ sealed class LoginAction : VectorViewModelAction {
// For the soft logout case // For the soft logout case
data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction()
data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction()
} }

View file

@ -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.POP_BACK_STACK_EXCLUSIVE
import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack 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.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
@ -54,7 +55,6 @@ import javax.inject.Inject
open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private val loginViewModel: LoginViewModel by viewModel() private val loginViewModel: LoginViewModel by viewModel()
private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
@ -98,14 +98,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
loginViewModel.handle(LoginAction.InitWith(loginConfig)) loginViewModel.handle(LoginAction.InitWith(loginConfig))
} }
loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java)
loginSharedActionViewModel
.observe()
.subscribe {
handleLoginNavigation(it)
}
.disposeOnDestroy()
loginViewModel loginViewModel
.subscribe(this) { .subscribe(this) {
updateWithState(it) updateWithState(it)
@ -124,65 +116,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) 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<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(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) { private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) {
when (loginViewEvents) { when (loginViewEvents) {
is LoginViewEvents.RegistrationFlowResult -> { is LoginViewEvents.RegistrationFlowResult -> {
// Check that all flows are supported by the application // Check that all flows are supported by the application
if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) {
// Display a popup to propose use web fallback // 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) AlertDialog.Builder(this)
.setTitle(R.string.login_error_outdated_homeserver_title) .setTitle(R.string.login_error_outdated_homeserver_title)
.setMessage(R.string.login_error_outdated_homeserver_content) .setMessage(R.string.login_error_outdated_homeserver_content)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
is LoginViewEvents.Failure -> is LoginViewEvents.Failure ->
// This is handled by the Fragments // This is handled by the Fragments
Unit Unit
is LoginViewEvents.OpenServerSelection ->
addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment::class.java,
option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(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() loginLoading.isVisible = loginViewState.isLoading()
} }
private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) {
// Pop the backstack // Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
@ -254,11 +239,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private fun onSignModeSelected() = withState(loginViewModel) { state -> private fun onSignModeSelected() = withState(loginViewModel) { state ->
when (state.signMode) { when (state.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> { SignMode.SignUp -> {
// This is managed by the LoginViewEvents // This is managed by the LoginViewEvents
} }
SignMode.SignIn -> { SignMode.SignIn -> {
// It depends on the LoginMode // It depends on the LoginMode
when (state.loginMode) { when (state.loginMode) {
LoginMode.Unknown -> error("Developer error") LoginMode.Unknown -> error("Developer error")
@ -272,7 +257,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes)
} }
} }
} SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginFragment::class.java,
tag = FRAGMENT_LOGIN_TAG,
option = commonOption)
}.exhaustive
} }
private fun onRegistrationStageNotSupported() { private fun onRegistrationStageNotSupported() {

View file

@ -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.MatrixError
import im.vector.matrix.android.api.failure.isInvalidPassword import im.vector.matrix.android.api.failure.isInvalidPassword
import im.vector.riotx.R 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.hideKeyboard
import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.extensions.toReducedUrl import im.vector.riotx.core.extensions.toReducedUrl
@ -73,16 +74,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private fun setupAutoFill(state: LoginViewState) { private fun setupAutoFill(state: LoginViewState) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
when (state.signMode) { when (state.signMode) {
SignMode.Unknown -> error("developer error") SignMode.Unknown -> error("developer error")
SignMode.SignUp -> { SignMode.SignUp -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
} }
SignMode.SignIn -> { SignMode.SignIn,
SignMode.SignInWithMatrixId -> {
loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME)
passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
} }
} }.exhaustive
} }
} }
@ -116,35 +118,44 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
} }
private fun setupUi(state: LoginViewState) { 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) { loginFieldTil.hint = getString(when (state.signMode) {
SignMode.Unknown -> error("developer error") SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_username_hint SignMode.SignUp -> R.string.login_signup_username_hint
SignMode.SignIn -> R.string.login_signin_username_hint SignMode.SignIn -> R.string.login_signin_username_hint
SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint
}) })
when (state.serverType) { // Handle direct signin first
ServerType.MatrixOrg -> { if (state.signMode == SignMode.SignInWithMatrixId) {
loginServerIcon.isVisible = true loginServerIcon.isVisible = false
loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) loginTitle.text = getString(R.string.login_signin_matrix_id_title)
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginNotice.text = getString(R.string.login_signin_matrix_id_notice)
loginNotice.text = getString(R.string.login_server_matrix_org_text) } 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 when (state.serverType) {
loginServerIcon.setImageResource(R.drawable.ic_logo_modular) ServerType.MatrixOrg -> {
loginTitle.text = getString(resId, "Modular") loginServerIcon.isVisible = true
loginNotice.text = getString(R.string.login_server_modular_text) loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
} loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
ServerType.Other -> { loginNotice.text = getString(R.string.login_server_matrix_org_text)
loginServerIcon.isVisible = false }
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) ServerType.Modular -> {
loginNotice.text = getString(R.string.login_server_other_text) 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 forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn
loginSubmit.text = getString(when (state.signMode) { loginSubmit.text = getString(when (state.signMode) {
SignMode.Unknown -> error("developer error") SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_submit SignMode.SignUp -> R.string.login_signup_submit
SignMode.SignIn -> R.string.login_signin SignMode.SignIn,
SignMode.SignInWithMatrixId -> R.string.login_signin
}) })
} }
@ -178,7 +190,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
@OnClick(R.id.forgetPasswordButton) @OnClick(R.id.forgetPasswordButton)
fun forgetPasswordClicked() { fun forgetPasswordClicked() {
loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked))
} }
private fun setupPasswordReveal() { private fun setupPasswordReveal() {

View file

@ -217,7 +217,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra
TextInputFormFragmentMode.SetEmail -> { TextInputFormFragmentMode.SetEmail -> {
if (throwable.is401()) { if (throwable.is401()) {
// This is normal use case, we go to the mail waiting screen // 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 { } else {
loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
} }
@ -225,7 +225,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra
TextInputFormFragmentMode.SetMsisdn -> { TextInputFormFragmentMode.SetMsisdn -> {
if (throwable.is401()) { if (throwable.is401()) {
// This is normal use case, we go to the enter code screen // 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 { } else {
loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable)
} }

View file

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

View file

@ -149,7 +149,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment()
resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error)
} }
is Success -> { is Success -> {
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone) Unit
} }
} }
} }

View file

@ -64,7 +64,7 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor() : Abstrac
.show() .show()
} }
is Success -> { is Success -> {
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess) Unit
} }
} }
} }

View file

@ -29,7 +29,7 @@ class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFra
@OnClick(R.id.resetPasswordSuccessSubmit) @OnClick(R.id.resetPasswordSuccessSubmit)
fun submit() { fun submit() {
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone))
} }
override fun resetViewModel() { override fun resetViewModel() {

View file

@ -95,10 +95,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
// Request login flow here // Request login flow here
loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url)))
} else { } 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() { override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetHomeServerType) loginViewModel.handle(LoginAction.ResetHomeServerType)
} }
@ -108,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
if (state.loginMode != LoginMode.Unknown) { if (state.loginMode != LoginMode.Unknown) {
// LoginFlow for matrix.org has been retrieved // LoginFlow for matrix.org has been retrieved
loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
} }
} }
} }

View file

@ -126,7 +126,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
if (state.loginMode != LoginMode.Unknown) { if (state.loginMode != LoginMode.Unknown) {
// The home server url is valid // The home server url is valid
loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved))
} }
} }
} }

View file

@ -78,7 +78,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr
@OnClick(R.id.loginSignupSigninSignIn) @OnClick(R.id.loginSignupSigninSignIn)
fun signIn() { fun signIn() {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn))
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
} }
override fun resetViewModel() { override fun resetViewModel() {

View file

@ -29,7 +29,7 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() {
@OnClick(R.id.loginSplashSubmit) @OnClick(R.id.loginSplashSubmit)
fun getStarted() { fun getStarted() {
loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OpenServerSelection))
} }
override fun resetViewModel() { override fun resetViewModel() {

View file

@ -23,10 +23,26 @@ import im.vector.riotx.core.platform.VectorViewEvents
/** /**
* Transient events for Login * Transient events for Login
*/ */
sealed class LoginViewEvents: VectorViewEvents { sealed class LoginViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : LoginViewEvents() data class Loading(val message: CharSequence? = null) : LoginViewEvents()
data class Failure(val throwable: Throwable) : LoginViewEvents() data class Failure(val throwable: Throwable) : LoginViewEvents()
data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents() data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents()
object OutdatedHomeserver : 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()
} }

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.login package im.vector.riotx.features.login
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -29,19 +30,24 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService 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.data.LoginFlowResult
import im.vector.matrix.android.api.auth.login.LoginWizard 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.FlowResult
import im.vector.matrix.android.api.auth.registration.RegistrationResult 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.RegistrationWizard
import im.vector.matrix.android.api.auth.registration.Stage 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.session.Session
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth 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.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.configureAndStart 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.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.signout.soft.SoftLogoutActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity
@ -51,14 +57,16 @@ import java.util.concurrent.CancellationException
/** /**
* *
*/ */
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, class LoginViewModel @AssistedInject constructor(
private val applicationContext: Context, @Assisted initialState: LoginViewState,
private val authenticationService: AuthenticationService, private val applicationContext: Context,
private val activeSessionHolder: ActiveSessionHolder, private val authenticationService: AuthenticationService,
private val pushRuleTriggerListener: PushRuleTriggerListener, private val activeSessionHolder: ActiveSessionHolder,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val pushRuleTriggerListener: PushRuleTriggerListener,
private val sessionListener: SessionListener, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val reAuthHelper: ReAuthHelper) private val sessionListener: SessionListener,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider)
: VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) { : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
@ -108,7 +116,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
} is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent)
}.exhaustive
} }
private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
@ -320,11 +329,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
) )
} }
if (action.signMode == SignMode.SignUp) { when (action.signMode) {
startRegistrationFlow() SignMode.SignUp -> startRegistrationFlow()
} else if (action.signMode == SignMode.SignIn) { SignMode.SignIn -> startAuthenticationFlow()
startAuthenticationFlow() SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected)
} SignMode.Unknown -> Unit
}.exhaustive
} }
private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { private fun handleUpdateServerType(action: LoginAction.UpdateServerType) {
@ -365,6 +375,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
resetPasswordEmail = action.email resetPasswordEmail = action.email
) )
} }
_viewEvents.post(LoginViewEvents.OnResetPasswordSendThreePidDone)
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -405,6 +417,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
resetPasswordEmail = null resetPasswordEmail = null
) )
} }
_viewEvents.post(LoginViewEvents.OnResetPasswordMailConfirmationSuccess)
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -421,10 +435,78 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state -> private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state ->
when (state.signMode) { when (state.signMode) {
SignMode.SignIn -> handleLogin(action) SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignUp -> handleRegisterWith(action) SignMode.SignIn -> handleLogin(action)
else -> error("Developer error, invalid sign mode") 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<WellknownResult> {
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<Session> {
override fun onSuccess(data: Session) {
onSessionCreated(data)
}
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
}
})
} }
private fun handleLogin(action: LoginAction.LoginOrRegister) { private fun handleLogin(action: LoginAction.LoginOrRegister) {
@ -477,6 +559,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
private fun startAuthenticationFlow() { private fun startAuthenticationFlow() {
// Ensure Wizard is ready // Ensure Wizard is ready
loginWizard loginWizard
_viewEvents.post(LoginViewEvents.OnSignModeSelected)
} }
private fun onFlowResponse(flowResult: FlowResult) { private fun onFlowResponse(flowResult: FlowResult) {

View file

@ -173,7 +173,7 @@ class LoginWebFragment @Inject constructor(
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl) 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?) { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {

View file

@ -21,5 +21,7 @@ enum class SignMode {
// Account creation // Account creation
SignUp, SignUp,
// Login // Login
SignIn SignIn,
// Login directly with matrix Id
SignInWithMatrixId
} }

View file

@ -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.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity 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.media.BigImageViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
@ -163,6 +164,11 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent) context.startActivity(intent)
} }
override fun openInviteUsersToRoom(context: Context, roomId: String) {
val intent = InviteUsersToRoomActivity.getIntent(context, roomId)
context.startActivity(intent)
}
override fun openRoomsFiltering(context: Context) { override fun openRoomsFiltering(context: Context) {
val intent = FilteredRoomsActivity.newIntent(context) val intent = FilteredRoomsActivity.newIntent(context)
context.startActivity(intent) context.startActivity(intent)

View file

@ -46,6 +46,8 @@ interface Navigator {
fun openCreateDirectRoom(context: Context) fun openCreateDirectRoom(context: Context)
fun openInviteUsersToRoom(context: Context, roomId: String)
fun openRoomDirectory(context: Context, initialFilter: String = "") fun openRoomDirectory(context: Context, initialFilter: String = "")
fun openRoomsFiltering(context: Context) fun openRoomsFiltering(context: Context)

View file

@ -348,12 +348,19 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
globalLastMessageTimestamp = lastMessageTimestamp 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( val notification = notificationUtils.buildMessagesListNotification(
style, style,
roomEventGroupInfo, roomEventGroupInfo,
largeBitmap, largeBitmap,
lastMessageTimestamp, lastMessageTimestamp,
myUserDisplayName) myUserDisplayName,
tickerText)
// is there an id for this room? // is there an id for this room?
notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)

View file

@ -381,7 +381,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?): Notification { senderDisplayNameForReplyCompat: String?,
tickerText: String): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked // Build the pending intent for when the notification is clicked
val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) 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) System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
setDeleteIntent(pendingIntent) setDeleteIntent(pendingIntent)
} }
.setTicker(tickerText)
.build() .build()
} }

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.roomprofile.members package im.vector.riotx.features.roomprofile.members
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
@ -43,6 +44,18 @@ class RoomMemberListFragment @Inject constructor(
override fun getLayoutResId() = R.layout.fragment_room_setting_generic 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this roomMemberListController.callback = this

View file

@ -30,7 +30,6 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback 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.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod 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.functions.BiFunction
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
data class DevicesViewState( data class DevicesViewState(
@ -65,6 +65,7 @@ data class DeviceFullInfo(
val deviceInfo: DeviceInfo, val deviceInfo: DeviceInfo,
val cryptoDeviceInfo: CryptoDeviceInfo? val cryptoDeviceInfo: CryptoDeviceInfo?
) )
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val session: Session private val session: Session
@ -215,8 +216,13 @@ class DevicesViewModel @AssistedInject constructor(
private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state -> private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state ->
viewModelScope.launch { viewModelScope.launch {
if (state.hasAccountCrossSigning) { if (state.hasAccountCrossSigning) {
awaitCallback<Unit> { try {
tryThis { session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) } awaitCallback<Unit> {
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 { } else {
// legacy // legacy

View file

@ -30,7 +30,7 @@ import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginAction
import im.vector.riotx.features.login.LoginMode 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 kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject import javax.inject.Inject
@ -94,7 +94,7 @@ class SoftLogoutFragment @Inject constructor(
} }
override fun signinFallbackSubmit() { override fun signinFallbackSubmit() {
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected))
} }
override fun clearData() { override fun clearData() {
@ -124,7 +124,7 @@ class SoftLogoutFragment @Inject constructor(
} }
override fun forgetPasswordClicked() { override fun forgetPasswordClicked() {
loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked))
} }
override fun revealPasswordClicked() { override fun revealPasswordClicked() {

View file

@ -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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.epoxy.EpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -41,7 +39,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : EpoxyController() { private val errorFormatter: ErrorFormatter) : EpoxyController() {
private var state: CreateDirectRoomViewState? = null private var state: UserDirectoryViewState? = null
var callback: Callback? = null var callback: Callback? = null
@ -49,7 +47,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
requestModelBuild() requestModelBuild()
} }
fun setData(state: CreateDirectRoomViewState) { fun setData(state: UserDirectoryViewState) {
this.state = state this.state = state
requestModelBuild() requestModelBuild()
} }
@ -110,7 +108,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
continue continue
} }
val isSelected = selectedUsers.contains(user.userId) val isSelected = selectedUsers.contains(user.userId)
createDirectRoomUserItem { userDirectoryUserItem {
id(user.userId) id(user.userId)
selected(isSelected) selected(isSelected)
matrixItem(user.toMatrixItem()) matrixItem(user.toMatrixItem())

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,7 +14,7 @@
* limitations under the License. * 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.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.epoxy.paging.PagedListEpoxyController
@ -49,7 +49,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
requestModelBuild() requestModelBuild()
} }
fun setData(state: CreateDirectRoomViewState) { fun setData(state: UserDirectoryViewState) {
this.isFiltering = !state.filterKnownUsersValue.isEmpty() this.isFiltering = !state.filterKnownUsersValue.isEmpty()
val newSelection = state.selectedUsers.map { it.userId } val newSelection = state.selectedUsers.map { it.userId }
this.users = state.knownUsers this.users = state.knownUsers
@ -65,7 +65,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
EmptyItem_().id(currentPosition) EmptyItem_().id(currentPosition)
} else { } else {
val isSelected = selectedUsers.contains(item.userId) val isSelected = selectedUsers.contains(item.userId)
CreateDirectRoomUserItem_() UserDirectoryUserItem_()
.id(item.userId) .id(item.userId)
.selected(isSelected) .selected(isSelected)
.matrixItem(item.toMatrixItem()) .matrixItem(item.toMatrixItem())
@ -84,13 +84,13 @@ class KnownUsersController @Inject constructor(private val session: Session,
} else { } else {
var lastFirstLetter: String? = null var lastFirstLetter: String? = null
for (model in models) { for (model in models) {
if (model is CreateDirectRoomUserItem) { if (model is UserDirectoryUserItem) {
if (model.matrixItem.id == session.myUserId) continue if (model.matrixItem.id == session.myUserId) continue
val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName() val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter lastFirstLetter = currentFirstLetter
CreateDirectRoomLetterHeaderItem_() UserDirectoryLetterHeaderItem_()
.id(currentFirstLetter) .id(currentFirstLetter)
.letter(currentFirstLetter) .letter(currentFirstLetter)
.addIf(showLetter, this) .addIf(showLetter, this)

View file

@ -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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ScrollView import android.widget.ScrollView
import androidx.core.view.forEach
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.args
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.jakewharton.rxbinding3.widget.textChanges 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.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter 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 import javax.inject.Inject
class CreateDirectRoomKnownUsersFragment @Inject constructor( class KnownUsersFragment @Inject constructor(
val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory,
private val knownUsersController: KnownUsersController, private val knownUsersController: KnownUsersController,
private val dimensionConverter: DimensionConverter private val dimensionConverter: DimensionConverter
) : VectorBaseFragment(), KnownUsersController.Callback { ) : 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() override fun getMenuRes() = args.menuResId
private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel
private val viewModel: UserDirectoryViewModel by activityViewModel()
private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar)
knownUsersTitle.text = args.title
vectorBaseActivity.setSupportActionBar(knownUsersToolbar)
setupRecyclerView() setupRecyclerView()
setupFilterView() setupFilterView()
setupAddByMatrixIdView() setupAddByMatrixIdView()
setupCloseView() setupCloseView()
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) {
renderSelectedUsers(it) renderSelectedUsers(it)
} }
} }
@ -71,27 +77,22 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { withState(viewModel) {
val createMenuItem = menu.findItem(R.id.action_create_direct_room)
val showMenuItem = it.selectedUsers.isNotEmpty() val showMenuItem = it.selectedUsers.isNotEmpty()
createMenuItem.setVisible(showMenuItem) menu.forEach { menuItem ->
menuItem.isVisible = showMenuItem
}
} }
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
return when (item.itemId) { sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.selectedUsers))
R.id.action_create_direct_room -> { return@withState true
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers)
true
}
else ->
super.onOptionsItemSelected(item)
}
} }
private fun setupAddByMatrixIdView() { private fun setupAddByMatrixIdView() {
addByMatrixId.setOnClickListener { addByMatrixId.setOnClickListener {
sharedActionViewModel.post(CreateDirectRoomSharedAction.OpenUsersDirectory) sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory)
} }
} }
@ -102,26 +103,26 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
} }
private fun setupFilterView() { private fun setupFilterView() {
createDirectRoomFilter knownUsersFilter
.textChanges() .textChanges()
.startWith(createDirectRoomFilter.text) .startWith(knownUsersFilter.text)
.subscribe { text -> .subscribe { text ->
val filterValue = text.trim() val filterValue = text.trim()
val action = if (filterValue.isBlank()) { val action = if (filterValue.isBlank()) {
CreateDirectRoomAction.ClearFilterKnownUsers UserDirectoryAction.ClearFilterKnownUsers
} else { } else {
CreateDirectRoomAction.FilterKnownUsers(filterValue.toString()) UserDirectoryAction.FilterKnownUsers(filterValue.toString())
} }
viewModel.handle(action) viewModel.handle(action)
} }
.disposeOnDestroyView() .disposeOnDestroyView()
createDirectRoomFilter.setupAsSearch() knownUsersFilter.setupAsSearch()
createDirectRoomFilter.requestFocus() knownUsersFilter.requestFocus()
} }
private fun setupCloseView() { private fun setupCloseView() {
createDirectRoomClose.setOnClickListener { knownUsersClose.setOnClickListener {
requireActivity().finish() requireActivity().finish()
} }
} }
@ -157,12 +158,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
chip.isCloseIconVisible = true chip.isCloseIconVisible = true
chipGroup.addView(chip) chipGroup.addView(chip)
chip.setOnCloseIconClickListener { chip.setOnCloseIconClickListener {
viewModel.handle(CreateDirectRoomAction.RemoveSelectedUser(user)) viewModel.handle(UserDirectoryAction.RemoveSelectedUser(user))
} }
} }
override fun onItemClick(user: User) { override fun onItemClick(user: User) {
view?.hideKeyboard() view?.hideKeyboard()
viewModel.handle(CreateDirectRoomAction.SelectUser(user)) viewModel.handle(UserDirectoryAction.SelectUser(user))
} }
} }

View file

@ -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<String>? = null
) : Parcelable

View file

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

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import android.os.Bundle import android.os.Bundle
import android.view.View 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.setupAsSearch
import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.platform.VectorBaseFragment 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 import javax.inject.Inject
class CreateDirectRoomDirectoryUsersFragment @Inject constructor( class UserDirectoryFragment @Inject constructor(
private val directRoomController: DirectoryUsersController private val directRoomController: DirectoryUsersController
) : VectorBaseFragment(), DirectoryUsersController.Callback { ) : 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: UserDirectorySharedActionViewModel
private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView() setupRecyclerView()
setupSearchByMatrixIdView() setupSearchByMatrixIdView()
setupCloseView() setupCloseView()
@ -62,19 +62,19 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
} }
private fun setupSearchByMatrixIdView() { private fun setupSearchByMatrixIdView() {
createDirectRoomSearchById.setupAsSearch(searchIconRes = 0) userDirectorySearchById.setupAsSearch(searchIconRes = 0)
createDirectRoomSearchById userDirectorySearchById
.textChanges() .textChanges()
.subscribe { .subscribe {
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString()))
} }
.disposeOnDestroyView() .disposeOnDestroyView()
createDirectRoomSearchById.showKeyboard(andRequestFocus = true) userDirectorySearchById.showKeyboard(andRequestFocus = true)
} }
private fun setupCloseView() { private fun setupCloseView() {
createDirectRoomClose.setOnClickListener { userDirectoryClose.setOnClickListener {
sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
} }
} }
@ -84,12 +84,12 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
override fun onItemClick(user: User) { override fun onItemClick(user: User) {
view?.hideKeyboard() view?.hideKeyboard()
viewModel.handle(CreateDirectRoomAction.SelectUser(user)) viewModel.handle(UserDirectoryAction.SelectUser(user))
sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) sharedActionViewModel.post(UserDirectorySharedAction.GoBack)
} }
override fun retryDirectoryUsersRequest() { override fun retryDirectoryUsersRequest() {
val currentSearch = createDirectRoomSearchById.text.toString() val currentSearch = userDirectorySearchById.text.toString()
viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(currentSearch)) viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch))
} }
} }

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute 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.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header) @EpoxyModelClass(layout = R.layout.item_user_directory_letter_header)
abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel<CreateDirectRoomLetterHeaderItem.Holder>() { abstract class UserDirectoryLetterHeaderItem : VectorEpoxyModel<UserDirectoryLetterHeaderItem.Holder>() {
@EpoxyAttribute var letter: String = "" @EpoxyAttribute var letter: String = ""
@ -33,6 +33,6 @@ abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel<CreateDirectR
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val letterView by bind<TextView>(R.id.createDirectRoomLetterView) val letterView by bind<TextView>(R.id.userDirectoryLetterView)
} }
} }

View file

@ -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<User>) : UserDirectorySharedAction()
}

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.login package im.vector.riotx.features.userdirectory
import im.vector.riotx.core.platform.VectorSharedActionViewModel import im.vector.riotx.core.platform.VectorSharedActionViewModel
import javax.inject.Inject import javax.inject.Inject
class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<LoginNavigation>() class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<UserDirectorySharedAction>()

View file

@ -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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * 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.
* *
* 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.view.View
import android.widget.ImageView 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.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_create_direct_room_user) @EpoxyModelClass(layout = R.layout.item_known_user)
abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() { abstract class UserDirectoryUserItem : VectorEpoxyModel<UserDirectoryUserItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@ -66,9 +64,9 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val userIdView by bind<TextView>(R.id.createDirectRoomUserID) val userIdView by bind<TextView>(R.id.knownUserID)
val nameView by bind<TextView>(R.id.createDirectRoomUserName) val nameView by bind<TextView>(R.id.knownUserName)
val avatarImageView by bind<ImageView>(R.id.createDirectRoomUserAvatar) val avatarImageView by bind<ImageView>(R.id.knownUserAvatar)
val avatarCheckedImageView by bind<ImageView>(R.id.createDirectRoomUserAvatarChecked) val avatarCheckedImageView by bind<ImageView>(R.id.knownUserAvatarChecked)
} }
} }

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,9 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.createdirect package im.vector.riotx.features.userdirectory
import im.vector.riotx.core.platform.VectorSharedActionViewModel import im.vector.riotx.core.platform.VectorViewEvents
import javax.inject.Inject
class CreateDirectRoomSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<CreateDirectRoomSharedAction>() /**
* Transient events for invite users to room screen
*/
sealed class UserDirectoryViewEvents : VectorViewEvents

View file

@ -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<UserDirectoryViewState, UserDirectoryAction, UserDirectoryViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel
}
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
companion object : MvRxViewModelFactory<UserDirectoryViewModel, UserDirectoryViewState> {
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<FragmentActivity>()) {
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().userDirectoryViewModelFactory.create(state)
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().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()
)
}
}
}

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