diff --git a/CHANGES.md b/CHANGES.md index 8bdd42636d..803b974413 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,19 +2,23 @@ Changes in RiotX 0.20.0 (2020-XX-XX) =================================================== 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) Translations 🗣: - SDK API changes ⚠️: - - + - excludedUserIds parameter add to to UserService.getPagedUsersLive() function Build 🧱: - diff --git a/gradle.properties b/gradle.properties index 2e2b110f15..d9d9e57cbc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx8192m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index ad50b9b972..5cc9d1fc00 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.send.UserDraft import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single @@ -96,6 +97,10 @@ class RxRoom(private val room: Room) { fun liveNotificationState(): Observable { return room.getLiveRoomNotificationState().asObservable() } + + fun invite(userId: String, reason: String? = null): Completable = completableBuilder { + room.invite(userId, reason, it) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index c2c8978500..e92da1e424 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -90,8 +90,8 @@ class RxSession(private val session: Session) { return session.getIgnoredUsersLive().asObservable() } - fun livePagedUsers(filter: String? = null): Observable> { - return session.getPagedUsersLive(filter).asObservable() + fun livePagedUsers(filter: String? = null, excludedUserIds: Set? = null): Observable> { + return session.getPagedUsersLive(filter, excludedUserIds).asObservable() } fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index 140d1c259f..5150420de2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -30,7 +31,6 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to authenticate or to create an account to a matrix server. */ interface AuthenticationService { - /** * Request the supported login flows for this homeserver. * This is the first method to call to be able to get a wizard to login or the create an account @@ -89,4 +89,20 @@ interface AuthenticationService { fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable + + /** + * Perform a wellknown request, using the domain from the matrixId + */ + fun getWellKnownData(matrixId: String, + callback: MatrixCallback): Cancelable + + /** + * Authenticate with a matrixId and a password + * Usually call this after a successful call to getWellKnownData() + */ + fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt index 72affe24bb..d88cd5e74d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/Credentials.kt @@ -24,16 +24,38 @@ import im.vector.matrix.android.internal.util.md5 * This data class hold credentials user data. * You shouldn't have to instantiate it. * The access token should be use to authenticate user in all server requests. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login */ @JsonClass(generateAdapter = true) data class Credentials( + /** + * The fully-qualified Matrix ID that has been registered. + */ @Json(name = "user_id") val userId: String, - @Json(name = "home_server") val homeServer: String, + /** + * An access token for the account. This access token can then be used to authorize other requests. + */ @Json(name = "access_token") val accessToken: String, + /** + * Not documented + */ @Json(name = "refresh_token") val refreshToken: String?, + /** + * The server_name of the homeserver on which the account has been registered. + * @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon) + * if they require it. Note also that homeserver is not spelt this way. + */ + @Json(name = "home_server") val homeServer: String, + /** + * ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified. + */ @Json(name = "device_id") val deviceId: String?, - // Optional data that may contain info to override home server and/or identity server - @Json(name = "well_known") val wellKnown: WellKnown? = null + /** + * Optional client configuration provided by the server. If present, clients SHOULD use the provided object to + * reconfigure themselves, optionally validating the URLs within. + * This object takes the same form as the one returned from .well-known autodiscovery. + */ + @Json(name = "well_known") val discoveryInformation: DiscoveryInformation? = null ) internal fun Credentials.sessionId(): String { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt new file mode 100644 index 0000000000..2aa741bad3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/DiscoveryInformation.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This is a light version of Wellknown model, used for login response + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login + */ +@JsonClass(generateAdapter = true) +data class DiscoveryInformation( + /** + * Required. Used by clients to discover homeserver information. + */ + @Json(name = "m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + /** + * Used by clients to discover identity server information. + * Note: matrix.org does not send this field + */ + @Json(name = "m.identity_server") + val identityServer: WellKnownBaseConfig? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt new file mode 100644 index 0000000000..58c7cf730e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/wellknown/WellknownResult.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown + +/** + * Ref: https://matrix.org/docs/spec/client_server/latest#well-known-uri + */ +sealed class WellknownResult { + /** + * The provided matrixId is no valid. Unable to extract a domain name. + */ + object InvalidMatrixId : WellknownResult() + + /** + * Retrieve the specific piece of information from the user in a way which fits within the existing client user experience, + * if the client is inclined to do so. Failure can take place instead if no good user experience for this is possible at this point. + */ + data class Prompt(val homeServerUrl: String, + val identityServerUrl: String?, + val wellKnown: WellKnown) : WellknownResult() + + /** + * Stop the current auto-discovery mechanism. If no more auto-discovery mechanisms are available, + * then the client may use other methods of determining the required parameters, such as prompting the user, or using default values. + */ + object Ignore : WellknownResult() + + /** + * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. + */ + object FailPrompt : WellknownResult() + + /** + * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. + * At this point, valid data was obtained, but no homeserver is available to serve the client. + * No further guess should be attempted and the user should make a conscientious decision what to do next. + */ + object FailError : WellknownResult() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 453400bc99..1abda8ec05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -61,9 +61,10 @@ interface UserService { /** * Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * @param filter the filter. It will look into userId and displayName. + * @param excludedUserIds userId list which will be excluded from the result list. * @return a Livedata of users */ - fun getPagedUsersLive(filter: String? = null): LiveData> + fun getPagedUsersLive(filter: String? = null, excludedUserIds: Set? = null): LiveData> /** * Get list of ignored users diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 6b6321de36..232cb3f541 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -25,6 +25,10 @@ import im.vector.matrix.android.internal.auth.db.AuthRealmMigration import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore +import im.vector.matrix.android.internal.auth.wellknown.DefaultDirectLoginTask +import im.vector.matrix.android.internal.auth.wellknown.DefaultGetWellknownTask +import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask +import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.di.AuthDatabase import io.realm.RealmConfiguration @@ -59,14 +63,20 @@ internal abstract class AuthModule { } @Binds - abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore + abstract fun bindSessionParamsStore(store: RealmSessionParamsStore): SessionParamsStore @Binds - abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore + abstract fun bindPendingSessionStore(store: RealmPendingSessionStore): PendingSessionStore @Binds - abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService + abstract fun bindAuthenticationService(service: DefaultAuthenticationService): AuthenticationService @Binds - abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator + abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator + + @Binds + abstract fun bindGetWellknownTask(task: DefaultGetWellknownTask): GetWellknownTask + + @Binds + abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 85c2cdbf3d..997cf70e5a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedByS import im.vector.matrix.android.api.auth.data.isSupportedBySdk import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -38,11 +39,16 @@ import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard +import im.vector.matrix.android.internal.auth.wellknown.DirectLoginTask +import im.vector.matrix.android.internal.auth.wellknown.GetWellknownTask import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.launchToCallback import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.toCancelable import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -59,7 +65,10 @@ internal class DefaultAuthenticationService @Inject constructor( private val sessionParamsStore: SessionParamsStore, private val sessionManager: SessionManager, private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + private val pendingSessionStore: PendingSessionStore, + private val getWellknownTask: GetWellknownTask, + private val directLoginTask: DirectLoginTask, + private val taskExecutor: TaskExecutor ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -148,27 +157,71 @@ internal class DefaultAuthenticationService @Inject constructor( val authAPI = buildAuthAPI(homeServerConnectionConfig) // Ok, try to get the config.json file of a RiotWeb client - val riotConfig = executeRequest(null) { - apiCall = authAPI.getRiotConfig() - } - - if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { - // Ok, good sign, we got a default hs url - val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) - ) - - val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) - - val versions = executeRequest(null) { - apiCall = newAuthAPI.versions() + return runCatching { + executeRequest(null) { + apiCall = authAPI.getRiotConfig() } - - return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) - } else { - // Config exists, but there is no default homeserver url (ex: https://riot.im/app) - throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) } + .map { riotConfig -> + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // Try with wellknown + getWellknownLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + + private suspend fun getWellknownLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + val domain = homeServerConnectionConfig.homeServerUri.host + ?: throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + + // Create a fake userId, for the getWellknown task + val fakeUserId = "@alice:$domain" + val wellknownResult = getWellknownTask.execute(GetWellknownTask.Params(fakeUserId)) + + return when (wellknownResult) { + is WellknownResult.Prompt -> { + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(wellknownResult.homeServerUrl), + identityServerUri = wellknownResult.identityServerUrl?.let { Uri.parse(it) } + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest(null) { + apiCall = newAuthAPI.versions() + } + + getLoginFlowResult(newAuthAPI, versions, wellknownResult.homeServerUrl) + } + else -> throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + }.exhaustive } private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { @@ -260,6 +313,26 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getWellKnownData(matrixId: String, callback: MatrixCallback): Cancelable { + return getWellknownTask + .configureWith(GetWellknownTask.Params(matrixId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, + matrixId: String, + password: String, + initialDeviceName: String, + callback: MatrixCallback): Cancelable { + return directLoginTask + .configureWith(DirectLoginTask.Params(homeServerConnectionConfig, matrixId, password, initialDeviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + private suspend fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) { sessionCreator.createSession(credentials, homeServerConnectionConfig) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt index 95a9fbb506..74f7cad67d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt @@ -46,14 +46,14 @@ internal class DefaultSessionCreator @Inject constructor( val sessionParams = SessionParams( credentials = credentials, homeServerConnectionConfig = homeServerConnectionConfig.copy( - homeServerUri = credentials.wellKnown?.homeServer?.baseURL + homeServerUri = credentials.discoveryInformation?.homeServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } ?.also { Timber.d("Overriding homeserver url to $it") } ?.let { Uri.parse(it) } ?: homeServerConnectionConfig.homeServerUri, - identityServerUri = credentials.wellKnown?.identityServer?.baseURL + identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL // remove trailing "/" ?.trim { it == '/' } ?.takeIf { it.isNotBlank() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt new file mode 100644 index 0000000000..01a3ab192d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/DirectLoginTask.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.wellknown + +import dagger.Lazy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.SessionCreator +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import okhttp3.OkHttpClient +import javax.inject.Inject + +internal interface DirectLoginTask : Task { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val userId: String, + val password: String, + val deviceName: String + ) +} + +internal class DefaultDirectLoginTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator +) : DirectLoginTask { + + override suspend fun execute(params: DirectLoginTask.Params): Session { + val authAPI = retrofitFactory.create(okHttpClient, params.homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) + + val credentials = executeRequest(null) { + apiCall = authAPI.login(loginParams) + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt new file mode 100644 index 0000000000..8ed2cb3b0f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/GetWellknownTask.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.wellknown + +import android.util.MalformedJsonException +import dagger.Lazy +import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.auth.data.WellKnown +import im.vector.matrix.android.api.auth.wellknown.WellknownResult +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.di.Unauthenticated +import im.vector.matrix.android.internal.identity.IdentityPingApi +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.isValidUrl +import okhttp3.OkHttpClient +import java.io.EOFException +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection + +internal interface GetWellknownTask : Task { + data class Params( + val matrixId: String + ) +} + +/** + * Inspired from AutoDiscovery class from legacy Matrix Android SDK + */ +internal class DefaultGetWellknownTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy, + private val retrofitFactory: RetrofitFactory +) : GetWellknownTask { + + override suspend fun execute(params: GetWellknownTask.Params): WellknownResult { + if (!MatrixPatterns.isUserId(params.matrixId)) { + return WellknownResult.InvalidMatrixId + } + + val homeServerDomain = params.matrixId.substringAfter(":") + + return findClientConfig(homeServerDomain) + } + + /** + * Find client config + * + * - Do the .well-known request + * - validate homeserver url and identity server url if provide in .well-known result + * - return action and .well-known data + * + * @param domain: homeserver domain, deduced from mx userId (ex: "matrix.org" from userId "@user:matrix.org") + */ + private suspend fun findClientConfig(domain: String): WellknownResult { + val wellKnownAPI = retrofitFactory.create(okHttpClient, "https://dummy.org") + .create(WellKnownAPI::class.java) + + return try { + val wellKnown = executeRequest(null) { + apiCall = wellKnownAPI.getWellKnown(domain) + } + + // Success + val homeServerBaseUrl = wellKnown.homeServer?.baseURL + if (homeServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailPrompt + } else { + if (homeServerBaseUrl.isValidUrl()) { + // Check that HS is a real one + validateHomeServer(homeServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } + } catch (throwable: Throwable) { + when (throwable) { + is Failure.NetworkConnection -> { + WellknownResult.Ignore + } + is Failure.OtherServerError -> { + when (throwable.httpCode) { + HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore + else -> WellknownResult.FailPrompt + } + } + is MalformedJsonException, is EOFException -> { + WellknownResult.FailPrompt + } + else -> { + throw throwable + } + } + } + } + + /** + * Return true if home server is valid, and (if applicable) if identity server is pingable + */ + private suspend fun validateHomeServer(homeServerBaseUrl: String, wellKnown: WellKnown): WellknownResult { + val capabilitiesAPI = retrofitFactory.create(okHttpClient, homeServerBaseUrl) + .create(CapabilitiesAPI::class.java) + + try { + executeRequest(null) { + apiCall = capabilitiesAPI.getVersions() + } + } catch (throwable: Throwable) { + return WellknownResult.FailError + } + + return if (wellKnown.identityServer == null) { + // No identity server + WellknownResult.Prompt(homeServerBaseUrl, null, wellKnown) + } else { + // if m.identity_server is present it must be valid + val identityServerBaseUrl = wellKnown.identityServer.baseURL + if (identityServerBaseUrl.isNullOrBlank()) { + WellknownResult.FailError + } else { + if (identityServerBaseUrl.isValidUrl()) { + if (validateIdentityServer(identityServerBaseUrl)) { + // All is ok + WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) + } else { + WellknownResult.FailError + } + } else { + WellknownResult.FailError + } + } + } + } + + /** + * Return true if identity server is pingable + */ + private suspend fun validateIdentityServer(identityServerBaseUrl: String): Boolean { + val identityPingApi = retrofitFactory.create(okHttpClient, identityServerBaseUrl) + .create(IdentityPingApi::class.java) + + return try { + executeRequest(null) { + apiCall = identityPingApi.ping() + } + + true + } catch (throwable: Throwable) { + false + } + } + + /** + * Try to get an identity server URL from a home server URL, using a .wellknown request + */ + /* + fun getIdentityServer(homeServerUrl: String, callback: ApiCallback) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.identityServer?.baseURL) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + + fun getServerPreferredIntegrationManagers(homeServerUrl: String, callback: ApiCallback>) { + if (homeServerUrl.startsWith("https://")) { + wellKnownRestClient.getWellKnown(homeServerUrl.substring("https://".length), + object : SimpleApiCallback(callback) { + override fun onSuccess(info: WellKnown) { + callback.onSuccess(info.getIntegrationManagers()) + } + }) + } else { + callback.onUnexpectedError(InvalidParameterException("malformed url")) + } + } + */ +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt new file mode 100644 index 0000000000..71928123bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/wellknown/WellKnownAPI.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.auth.wellknown + +import im.vector.matrix.android.api.auth.data.WellKnown +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +internal interface WellKnownAPI { + @GET("https://{domain}/.well-known/matrix/client") + fun getWellKnown(@Path("domain") domain: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 17f049512c..7064663995 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -1406,7 +1406,7 @@ internal class RealmCryptoStore @Inject constructor( } else { // Just override existing, caller should check and untrust id needed val existing = CrossSigningInfoEntity.getOrCreate(realm, userId) - existing.crossSigningKeys.forEach { it.deleteFromRealm() } + existing.crossSigningKeys.deleteAllFromRealm() existing.crossSigningKeys.addAll( info.crossSigningKeys.map { crossSigningKeysMapper.map(it) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt new file mode 100644 index 0000000000..2a0e00704c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/identity/IdentityPingApi.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.identity + +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET + +internal interface IdentityPingApi { + + /** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + * Simple ping call to check if server alive + * + * Ref: https://matrix.org/docs/spec/identity_service/unstable#status-check + * + * @return 200 in case of success + */ + @GET(NetworkConstants.URI_API_PREFIX_IDENTITY) + fun ping(): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt index c6c10d9a8f..ab6745148f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConstants.kt @@ -26,4 +26,10 @@ internal object NetworkConstants { // Media private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media" const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/" + + // Identity server + const val URI_IDENTITY_PATH = "_matrix/identity/api/v1/" + const val URI_IDENTITY_PATH_V2 = "_matrix/identity/v2/" + + const val URI_API_PREFIX_IDENTITY = "_matrix/identity/api/v1" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index 93b3889455..5a8b302f1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -39,6 +39,8 @@ internal class DefaultInviteTask @Inject constructor( return executeRequest(eventBus) { val body = InviteBody(params.userId, params.reason) apiCall = roomAPI.invite(params.roomId, body) + isRetryable = true + maxRetryCount = 3 } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index 761c810b41..7cd2f1b743 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona ) } - override fun getPagedUsersLive(filter: String?): LiveData> { + override fun getPagedUsersLive(filter: String?, excludedUserIds: Set?): LiveData> { realmDataSourceFactory.updateQuery { realm -> val query = realm.where(UserEntity::class.java) if (filter.isNullOrEmpty()) { @@ -104,6 +104,11 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona .contains(UserEntityFields.USER_ID, filter) .endGroup() } + excludedUserIds + ?.takeIf { it.isNotEmpty() } + ?.let { + query.not().`in`(UserEntityFields.USER_ID, it.toTypedArray()) + } query.sort(UserEntityFields.DISPLAY_NAME) } return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt index e57daed617..4adcee88aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/SearchUserAPI.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.session.user -import im.vector.matrix.android.internal.network.NetworkConstants.URI_API_PREFIX_PATH_R0 +import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.user.model.SearchUsersParams import im.vector.matrix.android.internal.session.user.model.SearchUsersResponse import retrofit2.Call @@ -30,6 +30,6 @@ internal interface SearchUserAPI { * * @param searchUsersParams the search params. */ - @POST(URI_API_PREFIX_PATH_R0 + "user_directory/search") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user_directory/search") fun searchUsers(@Body searchUsersParams: SearchUsersParams): Call } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt similarity index 61% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt index eeffc1f119..8ad5e89605 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/UrlUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,15 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.matrix.android.internal.util -import im.vector.riotx.core.platform.VectorSharedAction +import java.net.URL -sealed class CreateDirectRoomSharedAction : VectorSharedAction { - object OpenUsersDirectory : CreateDirectRoomSharedAction() - object Close : CreateDirectRoomSharedAction() - object GoBack : CreateDirectRoomSharedAction() +internal fun String.isValidUrl(): Boolean { + return try { + URL(this) + true + } catch (t: Throwable) { + false + } } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 092817a6cc..ae0ffa1f91 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -85,6 +85,7 @@ + diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 222e25102e..a0d249867f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -23,8 +23,6 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment -import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment -import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment @@ -63,6 +61,8 @@ import im.vector.riotx.features.login.LoginSplashFragment import im.vector.riotx.features.login.LoginWaitForEmailFragment import im.vector.riotx.features.login.LoginWebFragment import im.vector.riotx.features.login.terms.LoginTermsFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.UserDirectoryFragment import im.vector.riotx.features.qrcode.QrCodeScannerFragment import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment @@ -227,13 +227,13 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(CreateDirectRoomDirectoryUsersFragment::class) - fun bindCreateDirectRoomDirectoryUsersFragment(fragment: CreateDirectRoomDirectoryUsersFragment): Fragment + @FragmentKey(UserDirectoryFragment::class) + fun bindUserDirectoryFragment(fragment: UserDirectoryFragment): Fragment @Binds @IntoMap - @FragmentKey(CreateDirectRoomKnownUsersFragment::class) - fun bindCreateDirectRoomKnownUsersFragment(fragment: CreateDirectRoomKnownUsersFragment): Fragment + @FragmentKey(KnownUsersFragment::class) + fun bindKnownUsersFragment(fragment: KnownUsersFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index af49b00b59..c38c0c99e6 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -39,6 +39,7 @@ import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReaction import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.list.RoomListModule import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet +import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.login.LoginActivity @@ -116,6 +117,7 @@ interface ScreenComponent { fun inject(activity: DebugMenuActivity) fun inject(activity: SharedSecureStorageActivity) fun inject(activity: BigImageViewerActivity) + fun inject(activity: InviteUsersToRoomActivity) /* ========================================================================================== * BottomSheets diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 4bb0adb9f0..8046f67668 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -22,7 +22,6 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.core.platform.ConfigurationViewModel -import im.vector.riotx.features.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel @@ -31,10 +30,10 @@ import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel -import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @Module @@ -87,8 +86,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(CreateDirectRoomSharedActionViewModel::class) - fun bindCreateDirectRoomSharedActionViewModel(viewModel: CreateDirectRoomSharedActionViewModel): ViewModel + @ViewModelKey(UserDirectorySharedActionViewModel::class) + fun bindUserDirectorySharedActionViewModel(viewModel: UserDirectorySharedActionViewModel): ViewModel @Binds @IntoMap @@ -110,11 +109,6 @@ interface ViewModelModule { @ViewModelKey(RoomDirectorySharedActionViewModel::class) fun bindRoomDirectorySharedActionViewModel(viewModel: RoomDirectorySharedActionViewModel): ViewModel - @Binds - @IntoMap - @ViewModelKey(LoginSharedActionViewModel::class) - fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(RoomDetailSharedActionViewModel::class) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index 58ec4b22c6..e8e8f21259 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.hideKeyboard +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity.* import javax.inject.Inject @@ -107,4 +108,15 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { } super.onBackPressed() } + + protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + hideWaitingView() + observer(it) + } + .disposeOnDestroy() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt index 0e74ff71fd..f995f82ff7 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -20,10 +20,5 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.core.platform.VectorViewModelAction sealed class CreateDirectRoomAction : VectorViewModelAction { - object CreateRoomAndInviteSelectedUsers : CreateDirectRoomAction() - data class FilterKnownUsers(val value: String) : CreateDirectRoomAction() - data class SearchDirectoryUsers(val value: String) : CreateDirectRoomAction() - object ClearFilterKnownUsers : CreateDirectRoomAction() - data class SelectUser(val user: User) : CreateDirectRoomAction() - data class RemoveSelectedUser(val user: User) : CreateDirectRoomAction() + data class CreateRoomAndInviteSelectedUsers(val selectedUsers: Set) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt index 3ae206cd21..ef3e9bdeff 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt @@ -37,6 +37,12 @@ import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs +import im.vector.riotx.features.userdirectory.UserDirectoryFragment +import im.vector.riotx.features.userdirectory.UserDirectorySharedAction +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectoryViewModel import kotlinx.android.synthetic.main.activity.* import java.net.HttpURLConnection import javax.inject.Inject @@ -44,7 +50,8 @@ import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { private val viewModel: CreateDirectRoomViewModel by viewModel() - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel + @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory @Inject lateinit var createDirectRoomViewModelFactory: CreateDirectRoomViewModel.Factory @Inject lateinit var errorFormatter: ErrorFormatter @@ -56,26 +63,40 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.visibility = View.GONE - sharedActionViewModel = viewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) + sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) sharedActionViewModel .observe() .subscribe { sharedAction -> when (sharedAction) { - CreateDirectRoomSharedAction.OpenUsersDirectory -> - addFragmentToBackstack(R.id.container, CreateDirectRoomDirectoryUsersFragment::class.java) - CreateDirectRoomSharedAction.Close -> finish() - CreateDirectRoomSharedAction.GoBack -> onBackPressed() + UserDirectorySharedAction.OpenUsersDirectory -> + addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) + UserDirectorySharedAction.Close -> finish() + UserDirectorySharedAction.GoBack -> onBackPressed() + is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) } } .disposeOnDestroy() if (isFirstCreation()) { - addFragment(R.id.container, CreateDirectRoomKnownUsersFragment::class.java) + addFragment( + R.id.container, + KnownUsersFragment::class.java, + KnownUsersFragmentArgs( + title = getString(R.string.fab_menu_create_chat), + menuResId = R.menu.vector_create_direct_room + ) + ) } viewModel.selectSubscribe(this, CreateDirectRoomViewState::createAndInviteState) { renderCreateAndInviteState(it) } } + private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { + if (action.itemId == R.id.action_create_direct_room) { + viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(action.selectedUsers)) + } + } + private fun renderCreateAndInviteState(state: Async) { when (state) { is Loading -> renderCreationLoading() diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt index 0ed584ac6b..5ea344115a 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewEvents.kt @@ -18,7 +18,4 @@ package im.vector.riotx.features.createdirect import im.vector.riotx.core.platform.VectorViewEvents -/** - * Transient events for create direct room screen - */ sealed class CreateDirectRoomViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt index 71fae11486..1800759da6 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt @@ -1,42 +1,31 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * Copyright 2019 New Vector Ltd - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.riotx.features.createdirect -import arrow.core.Option import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams -import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.rx.rx -import im.vector.riotx.core.extensions.toggle import im.vector.riotx.core.platform.VectorViewModel -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import java.util.concurrent.TimeUnit - -private typealias KnowUsersFilter = String -private typealias DirectoryUsersSearch = String class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateDirectRoomViewState, @@ -48,9 +37,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel } - private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) - private val directoryUsersSearch = BehaviorRelay.create() - companion object : MvRxViewModelFactory { @JvmStatic @@ -60,25 +46,15 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } - init { - observeKnownUsers() - observeDirectoryUsers() - } - override fun handle(action: CreateDirectRoomAction) { when (action) { - is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers() - is CreateDirectRoomAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) - is CreateDirectRoomAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) - is CreateDirectRoomAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) - is CreateDirectRoomAction.SelectUser -> handleSelectUser(action) - is CreateDirectRoomAction.RemoveSelectedUser -> handleRemoveSelectedUser(action) + is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> createRoomAndInviteSelectedUsers(action.selectedUsers) } } - private fun createRoomAndInviteSelectedUsers() = withState { currentState -> + private fun createRoomAndInviteSelectedUsers(selectedUsers: Set) { val roomParams = CreateRoomParams( - invitedUserIds = currentState.selectedUsers.map { it.userId } + invitedUserIds = selectedUsers.map { it.userId } ) .setDirectMessage() .enableEncryptionIfInvitedUsersSupportIt() @@ -89,52 +65,4 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted copy(createAndInviteState = it) } } - - private fun handleRemoveSelectedUser(action: CreateDirectRoomAction.RemoveSelectedUser) = withState { state -> - val selectedUsers = state.selectedUsers.minus(action.user) - setState { copy(selectedUsers = selectedUsers) } - } - - private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state -> - // Reset the filter asap - directoryUsersSearch.accept("") - val selectedUsers = state.selectedUsers.toggle(action.user) - setState { copy(selectedUsers = selectedUsers) } - } - - private fun observeDirectoryUsers() { - directoryUsersSearch - .debounce(300, TimeUnit.MILLISECONDS) - .switchMapSingle { search -> - val stream = if (search.isBlank()) { - Single.just(emptyList()) - } else { - session.rx() - .searchUsersDirectory(search, 50, emptySet()) - .map { users -> - users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } - } - } - stream.toAsync { - copy(directoryUsers = it, directorySearchTerm = search) - } - } - .subscribe() - .disposeOnClear() - } - - private fun observeKnownUsers() { - knownUsersFilter - .throttleLast(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { - session.rx().livePagedUsers(it.orNull()) - } - .execute { async -> - copy( - knownUsers = async, - filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() - ) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt index dcf86ef6f1..8bb8c3ce58 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt @@ -1,41 +1,25 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * Copyright 2019 New Vector Ltd - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package im.vector.riotx.features.createdirect -import androidx.paging.PagedList -import arrow.core.Option import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.user.model.User data class CreateDirectRoomViewState( - val knownUsers: Async> = Uninitialized, - val directoryUsers: Async> = Uninitialized, - val selectedUsers: Set = emptySet(), - val createAndInviteState: Async = Uninitialized, - val directorySearchTerm: String = "", - val filterKnownUsersValue: Option = Option.empty() -) : MvRxState { - - enum class DisplayMode { - KNOWN_USERS, - DIRECTORY_USERS - } -} + val createAndInviteState: Async = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index 6d85dd8a3e..2edb2c4edf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -17,11 +17,13 @@ package im.vector.riotx.features.home import android.content.Context +import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget @@ -72,6 +74,28 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .into(target) } + @AnyThread + fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { + return glideRequest + .asBitmap() + .apply { + val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) + if (resolvedUrl != null) { + load(resolvedUrl) + } else { + val avatarColor = avatarColor(matrixItem, context) + load(TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) + .toBitmap(width = iconSize, height = iconSize)) + } + } + .submit(iconSize, iconSize) + .get() + } + @AnyThread fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable { return buildGlideRequest(glideRequest, matrixItem.avatarUrl) @@ -82,10 +106,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @AnyThread fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { - val avatarColor = when (matrixItem) { - is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) - else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) - } + val avatarColor = avatarColor(matrixItem, context) return TextDrawable.builder() .beginConfig() .bold() @@ -96,11 +117,21 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active // PRIVATE API ********************************************************************************* private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { - val resolvedUrl = activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() - ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) - + val resolvedUrl = resolvedUrl(avatarUrl) return glideRequest .load(resolvedUrl) .apply(RequestOptions.circleCropTransform()) } + + private fun resolvedUrl(avatarUrl: String?): String? { + return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() + ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + } + + private fun avatarColor(matrixItem: MatrixItem, context: Context): Int { + return when (matrixItem) { + is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) + else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 60c974c291..b6e3cbcd76 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -65,6 +65,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var popupAlertManager: PopupAlertManager + @Inject lateinit var shortcutsHandler: ShortcutsHandler private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -144,6 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { && activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) { promptCompleteSecurityIfNeeded() } + + shortcutsHandler.observeRoomsAndBuildShortcuts() + .disposeOnDestroy() } private fun promptCompleteSecurityIfNeeded() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt new file mode 100644 index 0000000000..657942457e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/ShortcutsHandler.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home + +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.glide.GlideApp +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.home.room.detail.RoomDetailActivity +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +private val useAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O +private const val adaptiveIconSizeDp = 108 +private const val adaptiveIconOuterSidesDp = 18 + +class ShortcutsHandler @Inject constructor( + private val context: Context, + private val homeRoomListStore: HomeRoomListDataSource, + private val avatarRenderer: AvatarRenderer, + private val dimensionConverter: DimensionConverter +) { + private val adaptiveIconSize = dimensionConverter.dpToPx(adaptiveIconSizeDp) + private val adaptiveIconOuterSides = dimensionConverter.dpToPx(adaptiveIconOuterSidesDp) + private val iconSize by lazy { + if (useAdaptiveIcon) { + adaptiveIconSize - adaptiveIconOuterSides + } else { + dimensionConverter.dpToPx(72) + } + } + + fun observeRoomsAndBuildShortcuts(): Disposable { + return homeRoomListStore + .observe() + .distinct() + .observeOn(Schedulers.computation()) + .subscribe { rooms -> + val shortcuts = rooms + .filter { room -> room.tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } } + .take(n = 4) // Android only allows us to create 4 shortcuts + .map { room -> + val intent = RoomDetailActivity.shortcutIntent(context, room.roomId) + val bitmap = avatarRenderer.shortcutDrawable(context, GlideApp.with(context), room.toMatrixItem(), iconSize) + + ShortcutInfoCompat.Builder(context, room.roomId) + .setShortLabel(room.displayName) + .setIcon(bitmap.toProfileImageIcon()) + .setIntent(intent) + .build() + } + + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) + } + } + + // PRIVATE API ********************************************************************************* + + private fun Bitmap.toProfileImageIcon(): IconCompat { + return if (useAdaptiveIcon) { + IconCompat.createWithAdaptiveBitmap(this) + } else { + IconCompat.createWithBitmap(this) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index fe4d0ae1f7..6507bf6030 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -44,8 +44,14 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { super.onCreate(savedInstanceState) waitingView = waiting_view if (isFirstCreation()) { - val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) - ?: return + val roomDetailArgs: RoomDetailArgs? = if (intent?.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { + RoomDetailArgs(roomId = intent?.extras?.getString(EXTRA_ROOM_ID)!!) + } else { + intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) + } + + if (roomDetailArgs == null) return + currentRoomId = roomDetailArgs.roomId replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) @@ -110,11 +116,20 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" + const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" + const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" fun newIntent(context: Context, roomDetailArgs: RoomDetailArgs): Intent { return Intent(context, RoomDetailActivity::class.java).apply { putExtra(EXTRA_ROOM_DETAIL_ARGS, roomDetailArgs) } } + + fun shortcutIntent(context: Context, roomId: String): Intent { + return Intent(context, RoomDetailActivity::class.java).apply { + action = ACTION_ROOM_DETAILS_FROM_SHORTCUT + putExtra(EXTRA_ROOM_ID, roomId) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt new file mode 100644 index 0000000000..8a62935bdd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.invite + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class InviteUsersToRoomAction : VectorViewModelAction { + data class InviteSelectedUsers(val selectedUsers: Set) : InviteUsersToRoomAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt new file mode 100644 index 0000000000..839a0767d8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomActivity.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.invite + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.failure.Failure +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.platform.SimpleFragmentActivity +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.userdirectory.KnownUsersFragment +import im.vector.riotx.features.userdirectory.KnownUsersFragmentArgs +import im.vector.riotx.features.userdirectory.UserDirectoryFragment +import im.vector.riotx.features.userdirectory.UserDirectorySharedAction +import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel +import im.vector.riotx.features.userdirectory.UserDirectoryViewModel +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.activity.* +import java.net.HttpURLConnection +import javax.inject.Inject + +@Parcelize +data class InviteUsersToRoomArgs(val roomId: String) : Parcelable + +class InviteUsersToRoomActivity : SimpleFragmentActivity() { + + private val viewModel: InviteUsersToRoomViewModel by viewModel() + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel + @Inject lateinit var userDirectoryViewModelFactory: UserDirectoryViewModel.Factory + @Inject lateinit var inviteUsersToRoomViewModelFactory: InviteUsersToRoomViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + toolbar.visibility = View.GONE + sharedActionViewModel = viewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + UserDirectorySharedAction.OpenUsersDirectory -> + addFragmentToBackstack(R.id.container, UserDirectoryFragment::class.java) + UserDirectorySharedAction.Close -> finish() + UserDirectorySharedAction.GoBack -> onBackPressed() + is UserDirectorySharedAction.OnMenuItemSelected -> onMenuItemSelected(sharedAction) + } + } + .disposeOnDestroy() + if (isFirstCreation()) { + addFragment( + R.id.container, + KnownUsersFragment::class.java, + KnownUsersFragmentArgs( + title = getString(R.string.invite_users_to_room_title), + menuResId = R.menu.vector_invite_users_to_room, + excludedUserIds = viewModel.getUserIdsOfRoomMembers() + ) + ) + } + + viewModel.observeViewEvents { renderInviteEvents(it) } + } + + private fun onMenuItemSelected(action: UserDirectorySharedAction.OnMenuItemSelected) { + if (action.itemId == R.id.action_invite_users_to_room_invite) { + viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selectedUsers)) + } + } + + private fun renderInviteEvents(viewEvent: InviteUsersToRoomViewEvents) { + when (viewEvent) { + is InviteUsersToRoomViewEvents.Loading -> renderInviteLoading() + is InviteUsersToRoomViewEvents.Success -> renderInvitationSuccess(viewEvent.successMessage) + is InviteUsersToRoomViewEvents.Failure -> renderInviteFailure(viewEvent.throwable) + } + } + + private fun renderInviteLoading() { + updateWaitingView(WaitingViewData(getString(R.string.inviting_users_to_room))) + } + + private fun renderInviteFailure(error: Throwable) { + hideWaitingView() + val message = if (error is Failure.ServerError && error.httpCode == HttpURLConnection.HTTP_INTERNAL_ERROR /*500*/) { + // This error happen if the invited userId does not exist. + getString(R.string.invite_users_to_room_failure) + } else { + errorFormatter.toHumanReadable(error) + } + AlertDialog.Builder(this) + .setMessage(message) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun renderInvitationSuccess(successMessage: String) { + toast(successMessage) + finish() + } + + companion object { + + fun getIntent(context: Context, roomId: String): Intent { + return Intent(context, InviteUsersToRoomActivity::class.java).also { + it.putExtra(MvRx.KEY_ARG, InviteUsersToRoomArgs(roomId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewEvents.kt new file mode 100644 index 0000000000..a76d4a4077 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.invite + +import im.vector.riotx.core.platform.VectorViewEvents + +sealed class InviteUsersToRoomViewEvents : VectorViewEvents { + object Loading : InviteUsersToRoomViewEvents() + data class Failure(val throwable: Throwable) : InviteUsersToRoomViewEvents() + data class Success(val successMessage: String) : InviteUsersToRoomViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt new file mode 100644 index 0000000000..07c0cdbc7d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewModel.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.invite + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.rx.rx +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import io.reactivex.Observable + +class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted + initialState: InviteUsersToRoomViewState, + session: Session, + val stringProvider: StringProvider) + : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + + @AssistedInject.Factory + interface Factory { + fun create(initialState: InviteUsersToRoomViewState): InviteUsersToRoomViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: InviteUsersToRoomViewState): InviteUsersToRoomViewModel? { + val activity: InviteUsersToRoomActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.inviteUsersToRoomViewModelFactory.create(state) + } + } + + override fun handle(action: InviteUsersToRoomAction) { + when (action) { + is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selectedUsers) + } + } + + private fun inviteUsersToRoom(selectedUsers: Set) { + _viewEvents.post(InviteUsersToRoomViewEvents.Loading) + + Observable.fromIterable(selectedUsers).flatMapCompletable { user -> + room.rx().invite(user.userId, null) + }.subscribe( + { + val successMessage = when (selectedUsers.size) { + 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, + selectedUsers.first().displayName) + 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, + selectedUsers.first().displayName, + selectedUsers.last().displayName) + else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, + selectedUsers.size - 1, + selectedUsers.first().displayName, + selectedUsers.size - 1) + } + _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) + }, + { + _viewEvents.post(InviteUsersToRoomViewEvents.Failure(it)) + }) + .disposeOnClear() + } + + fun getUserIdsOfRoomMembers(): Set { + return room.roomSummary()?.otherMemberIds?.toSet() ?: emptySet() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewState.kt new file mode 100644 index 0000000000..e0c3ec24a3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/invite/InviteUsersToRoomViewState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.invite + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class InviteUsersToRoomViewState( + val roomId: String, + val inviteState: Async = Uninitialized +) : MvRxState { + + constructor(args: InviteUsersToRoomArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt index 83263d05a2..8fceaad07f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/AbstractLoginFragment.kt @@ -38,7 +38,6 @@ import javax.net.ssl.HttpsURLConnection abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { protected val loginViewModel: LoginViewModel by activityViewModel() - protected lateinit var loginSharedActionViewModel: LoginSharedActionViewModel private var isResetPasswordStarted = false @@ -57,8 +56,6 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - loginSharedActionViewModel = activityViewModelProvider.get(LoginSharedActionViewModel::class.java) - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt index 90d6754448..3403760136 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginAction.kt @@ -58,4 +58,6 @@ sealed class LoginAction : VectorViewModelAction { // For the soft logout case data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() + + data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index c67e45d9e7..99d8da490d 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -38,6 +38,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.HomeActivity @@ -54,7 +55,6 @@ import javax.inject.Inject open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private val loginViewModel: LoginViewModel by viewModel() - private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel @Inject lateinit var loginViewModelFactory: LoginViewModel.Factory @@ -98,14 +98,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { loginViewModel.handle(LoginAction.InitWith(loginConfig)) } - loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java) - loginSharedActionViewModel - .observe() - .subscribe { - handleLoginNavigation(it) - } - .disposeOnDestroy() - loginViewModel .subscribe(this) { updateWithState(it) @@ -124,65 +116,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java) } - private fun handleLoginNavigation(loginNavigation: LoginNavigation) { - // Assigning to dummy make sure we do not forget a case - @Suppress("UNUSED_VARIABLE") - val dummy = when (loginNavigation) { - is LoginNavigation.OpenServerSelection -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginServerSelectionFragment::class.java, - option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } - // TODO Disabled because it provokes a flickering - // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) - }) - is LoginNavigation.OnServerSelectionDone -> onServerSelectionDone() - is LoginNavigation.OnSignModeSelected -> onSignModeSelected() - is LoginNavigation.OnLoginFlowRetrieved -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginSignUpSignInSelectionFragment::class.java, - option = commonOption) - is LoginNavigation.OnWebLoginError -> onWebLoginError(loginNavigation) - is LoginNavigation.OnForgetPasswordClicked -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordFragment::class.java, - option = commonOption) - is LoginNavigation.OnResetPasswordSendThreePidDone -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordMailConfirmationFragment::class.java, - option = commonOption) - } - is LoginNavigation.OnResetPasswordMailConfirmationSuccess -> { - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginResetPasswordSuccessFragment::class.java, - option = commonOption) - } - is LoginNavigation.OnResetPasswordMailConfirmationSuccessDone -> { - // Go back to the login fragment - supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - } - is LoginNavigation.OnSendEmailSuccess -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginWaitForEmailFragment::class.java, - LoginWaitForEmailFragmentArgument(loginNavigation.email), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - is LoginNavigation.OnSendMsisdnSuccess -> - addFragmentToBackstack(R.id.loginFragmentContainer, - LoginGenericTextInputFormFragment::class.java, - LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginNavigation.msisdn), - tag = FRAGMENT_REGISTRATION_STAGE_TAG, - option = commonOption) - } - } - private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { when (loginViewEvents) { - is LoginViewEvents.RegistrationFlowResult -> { + is LoginViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback @@ -203,15 +139,64 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { } } } - is LoginViewEvents.OutdatedHomeserver -> + is LoginViewEvents.OutdatedHomeserver -> AlertDialog.Builder(this) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_content) .setPositiveButton(R.string.ok, null) .show() - is LoginViewEvents.Failure -> + is LoginViewEvents.Failure -> // This is handled by the Fragments Unit + is LoginViewEvents.OpenServerSelection -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginServerSelectionFragment::class.java, + option = { ft -> + findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // TODO Disabled because it provokes a flickering + // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + }) + is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone() + is LoginViewEvents.OnSignModeSelected -> onSignModeSelected() + is LoginViewEvents.OnLoginFlowRetrieved -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginSignUpSignInSelectionFragment::class.java, + option = commonOption) + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordFragment::class.java, + option = commonOption) + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginResetPasswordSuccessFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents.OnSendEmailSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginWaitForEmailFragment::class.java, + LoginWaitForEmailFragmentArgument(loginViewEvents.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is LoginViewEvents.OnSendMsisdnSuccess -> + addFragmentToBackstack(R.id.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) } } @@ -230,7 +215,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { loginLoading.isVisible = loginViewState.isLoading() } - private fun onWebLoginError(onWebLoginError: LoginNavigation.OnWebLoginError) { + private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { // Pop the backstack supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) @@ -254,11 +239,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private fun onSignModeSelected() = withState(loginViewModel) { state -> when (state.signMode) { - SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> { // This is managed by the LoginViewEvents } - SignMode.SignIn -> { + SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { LoginMode.Unknown -> error("Developer error") @@ -272,7 +257,11 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) } } - } + SignMode.SignInWithMatrixId -> addFragmentToBackstack(R.id.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive } private fun onRegistrationStageNotSupported() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 8b89aeda2a..c2bd02b817 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.isInvalidPassword import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.extensions.toReducedUrl @@ -73,16 +74,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { private fun setupAutoFill(state: LoginViewState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) } - SignMode.SignIn -> { + SignMode.SignIn, + SignMode.SignInWithMatrixId -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) } - } + }.exhaustive } } @@ -116,35 +118,44 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } private fun setupUi(state: LoginViewState) { - val resId = when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_to - SignMode.SignIn -> R.string.login_connect_to - } - loginFieldTil.hint = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_username_hint - SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_username_hint + SignMode.SignIn -> R.string.login_signin_username_hint + SignMode.SignInWithMatrixId -> R.string.login_signin_matrix_id_hint }) - when (state.serverType) { - ServerType.MatrixOrg -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_matrix_org_text) + // Handle direct signin first + if (state.signMode == SignMode.SignInWithMatrixId) { + loginServerIcon.isVisible = false + loginTitle.text = getString(R.string.login_signin_matrix_id_title) + loginNotice.text = getString(R.string.login_signin_matrix_id_notice) + } else { + val resId = when (state.signMode) { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to + SignMode.SignInWithMatrixId -> R.string.login_connect_to } - ServerType.Modular -> { - loginServerIcon.isVisible = true - loginServerIcon.setImageResource(R.drawable.ic_logo_modular) - loginTitle.text = getString(resId, "Modular") - loginNotice.text = getString(R.string.login_server_modular_text) - } - ServerType.Other -> { - loginServerIcon.isVisible = false - loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) - loginNotice.text = getString(R.string.login_server_other_text) + + when (state.serverType) { + ServerType.MatrixOrg -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_matrix_org_text) + } + ServerType.Modular -> { + loginServerIcon.isVisible = true + loginServerIcon.setImageResource(R.drawable.ic_logo_modular) + loginTitle.text = getString(resId, "Modular") + loginNotice.text = getString(R.string.login_server_modular_text) + } + ServerType.Other -> { + loginServerIcon.isVisible = false + loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) + loginNotice.text = getString(R.string.login_server_other_text) + } } } } @@ -153,9 +164,10 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { forgetPasswordButton.isVisible = state.signMode == SignMode.SignIn loginSubmit.text = getString(when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_submit - SignMode.SignIn -> R.string.login_signin + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_submit + SignMode.SignIn, + SignMode.SignInWithMatrixId -> R.string.login_signin }) } @@ -178,7 +190,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { @OnClick(R.id.forgetPasswordButton) fun forgetPasswordClicked() { - loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) } private fun setupPasswordReveal() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt index 3ee1cd6d64..5203e60b26 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginGenericTextInputFormFragment.kt @@ -217,7 +217,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra TextInputFormFragmentMode.SetEmail -> { if (throwable.is401()) { // This is normal use case, we go to the mail waiting screen - loginSharedActionViewModel.post(LoginNavigation.OnSendEmailSuccess(loginViewModel.currentThreePid ?: "")) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendEmailSuccess(loginViewModel.currentThreePid ?: ""))) } else { loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } @@ -225,7 +225,7 @@ class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFra TextInputFormFragmentMode.SetMsisdn -> { if (throwable.is401()) { // This is normal use case, we go to the enter code screen - loginSharedActionViewModel.post(LoginNavigation.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: "")) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSendMsisdnSuccess(loginViewModel.currentThreePid ?: ""))) } else { loginGenericTextInputFormTil.error = errorFormatter.toHumanReadable(throwable) } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt deleted file mode 100644 index 79c6409a3f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginNavigation.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.riotx.features.login - -import im.vector.riotx.core.platform.VectorSharedAction - -// Supported navigation actions for LoginActivity -sealed class LoginNavigation : VectorSharedAction { - object OpenServerSelection : LoginNavigation() - object OnServerSelectionDone : LoginNavigation() - object OnLoginFlowRetrieved : LoginNavigation() - object OnSignModeSelected : LoginNavigation() - object OnForgetPasswordClicked : LoginNavigation() - object OnResetPasswordSendThreePidDone : LoginNavigation() - object OnResetPasswordMailConfirmationSuccess : LoginNavigation() - object OnResetPasswordMailConfirmationSuccessDone : LoginNavigation() - - data class OnSendEmailSuccess(val email: String) : LoginNavigation() - data class OnSendMsisdnSuccess(val msisdn: String) : LoginNavigation() - - data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginNavigation() -} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt index d3a86ef769..d90cfc77e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordFragment.kt @@ -149,7 +149,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment() resetPasswordEmailTil.error = errorFormatter.toHumanReadable(state.asyncResetPassword.error) } is Success -> { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordSendThreePidDone) + Unit } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt index cace48b7f2..f340fb8f9f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordMailConfirmationFragment.kt @@ -64,7 +64,7 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor() : Abstrac .show() } is Success -> { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccess) + Unit } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt index 4faeef1269..fd3c5e6377 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginResetPasswordSuccessFragment.kt @@ -29,7 +29,7 @@ class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFra @OnClick(R.id.resetPasswordSuccessSubmit) fun submit() { - loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt index 9050ea2688..0e234d3da8 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerSelectionFragment.kt @@ -95,10 +95,15 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment // Request login flow here loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url))) } else { - loginSharedActionViewModel.post(LoginNavigation.OnServerSelectionDone) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnServerSelectionDone)) } } + @OnClick(R.id.loginServerIKnowMyIdSubmit) + fun loginWithMatrixId() { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignInWithMatrixId)) + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetHomeServerType) } @@ -108,7 +113,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt index 898ee97656..92dcfcc8aa 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginServerUrlFormFragment.kt @@ -126,7 +126,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginSharedActionViewModel.post(LoginNavigation.OnLoginFlowRetrieved) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt index 9f084299b7..f09053c883 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSignUpSignInSelectionFragment.kt @@ -78,7 +78,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFr @OnClick(R.id.loginSignupSigninSignIn) fun signIn() { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignIn)) - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt index 53de8c2c43..c860d02fec 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSplashFragment.kt @@ -29,7 +29,7 @@ class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() { @OnClick(R.id.loginSplashSubmit) fun getStarted() { - loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OpenServerSelection)) } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt index 25747df3d4..c7c2ee6273 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewEvents.kt @@ -23,10 +23,26 @@ import im.vector.riotx.core.platform.VectorViewEvents /** * Transient events for Login */ -sealed class LoginViewEvents: VectorViewEvents { +sealed class LoginViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : LoginViewEvents() data class Failure(val throwable: Throwable) : LoginViewEvents() data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : LoginViewEvents() object OutdatedHomeserver : LoginViewEvents() + + // Navigation event + + object OpenServerSelection : LoginViewEvents() + object OnServerSelectionDone : LoginViewEvents() + object OnLoginFlowRetrieved : LoginViewEvents() + object OnSignModeSelected : LoginViewEvents() + object OnForgetPasswordClicked : LoginViewEvents() + object OnResetPasswordSendThreePidDone : LoginViewEvents() + object OnResetPasswordMailConfirmationSuccess : LoginViewEvents() + object OnResetPasswordMailConfirmationSuccessDone : LoginViewEvents() + + data class OnSendEmailSuccess(val email: String) : LoginViewEvents() + data class OnSendMsisdnSuccess(val msisdn: String) : LoginViewEvents() + + data class OnWebLoginError(val errorCode: Int, val description: String, val failingUrl: String) : LoginViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index 80b04fe062..81dcfcea9f 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.login import android.content.Context +import android.net.Uri import androidx.fragment.app.FragmentActivity import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail @@ -29,19 +30,24 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.FlowResult import im.vector.matrix.android.api.auth.registration.RegistrationResult import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.registration.Stage +import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.signout.soft.SoftLogoutActivity @@ -51,14 +57,16 @@ import java.util.concurrent.CancellationException /** * */ -class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState, - private val applicationContext: Context, - private val authenticationService: AuthenticationService, - private val activeSessionHolder: ActiveSessionHolder, - private val pushRuleTriggerListener: PushRuleTriggerListener, - private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, - private val sessionListener: SessionListener, - private val reAuthHelper: ReAuthHelper) +class LoginViewModel @AssistedInject constructor( + @Assisted initialState: LoginViewState, + private val applicationContext: Context, + private val authenticationService: AuthenticationService, + private val activeSessionHolder: ActiveSessionHolder, + private val pushRuleTriggerListener: PushRuleTriggerListener, + private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, + private val sessionListener: SessionListener, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -108,7 +116,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi is LoginAction.RegisterAction -> handleRegisterAction(action) is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) - } + is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + }.exhaustive } private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) { @@ -320,11 +329,12 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi ) } - if (action.signMode == SignMode.SignUp) { - startRegistrationFlow() - } else if (action.signMode == SignMode.SignIn) { - startAuthenticationFlow() - } + when (action.signMode) { + SignMode.SignUp -> startRegistrationFlow() + SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected) + SignMode.Unknown -> Unit + }.exhaustive } private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { @@ -365,6 +375,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi resetPasswordEmail = action.email ) } + + _viewEvents.post(LoginViewEvents.OnResetPasswordSendThreePidDone) } override fun onFailure(failure: Throwable) { @@ -405,6 +417,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi resetPasswordEmail = null ) } + + _viewEvents.post(LoginViewEvents.OnResetPasswordMailConfirmationSuccess) } override fun onFailure(failure: Throwable) { @@ -421,10 +435,78 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state -> when (state.signMode) { - SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(action) - else -> error("Developer error, invalid sign mode") + SignMode.Unknown -> error("Developer error, invalid sign mode") + SignMode.SignIn -> handleLogin(action) + SignMode.SignUp -> handleRegisterWith(action) + SignMode.SignInWithMatrixId -> handleDirectLogin(action) + }.exhaustive + } + + private fun handleDirectLogin(action: LoginAction.LoginOrRegister) { + setState { + copy( + asyncLoginAction = Loading() + ) } + + authenticationService.getWellKnownData(action.username, object : MatrixCallback { + override fun onSuccess(data: WellknownResult) { + when (data) { + is WellknownResult.Prompt -> + onWellknownSuccess(action, data) + is WellknownResult.InvalidMatrixId -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)))) + } + else -> { + setState { + copy( + asyncLoginAction = Uninitialized + ) + } + _viewEvents.post(LoginViewEvents.Failure(Exception(stringProvider.getString(R.string.autodiscover_well_known_error)))) + } + }.exhaustive + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) + } + + private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) { + val homeServerConnectionConfig = HomeServerConnectionConfig( + homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl), + identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } + ) + + authenticationService.directAuthentication( + homeServerConnectionConfig, + action.username, + action.password, + action.initialDeviceName, + object : MatrixCallback { + override fun onSuccess(data: Session) { + onSessionCreated(data) + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + }) } private fun handleLogin(action: LoginAction.LoginOrRegister) { @@ -477,6 +559,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun startAuthenticationFlow() { // Ensure Wizard is ready loginWizard + + _viewEvents.post(LoginViewEvents.OnSignModeSelected) } private fun onFlowResponse(flowResult: FlowResult) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt index f9b0b98f29..cf3b39ebb0 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginWebFragment.kt @@ -173,7 +173,7 @@ class LoginWebFragment @Inject constructor( override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { super.onReceivedError(view, errorCode, description, failingUrl) - loginSharedActionViewModel.post(LoginNavigation.OnWebLoginError(errorCode, description, failingUrl)) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnWebLoginError(errorCode, description, failingUrl))) } override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt index b793a0fe1d..ad06f1f4a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/SignMode.kt @@ -21,5 +21,7 @@ enum class SignMode { // Account creation SignUp, // Login - SignIn + SignIn, + // Login directly with matrix Id + SignInWithMatrixId } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2e2814cb78..ac725eb850 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -41,6 +41,7 @@ import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity +import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity @@ -163,6 +164,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(intent) } + override fun openInviteUsersToRoom(context: Context, roomId: String) { + val intent = InviteUsersToRoomActivity.getIntent(context, roomId) + context.startActivity(intent) + } + override fun openRoomsFiltering(context: Context) { val intent = FilteredRoomsActivity.newIntent(context) context.startActivity(intent) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index bf99643912..cc8e7cac34 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -46,6 +46,8 @@ interface Navigator { fun openCreateDirectRoom(context: Context) + fun openInviteUsersToRoom(context: Context, roomId: String) + fun openRoomDirectory(context: Context, initialFilter: String = "") fun openRoomsFiltering(context: Context) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index ae8e7f23fc..6fc396b264 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -348,12 +348,19 @@ class NotificationDrawerManager @Inject constructor(private val context: Context globalLastMessageTimestamp = lastMessageTimestamp } + val tickerText = if (roomEventGroupInfo.isDirect) { + stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description) + } else { + stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) + } + val notification = notificationUtils.buildMessagesListNotification( style, roomEventGroupInfo, largeBitmap, lastMessageTimestamp, - myUserDisplayName) + myUserDisplayName, + tickerText) // is there an id for this room? notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt index 50fb5b70de..178235ab5f 100755 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationUtils.kt @@ -381,7 +381,8 @@ class NotificationUtils @Inject constructor(private val context: Context, roomInfo: RoomEventGroupInfo, largeIcon: Bitmap?, lastMessageTimestamp: Long, - senderDisplayNameForReplyCompat: String?): Notification { + senderDisplayNameForReplyCompat: String?, + tickerText: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) // Build the pending intent for when the notification is clicked val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) @@ -478,6 +479,7 @@ class NotificationUtils @Inject constructor(private val context: Context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) setDeleteIntent(pendingIntent) } + .setTicker(tickerText) .build() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt index e6e54d6771..8a08cbae8a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.roomprofile.members import android.os.Bundle +import android.view.MenuItem import android.view.View import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel @@ -43,6 +44,18 @@ class RoomMemberListFragment @Inject constructor( override fun getLayoutResId() = R.layout.fragment_room_setting_generic + override fun getMenuRes() = R.menu.menu_room_member_list + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_room_member_list_add_member -> { + navigator.openInviteUsersToRoom(requireContext(), roomProfileArgs.roomId) + return true + } + } + return super.onOptionsItemSelected(item) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index cd5e53b7c3..d0369e7707 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -30,7 +30,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback -import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod @@ -48,6 +47,7 @@ import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.launch +import timber.log.Timber import java.util.concurrent.TimeUnit data class DevicesViewState( @@ -65,6 +65,7 @@ data class DeviceFullInfo( val deviceInfo: DeviceInfo, val cryptoDeviceInfo: CryptoDeviceInfo? ) + class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, private val session: Session @@ -215,8 +216,13 @@ class DevicesViewModel @AssistedInject constructor( private fun handleVerifyManually(action: DevicesAction.MarkAsManuallyVerified) = withState { state -> viewModelScope.launch { if (state.hasAccountCrossSigning) { - awaitCallback { - tryThis { session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) } + try { + awaitCallback { + session.cryptoService().crossSigningService().trustDevice(action.cryptoDeviceInfo.deviceId, it) + } + } catch (failure: Throwable) { + Timber.e("Failed to manually cross sign device ${action.cryptoDeviceInfo.deviceId} : ${failure.localizedMessage}") + _viewEvents.post(DevicesViewEvents.Failure(failure)) } } else { // legacy diff --git a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt index d3288c5b2e..13b90f26e8 100644 --- a/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/signout/soft/SoftLogoutFragment.kt @@ -30,7 +30,7 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginMode -import im.vector.riotx.features.login.LoginNavigation +import im.vector.riotx.features.login.LoginViewEvents import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject @@ -94,7 +94,7 @@ class SoftLogoutFragment @Inject constructor( } override fun signinFallbackSubmit() { - loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected)) } override fun clearData() { @@ -124,7 +124,7 @@ class SoftLogoutFragment @Inject constructor( } override fun forgetPasswordClicked() { - loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnForgetPasswordClicked)) } override fun revealPasswordClicked() { diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt similarity index 83% rename from vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt index 1c38e6f723..9d11387fe8 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/DirectoryUsersController.kt @@ -1,22 +1,20 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * Copyright 2019 New Vector Ltd - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Fail @@ -41,7 +39,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, private val stringProvider: StringProvider, private val errorFormatter: ErrorFormatter) : EpoxyController() { - private var state: CreateDirectRoomViewState? = null + private var state: UserDirectoryViewState? = null var callback: Callback? = null @@ -49,7 +47,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, requestModelBuild() } - fun setData(state: CreateDirectRoomViewState) { + fun setData(state: UserDirectoryViewState) { this.state = state requestModelBuild() } @@ -110,7 +108,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session, continue } val isSelected = selectedUsers.contains(user.userId) - createDirectRoomUserItem { + userDirectoryUserItem { id(user.userId) selected(isSelected) matrixItem(user.toMatrixItem()) diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt similarity index 92% rename from vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt index a0e20b45f5..7a1ad49b8c 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersController.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController @@ -49,7 +49,7 @@ class KnownUsersController @Inject constructor(private val session: Session, requestModelBuild() } - fun setData(state: CreateDirectRoomViewState) { + fun setData(state: UserDirectoryViewState) { this.isFiltering = !state.filterKnownUsersValue.isEmpty() val newSelection = state.selectedUsers.map { it.userId } this.users = state.knownUsers @@ -65,7 +65,7 @@ class KnownUsersController @Inject constructor(private val session: Session, EmptyItem_().id(currentPosition) } else { val isSelected = selectedUsers.contains(item.userId) - CreateDirectRoomUserItem_() + UserDirectoryUserItem_() .id(item.userId) .selected(isSelected) .matrixItem(item.toMatrixItem()) @@ -84,13 +84,13 @@ class KnownUsersController @Inject constructor(private val session: Session, } else { var lastFirstLetter: String? = null for (model in models) { - if (model is CreateDirectRoomUserItem) { + if (model is UserDirectoryUserItem) { if (model.matrixItem.id == session.myUserId) continue val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName() val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter lastFirstLetter = currentFirstLetter - CreateDirectRoomLetterHeaderItem_() + UserDirectoryLetterHeaderItem_() .id(currentFirstLetter) .letter(currentFirstLetter) .addIf(showLetter, this) diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt similarity index 60% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt index 24b5394e5c..78482e0b54 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragment.kt @@ -1,29 +1,29 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * Copyright 2019 New Vector Ltd - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.ScrollView +import androidx.core.view.forEach import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args import com.airbnb.mvrx.withState import com.google.android.material.chip.Chip import com.jakewharton.rxbinding3.widget.textChanges @@ -35,30 +35,36 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter -import kotlinx.android.synthetic.main.fragment_create_direct_room.* +import kotlinx.android.synthetic.main.fragment_known_users.* import javax.inject.Inject -class CreateDirectRoomKnownUsersFragment @Inject constructor( +class KnownUsersFragment @Inject constructor( + val userDirectoryViewModelFactory: UserDirectoryViewModel.Factory, private val knownUsersController: KnownUsersController, private val dimensionConverter: DimensionConverter ) : VectorBaseFragment(), KnownUsersController.Callback { - override fun getLayoutResId() = R.layout.fragment_create_direct_room + private val args: KnownUsersFragmentArgs by args() - override fun getMenuRes() = R.menu.vector_create_direct_room + override fun getLayoutResId() = R.layout.fragment_known_users - private val viewModel: CreateDirectRoomViewModel by activityViewModel() - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + override fun getMenuRes() = args.menuResId + + private val viewModel: UserDirectoryViewModel by activityViewModel() + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) - vectorBaseActivity.setSupportActionBar(createDirectRoomToolbar) + sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) + + knownUsersTitle.text = args.title + + vectorBaseActivity.setSupportActionBar(knownUsersToolbar) setupRecyclerView() setupFilterView() setupAddByMatrixIdView() setupCloseView() - viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { + viewModel.selectSubscribe(this, UserDirectoryViewState::selectedUsers) { renderSelectedUsers(it) } } @@ -71,27 +77,22 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( override fun onPrepareOptionsMenu(menu: Menu) { withState(viewModel) { - val createMenuItem = menu.findItem(R.id.action_create_direct_room) val showMenuItem = it.selectedUsers.isNotEmpty() - createMenuItem.setVisible(showMenuItem) + menu.forEach { menuItem -> + menuItem.isVisible = showMenuItem + } } super.onPrepareOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_create_direct_room -> { - viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) - true - } - else -> - super.onOptionsItemSelected(item) - } + override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { + sharedActionViewModel.post(UserDirectorySharedAction.OnMenuItemSelected(item.itemId, it.selectedUsers)) + return@withState true } private fun setupAddByMatrixIdView() { addByMatrixId.setOnClickListener { - sharedActionViewModel.post(CreateDirectRoomSharedAction.OpenUsersDirectory) + sharedActionViewModel.post(UserDirectorySharedAction.OpenUsersDirectory) } } @@ -102,26 +103,26 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( } private fun setupFilterView() { - createDirectRoomFilter + knownUsersFilter .textChanges() - .startWith(createDirectRoomFilter.text) + .startWith(knownUsersFilter.text) .subscribe { text -> val filterValue = text.trim() val action = if (filterValue.isBlank()) { - CreateDirectRoomAction.ClearFilterKnownUsers + UserDirectoryAction.ClearFilterKnownUsers } else { - CreateDirectRoomAction.FilterKnownUsers(filterValue.toString()) + UserDirectoryAction.FilterKnownUsers(filterValue.toString()) } viewModel.handle(action) } .disposeOnDestroyView() - createDirectRoomFilter.setupAsSearch() - createDirectRoomFilter.requestFocus() + knownUsersFilter.setupAsSearch() + knownUsersFilter.requestFocus() } private fun setupCloseView() { - createDirectRoomClose.setOnClickListener { + knownUsersClose.setOnClickListener { requireActivity().finish() } } @@ -157,12 +158,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( chip.isCloseIconVisible = true chipGroup.addView(chip) chip.setOnCloseIconClickListener { - viewModel.handle(CreateDirectRoomAction.RemoveSelectedUser(user)) + viewModel.handle(UserDirectoryAction.RemoveSelectedUser(user)) } } override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(CreateDirectRoomAction.SelectUser(user)) + viewModel.handle(UserDirectoryAction.SelectUser(user)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragmentArgs.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragmentArgs.kt new file mode 100644 index 0000000000..9e87633608 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/KnownUsersFragmentArgs.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.userdirectory + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class KnownUsersFragmentArgs( + val title: String, + val menuResId: Int, + val excludedUserIds: Set? = null +) : Parcelable diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt new file mode 100644 index 0000000000..1df3c02736 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryAction.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.userdirectory + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class UserDirectoryAction : VectorViewModelAction { + data class FilterKnownUsers(val value: String) : UserDirectoryAction() + data class SearchDirectoryUsers(val value: String) : UserDirectoryAction() + object ClearFilterKnownUsers : UserDirectoryAction() + data class SelectUser(val user: User) : UserDirectoryAction() + data class RemoveSelectedUser(val user: User) : UserDirectoryAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt similarity index 64% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt index ecfe054767..28aa2d433b 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryFragment.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import android.os.Bundle import android.view.View @@ -29,22 +29,22 @@ import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setupAsSearch import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.platform.VectorBaseFragment -import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* +import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.recyclerView +import kotlinx.android.synthetic.main.fragment_user_directory.* import javax.inject.Inject -class CreateDirectRoomDirectoryUsersFragment @Inject constructor( +class UserDirectoryFragment @Inject constructor( private val directRoomController: DirectoryUsersController ) : VectorBaseFragment(), DirectoryUsersController.Callback { - override fun getLayoutResId() = R.layout.fragment_create_direct_room_directory_users + override fun getLayoutResId() = R.layout.fragment_user_directory + private val viewModel: UserDirectoryViewModel by activityViewModel() - private val viewModel: CreateDirectRoomViewModel by activityViewModel() - - private lateinit var sharedActionViewModel: CreateDirectRoomSharedActionViewModel + private lateinit var sharedActionViewModel: UserDirectorySharedActionViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(CreateDirectRoomSharedActionViewModel::class.java) + sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java) setupRecyclerView() setupSearchByMatrixIdView() setupCloseView() @@ -62,19 +62,19 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( } private fun setupSearchByMatrixIdView() { - createDirectRoomSearchById.setupAsSearch(searchIconRes = 0) - createDirectRoomSearchById + userDirectorySearchById.setupAsSearch(searchIconRes = 0) + userDirectorySearchById .textChanges() .subscribe { - viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(it.toString())) + viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(it.toString())) } .disposeOnDestroyView() - createDirectRoomSearchById.showKeyboard(andRequestFocus = true) + userDirectorySearchById.showKeyboard(andRequestFocus = true) } private fun setupCloseView() { - createDirectRoomClose.setOnClickListener { - sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) + userDirectoryClose.setOnClickListener { + sharedActionViewModel.post(UserDirectorySharedAction.GoBack) } } @@ -84,12 +84,12 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(CreateDirectRoomAction.SelectUser(user)) - sharedActionViewModel.post(CreateDirectRoomSharedAction.GoBack) + viewModel.handle(UserDirectoryAction.SelectUser(user)) + sharedActionViewModel.post(UserDirectorySharedAction.GoBack) } override fun retryDirectoryUsersRequest() { - val currentSearch = createDirectRoomSearchById.text.toString() - viewModel.handle(CreateDirectRoomAction.SearchDirectoryUsers(currentSearch)) + val currentSearch = userDirectorySearchById.text.toString() + viewModel.handle(UserDirectoryAction.SearchDirectoryUsers(currentSearch)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryLetterHeaderItem.kt similarity index 70% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryLetterHeaderItem.kt index e512337c64..e7e9183ada 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryLetterHeaderItem.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -23,8 +23,8 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_create_direct_room_letter_header) -abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_user_directory_letter_header) +abstract class UserDirectoryLetterHeaderItem : VectorEpoxyModel() { @EpoxyAttribute var letter: String = "" @@ -33,6 +33,6 @@ abstract class CreateDirectRoomLetterHeaderItem : VectorEpoxyModel(R.id.createDirectRoomLetterView) + val letterView by bind(R.id.userDirectoryLetterView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt new file mode 100644 index 0000000000..7d1987aa4b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedAction.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.userdirectory + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotx.core.platform.VectorSharedAction + +sealed class UserDirectorySharedAction : VectorSharedAction { + object OpenUsersDirectory : UserDirectorySharedAction() + object Close : UserDirectorySharedAction() + object GoBack : UserDirectorySharedAction() + data class OnMenuItemSelected(val itemId: Int, val selectedUsers: Set) : UserDirectorySharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedActionViewModel.kt similarity index 76% rename from vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedActionViewModel.kt index 625208b682..e7081ea969 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectorySharedActionViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ * limitations under the License. */ -package im.vector.riotx.features.login +package im.vector.riotx.features.userdirectory import im.vector.riotx.core.platform.VectorSharedActionViewModel import javax.inject.Inject -class LoginSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +class UserDirectorySharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryUserItem.kt similarity index 63% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryUserItem.kt index f2f517fd6e..7ea0709ce8 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryUserItem.kt @@ -1,22 +1,20 @@ /* + * Copyright (c) 2020 New Vector Ltd * - * * Copyright 2019 New Vector Ltd - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory import android.view.View import android.widget.ImageView @@ -31,8 +29,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.features.home.AvatarRenderer -@EpoxyModelClass(layout = R.layout.item_create_direct_room_user) -abstract class CreateDirectRoomUserItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_known_user) +abstract class UserDirectoryUserItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var matrixItem: MatrixItem @@ -66,9 +64,9 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel(R.id.createDirectRoomUserID) - val nameView by bind(R.id.createDirectRoomUserName) - val avatarImageView by bind(R.id.createDirectRoomUserAvatar) - val avatarCheckedImageView by bind(R.id.createDirectRoomUserAvatarChecked) + val userIdView by bind(R.id.knownUserID) + val nameView by bind(R.id.knownUserName) + val avatarImageView by bind(R.id.knownUserAvatar) + val avatarCheckedImageView by bind(R.id.knownUserAvatarChecked) } } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewEvents.kt similarity index 60% rename from vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewEvents.kt index 91c21378d2..435fce8b16 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewEvents.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2020 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,9 +14,11 @@ * limitations under the License. */ -package im.vector.riotx.features.createdirect +package im.vector.riotx.features.userdirectory -import im.vector.riotx.core.platform.VectorSharedActionViewModel -import javax.inject.Inject +import im.vector.riotx.core.platform.VectorViewEvents -class CreateDirectRoomSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() +/** + * Transient events for invite users to room screen + */ +sealed class UserDirectoryViewEvents : VectorViewEvents diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt new file mode 100644 index 0000000000..3111a86bf7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewModel.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.userdirectory + +import androidx.fragment.app.FragmentActivity +import arrow.core.Option +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.toggle +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.createdirect.CreateDirectRoomActivity +import im.vector.riotx.features.invite.InviteUsersToRoomActivity +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import java.util.concurrent.TimeUnit + +private typealias KnowUsersFilter = String +private typealias DirectoryUsersSearch = String + +class UserDirectoryViewModel @AssistedInject constructor(@Assisted + initialState: UserDirectoryViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: UserDirectoryViewState): UserDirectoryViewModel + } + + private val knownUsersFilter = BehaviorRelay.createDefault>(Option.empty()) + private val directoryUsersSearch = BehaviorRelay.create() + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: UserDirectoryViewState): UserDirectoryViewModel? { + return when (viewModelContext) { + is FragmentViewModelContext -> (viewModelContext.fragment() as KnownUsersFragment).userDirectoryViewModelFactory.create(state) + is ActivityViewModelContext -> { + when (viewModelContext.activity()) { + is CreateDirectRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state) + is InviteUsersToRoomActivity -> viewModelContext.activity().userDirectoryViewModelFactory.create(state) + else -> error("Wrong activity or fragment") + } + } + else -> error("Wrong activity or fragment") + } + } + } + + init { + observeKnownUsers() + observeDirectoryUsers() + } + + override fun handle(action: UserDirectoryAction) { + when (action) { + is UserDirectoryAction.FilterKnownUsers -> knownUsersFilter.accept(Option.just(action.value)) + is UserDirectoryAction.ClearFilterKnownUsers -> knownUsersFilter.accept(Option.empty()) + is UserDirectoryAction.SearchDirectoryUsers -> directoryUsersSearch.accept(action.value) + is UserDirectoryAction.SelectUser -> handleSelectUser(action) + is UserDirectoryAction.RemoveSelectedUser -> handleRemoveSelectedUser(action) + } + } + + private fun handleRemoveSelectedUser(action: UserDirectoryAction.RemoveSelectedUser) = withState { state -> + val selectedUsers = state.selectedUsers.minus(action.user) + setState { copy(selectedUsers = selectedUsers) } + } + + private fun handleSelectUser(action: UserDirectoryAction.SelectUser) = withState { state -> + // Reset the filter asap + directoryUsersSearch.accept("") + val selectedUsers = state.selectedUsers.toggle(action.user) + setState { copy(selectedUsers = selectedUsers) } + } + + private fun observeDirectoryUsers() = withState { state -> + directoryUsersSearch + .debounce(300, TimeUnit.MILLISECONDS) + .switchMapSingle { search -> + val stream = if (search.isBlank()) { + Single.just(emptyList()) + } else { + session.rx() + .searchUsersDirectory(search, 50, state.excludedUserIds ?: emptySet()) + .map { users -> + users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() } + } + } + stream.toAsync { + copy(directoryUsers = it, directorySearchTerm = search) + } + } + .subscribe() + .disposeOnClear() + } + + private fun observeKnownUsers() = withState { state -> + knownUsersFilter + .throttleLast(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { + session.rx().livePagedUsers(it.orNull(), state.excludedUserIds) + } + .execute { async -> + copy( + knownUsers = async, + filterKnownUsersValue = knownUsersFilter.value ?: Option.empty() + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt new file mode 100644 index 0000000000..52f92a9994 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/userdirectory/UserDirectoryViewState.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.userdirectory + +import androidx.paging.PagedList +import arrow.core.Option +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.user.model.User + +data class UserDirectoryViewState( + val excludedUserIds: Set? = null, + val knownUsers: Async> = Uninitialized, + val directoryUsers: Async> = Uninitialized, + val selectedUsers: Set = emptySet(), + val createAndInviteState: Async = Uninitialized, + val directorySearchTerm: String = "", + val filterKnownUsersValue: Option = Option.empty() +) : MvRxState { + + constructor(args: KnownUsersFragmentArgs) : this(excludedUserIds = args.excludedUserIds) +} diff --git a/vector/src/main/java/im/vector/riotx/features/widgets/RoomWidgetFragment.kt b/vector/src/main/java/im/vector/riotx/features/widgets/RoomWidgetFragment.kt index 47f42c10a8..f4fef48abb 100644 --- a/vector/src/main/java/im/vector/riotx/features/widgets/RoomWidgetFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/widgets/RoomWidgetFragment.kt @@ -38,7 +38,7 @@ import im.vector.riotx.features.webview.WebViewEventListener import im.vector.riotx.features.widgets.webview.clearAfterWidget import im.vector.riotx.features.widgets.webview.setupForWidget import kotlinx.android.parcel.Parcelize -import kotlinx.android.synthetic.main.fragment_widget.* +import kotlinx.android.synthetic.main.fragment_room_widget.* import timber.log.Timber import javax.inject.Inject @@ -54,7 +54,7 @@ class WidgetFragment @Inject constructor( private val fragmentArgs: WidgetArgs by args() private val viewModel: RoomWidgetViewModel by fragmentViewModel() - override fun getLayoutResId() = R.layout.fragment_widget + override fun getLayoutResId() = R.layout.fragment_room_widget override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/res/drawable/ic_invite_users.xml b/vector/src/main/res/drawable/ic_invite_users.xml new file mode 100644 index 0000000000..64e5a3788d --- /dev/null +++ b/vector/src/main/res/drawable/ic_invite_users.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/vector/src/main/res/layout/fragment_known_users.xml b/vector/src/main/res/layout/fragment_known_users.xml new file mode 100644 index 0000000000..915d27bdf7 --- /dev/null +++ b/vector/src/main/res/layout/fragment_known_users.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_selection.xml b/vector/src/main/res/layout/fragment_login_server_selection.xml index c97b32bd21..ba74ce26f8 100644 --- a/vector/src/main/res/layout/fragment_login_server_selection.xml +++ b/vector/src/main/res/layout/fragment_login_server_selection.xml @@ -16,7 +16,9 @@ style="@style/LoginFormScrollView" tools:ignore="MissingConstraints"> - + + + + + diff --git a/vector/src/main/res/layout/fragment_user_directory.xml b/vector/src/main/res/layout/fragment_user_directory.xml new file mode 100644 index 0000000000..e10f5bcaa9 --- /dev/null +++ b/vector/src/main/res/layout/fragment_user_directory.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_known_user.xml b/vector/src/main/res/layout/item_known_user.xml new file mode 100644 index 0000000000..e90b2c6256 --- /dev/null +++ b/vector/src/main/res/layout/item_known_user.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_user_directory_letter_header.xml b/vector/src/main/res/layout/item_user_directory_letter_header.xml new file mode 100644 index 0000000000..0cb2faf9bc --- /dev/null +++ b/vector/src/main/res/layout/item_user_directory_letter_header.xml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_room_member_list.xml b/vector/src/main/res/menu/menu_room_member_list.xml new file mode 100644 index 0000000000..ef452de70f --- /dev/null +++ b/vector/src/main/res/menu/menu_room_member_list.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/vector_invite_users_to_room.xml b/vector/src/main/res/menu/vector_invite_users_to_room.xml new file mode 100755 index 0000000000..2e799b5c03 --- /dev/null +++ b/vector/src/main/res/menu/vector_invite_users_to_room.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ccf4dfee3d..ff821f5b95 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1089,6 +1089,8 @@ New Invitation Me ** Failed to send - please open room + %1$s: %2$s + %1$s: %2$s %3$s Search for historical @@ -2369,4 +2371,17 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming The link %1$s is taking you to another site: %2$s.\n\nAre you sure you want to continue? "We couldn't create your DM. Please check the users you want to invite and try again." - + + Add members + INVITE + Inviting users… + Invite Users + Invitation sent to %1$s + Invitations sent to %1$s and %2$s + + Invitations sent to %1$s and one more + Invitations sent to %1$s and %2$d more + + We could not invite users. Please check the users you want to invite and try again. + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 585102d46a..dd1043819d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -57,4 +57,12 @@ + Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method: + Sign in with my Matrix identifier + Sign in + Enter your identifier and your password + User identifier + This is not a valid user identifier. Expected format: \'@user:homeserver.org\' + Unable to find a valid homeserver. Please check your identifier +