From bd9da8eaa6dddf74e23f678e01539c01955d462b Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 11 Dec 2020 15:06:22 +0100 Subject: [PATCH] element:// support + basic peeking + fix join via server --- .../org/matrix/android/sdk/rx/RxSession.kt | 3 +- .../session/permalinks/PermalinkService.kt | 1 + .../sdk/api/session/room/RoomService.kt | 14 +- .../session/room/DefaultRoomService.kt | 25 ++- .../sdk/internal/session/room/RoomAPI.kt | 3 + .../sdk/internal/session/room/RoomModule.kt | 10 + .../room/alias/GetRoomIdByAliasTask.kt | 14 +- .../room/alias/RoomAliasDescription.kt | 2 +- .../session/room/peeking/PeekRoomTask.kt | 173 ++++++++++++++++++ .../room/peeking/ResolveRoomStateTask.kt | 42 +++++ vector/src/main/AndroidManifest.xml | 5 + .../vector/app/features/home/HomeActivity.kt | 34 +++- .../features/permalink/PermalinkHandler.kt | 41 +++-- .../app/features/popup/PopupAlertManager.kt | 2 +- .../roompreview/RoomPreviewActivity.kt | 1 + .../RoomPreviewNoPreviewFragment.kt | 76 ++++++-- .../roompreview/RoomPreviewViewModel.kt | 56 +++++- .../roompreview/RoomPreviewViewState.kt | 28 ++- .../fragment_room_preview_no_preview.xml | 10 +- vector/src/main/res/values/strings.xml | 3 +- 20 files changed, 488 insertions(+), 55 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 0e5b88adb2..a7b269fcc6 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription class RxSession(private val session: Session) { @@ -139,7 +140,7 @@ class RxSession(private val session: Session) { } fun getRoomIdByAlias(roomAlias: String, - searchOnServer: Boolean): Single> = singleBuilder { + searchOnServer: Boolean): Single> = singleBuilder { session.getRoomIdByAlias(roomAlias, searchOnServer, it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index ac1d726d03..aefc086b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -25,6 +25,7 @@ interface PermalinkService { companion object { const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" + const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 477bef66cf..441a64e2c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription +import org.matrix.android.sdk.internal.session.room.peeking.PeekResult /** * This interface defines methods to get rooms. It's implemented at the session level. @@ -120,7 +123,7 @@ interface RoomService { */ fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, - callback: MatrixCallback>): Cancelable + callback: MatrixCallback>): Cancelable /** * Delete a room alias @@ -163,4 +166,13 @@ interface RoomService { * @return a LiveData of the optional found room member */ fun getRoomMemberLive(userId: String, roomId: String): LiveData> + + fun getRoomState(roomId: String, callback: MatrixCallback>) + + /** + * Use this if you want to get information from a room that you are not yet in (or invited) + * It might be possible to get some information on this room if it is public or if guest access is allowed + * This call will try to gather some information on this room, but it could fail and get nothing more + */ + fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 9ec985e0b6..e540105800 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams @@ -35,10 +36,14 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.PeekResult +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask @@ -55,6 +60,8 @@ internal class DefaultRoomService @Inject constructor( private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val roomIdByAliasTask: GetRoomIdByAliasTask, private val deleteRoomAliasTask: DeleteRoomAliasTask, + private val resolveRoomStateTask: ResolveRoomStateTask, + private val peekRoomTask: PeekRoomTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor( .executeBy(taskExecutor) } - override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { + override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { return roomIdByAliasTask .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { this.callback = callback @@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor( results.firstOrNull().toOptional() } } + + override fun getRoomState(roomId: String, callback: MatrixCallback>) { + resolveRoomStateTask + .configureWith(ResolveRoomStateTask.Params(roomId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) { + peekRoomTask + .configureWith(PeekRoomTask.Params(roomIdOrAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 955a251b52..f335987085 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -354,4 +354,7 @@ internal interface RoomAPI { fun deleteTag(@Path("userId") userId: String, @Path("roomId") roomId: String, @Path("tag") tag: String): Call + + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") + fun getRoomState(@Path("roomId") roomId: String) : Call> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 3a94396a61..92f4ea2aea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -57,6 +57,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLe import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask +import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask @@ -223,4 +227,10 @@ internal abstract class RoomModule { @Binds abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask + + @Binds + abstract fun bindResolveRoomStateTask(task: DefaultResolveRoomStateTask): ResolveRoomStateTask + + @Binds + abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt index 3c47ee6ef0..2fe290ead2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -29,7 +29,7 @@ import org.matrix.android.sdk.internal.session.directory.DirectoryAPI import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface GetRoomIdByAliasTask : Task> { +internal interface GetRoomIdByAliasTask : Task> { data class Params( val roomAlias: String, val searchOnServer: Boolean @@ -42,21 +42,21 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor( private val eventBus: EventBus ) : GetRoomIdByAliasTask { - override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { + override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { var roomId = Realm.getInstance(monarchy.realmConfiguration).use { RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId } return if (roomId != null) { - Optional.from(roomId) + Optional.from(RoomAliasDescription(roomId)) } else if (!params.searchOnServer) { - Optional.from(null) + Optional.from(null) } else { - roomId = tryOrNull("## Failed to get roomId from alias") { + val description = tryOrNull("## Failed to get roomId from alias") { executeRequest(eventBus) { apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias) } - }?.roomId - Optional.from(roomId) + } + Optional.from(description) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt index ada3839fa0..d1f93c50be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class RoomAliasDescription( +data class RoomAliasDescription( /** * The room ID for this alias. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt new file mode 100644 index 0000000000..ae557adb5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -0,0 +1,173 @@ +/* + * 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 org.matrix.android.sdk.internal.session.room.peeking + +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomTopicContent +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +sealed class PeekResult { + data class Success( + val roomId: String, + val alias: String?, + val name: String?, + val topic: String?, + val avatarUrl: String?, + val numJoinedMembers: Int?, + val viaServers: List + ) : PeekResult() + + data class PeekingNotAllowed( + val roomId: String, + val alias: String?, + val viaServers: List + ) : PeekResult() + + object UnknownAlias : PeekResult() +} + +internal interface PeekRoomTask : Task { + data class Params( + val roomIdOrAlias: String + ) +} + +internal class DefaultPeekRoomTask @Inject constructor( + private val getRoomIdByAliasTask: GetRoomIdByAliasTask, + private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, + private val getPublicRoomTask: GetPublicRoomTask, + private val resolveRoomStateTask: ResolveRoomStateTask +) : PeekRoomTask { + + override suspend fun execute(params: PeekRoomTask.Params): PeekResult { + val roomId: String? + val serverList: List + val isAlias: Boolean + if (MatrixPatterns.isRoomAlias(params.roomIdOrAlias)) { + isAlias = true + // get alias description + val aliasDescription = getRoomIdByAliasTask + .execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true)) + .getOrNull() + ?: return PeekResult.UnknownAlias + + roomId = aliasDescription.roomId + serverList = aliasDescription.servers + } else { + isAlias = false + roomId = params.roomIdOrAlias + serverList = emptyList() + } + + // Is it a public room? + val publicRepoResult = when (getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))) { + RoomDirectoryVisibility.PRIVATE -> { + // We cannot resolve this room :/ + null + } + RoomDirectoryVisibility.PUBLIC -> { + // Try to find it in directory + val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1)) + else null + + getPublicRoomTask.execute(GetPublicRoomTask.Params( + server = serverList.firstOrNull(), + publicRoomsParams = PublicRoomsParams( + filter = filter, + limit = 20.takeIf { filter != null } ?: 100 + ) + )).chunk?.firstOrNull { it.roomId == roomId } + } + } + + if (publicRepoResult != null) { + return PeekResult.Success( + roomId = roomId, + alias = publicRepoResult.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias }, + avatarUrl = publicRepoResult.avatarUrl, + name = publicRepoResult.name, + topic = publicRepoResult.topic, + numJoinedMembers = publicRepoResult.numJoinedMembers, + viaServers = serverList + ) + } + + // mm... try to peek state ? maybe the room is not public but yet allow guest to get events? + // this could be slow + try { + val stateEvents = resolveRoomStateTask + .execute(ResolveRoomStateTask.Params(roomId)) + val name = stateEvents.lastOrNull { + it.type == EventType.STATE_ROOM_NAME + && it.stateKey == "" + }?.let { it.content?.toModel()?.name } + + val topic = stateEvents.lastOrNull { + it.type == EventType.STATE_ROOM_TOPIC + && it.stateKey == "" + }?.let { it.content?.toModel()?.topic } + + val avatarUrl = stateEvents.lastOrNull { + it.type == EventType.STATE_ROOM_AVATAR + }?.let { it.content?.toModel()?.avatarUrl } + + val alias = stateEvents.lastOrNull { + it.type == EventType.STATE_ROOM_CANONICAL_ALIAS + }?.let { + it.content?.toModel()?.canonicalAlias + ?: it.content?.toModel()?.alternativeAliases?.firstOrNull() + } + + // not sure if it's the right way to do that :/ + val memberCount = stateEvents.filter { + it.type == EventType.STATE_ROOM_MEMBER + && it.stateKey?.isNotEmpty() == true + }.distinctBy { it.stateKey } + .count() + + return PeekResult.Success( + roomId = roomId, + alias = alias, + avatarUrl = avatarUrl, + name = name, + topic = topic, + numJoinedMembers = memberCount, + viaServers = serverList + ) + } catch (failure: Throwable) { + // Would be M_FORBIDDEN if cannot peek :/ + // User XXX not in room !XXX, and room previews are disabled + return PeekResult.PeekingNotAllowed( + roomId = roomId, + alias = params.roomIdOrAlias.takeIf { isAlias }, + viaServers = serverList + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt new file mode 100644 index 0000000000..289fdb498f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.peeking + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ResolveRoomStateTask : Task> { + data class Params( + val roomId: String + ) +} + +internal class DefaultResolveRoomStateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : ResolveRoomStateTask { + + override suspend fun execute(params: ResolveRoomStateTask.Params): List { + return executeRequest(eventBus) { + apiCall = roomAPI.getRoomState(params.roomId) + } + } +} diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e9bd03cb4b..fc3c7366ec 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -199,6 +199,11 @@ + + + diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 7dde0edf32..4ec786f841 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -126,9 +126,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet .observe() .subscribe { sharedAction -> when (sharedAction) { - is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { + is HomeActivitySharedAction.OpenGroup -> { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) } @@ -145,9 +145,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet homeActivityViewModel.observeViewEvents { when (it) { is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) - is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) - HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() - is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) + is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) + HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() + is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) }.exhaustive } homeActivityViewModel.subscribe(this) { renderState(it) } @@ -162,9 +162,27 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet private fun handleIntent(intent: Intent?) { intent?.dataString?.let { deepLink -> - if (!deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) return@let + val resolvedLink = if (deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)) { + deepLink + } else if (deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE)) { + // This is a bit hugly, but for now just convert to matrix.to link for compatibility + val service = activeSessionHolder.getSafeActiveSession()?.permalinkService() + val roomLinkPrefix = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" + val userPrefix = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" + when { + deepLink.startsWith(userPrefix) -> { + val userId = deepLink.substring(userPrefix.length) + service?.createPermalink(userId) + } + deepLink.startsWith(roomLinkPrefix) -> { + val param = deepLink.substring(roomLinkPrefix.length) + service?.createRoomPermalink(param) + } + else -> null + } + } else null - permalinkHandler.launch(this, deepLink, + permalinkHandler.launch(this, resolvedLink, navigationInterceptor = this, buildTask = true) // .delay(500, TimeUnit.MILLISECONDS) @@ -180,7 +198,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet private fun renderState(state: HomeActivityViewState) { when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { + is InitialSyncProgressService.Status.Idle -> { waiting_view.isVisible = false } is InitialSyncProgressService.Status.Progressing -> { diff --git a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt index f1149d8990..4e8b73cb2b 100644 --- a/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/app/features/permalink/PermalinkHandler.kt @@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.rx.rx import javax.inject.Inject @@ -76,7 +77,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti buildTask: Boolean ): Single { return when (permalinkData) { - is PermalinkData.RoomLink -> { + is PermalinkData.RoomLink -> { permalinkData.getRoomId() .observeOn(AndroidSchedulers.mainThread()) .map { @@ -92,11 +93,11 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti true } } - is PermalinkData.GroupLink -> { + is PermalinkData.GroupLink -> { navigator.openGroupDetail(permalinkData.groupId, context, buildTask) Single.just(true) } - is PermalinkData.UserLink -> { + is PermalinkData.UserLink -> { if (navigationInterceptor?.navToMemberProfile(permalinkData.userId, rawLink) != true) { navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask) } @@ -111,7 +112,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti private fun PermalinkData.RoomLink.getRoomId(): Single> { val session = activeSessionHolder.getSafeActiveSession() return if (isRoomAlias && session != null) { - session.rx().getRoomIdByAlias(roomIdOrAlias, true).subscribeOn(Schedulers.io()) + session.rx().getRoomIdByAlias(roomIdOrAlias, true).map { it.getOrNull()?.roomId.toOptional() }.subscribeOn(Schedulers.io()) } else { Single.just(Optional.from(roomIdOrAlias)) } @@ -149,16 +150,28 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti navigator.openRoom(context, roomId, eventId, buildTask) } else -> { - val roomPreviewData = RoomPreviewData( - roomId = roomId, - eventId = eventId, - roomAlias = roomAlias ?: roomSummary?.canonicalAlias, - roomName = roomSummary?.displayName, - avatarUrl = roomSummary?.avatarUrl, - buildTask = buildTask, - homeServers = permalinkData.viaParameters - ) - navigator.openRoomPreview(context, roomPreviewData) + if (roomSummary == null) { + // we don't know this room, try to peek + val roomPreviewData = RoomPreviewData( + roomId = roomId, + roomAlias = roomAlias, + peekFromServer = true, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } else { + val roomPreviewData = RoomPreviewData( + roomId = roomId, + eventId = eventId, + roomAlias = roomAlias ?: roomSummary.canonicalAlias, + roomName = roomSummary.displayName, + avatarUrl = roomSummary.avatarUrl, + buildTask = buildTask, + homeServers = permalinkData.viaParameters + ) + navigator.openRoomPreview(context, roomPreviewData) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index b2257b250a..c677a99175 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -130,7 +130,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy = emptyList(), + val peekFromServer: Boolean = false, val buildTask: Boolean = false ) : Parcelable { val matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt index 108c3bacf1..bc4552fc11 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewNoPreviewFragment.kt @@ -20,6 +20,8 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import androidx.transition.TransitionManager +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -30,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomdirectory.JoinState import kotlinx.android.synthetic.main.fragment_room_preview_no_preview.* +import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject /** @@ -48,22 +51,6 @@ class RoomPreviewNoPreviewFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(roomPreviewNoPreviewToolbar) - val titleText = roomPreviewData.roomName ?: roomPreviewData.roomAlias ?: roomPreviewData.roomId - - // Toolbar - avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewToolbarAvatar) - roomPreviewNoPreviewToolbarTitle.text = titleText - - // Screen - avatarRenderer.render(roomPreviewData.matrixItem, roomPreviewNoPreviewAvatar) - roomPreviewNoPreviewName.text = titleText - roomPreviewNoPreviewTopic.setTextOrHide(roomPreviewData.topic) - - if (roomPreviewData.worldReadable) { - roomPreviewNoPreviewLabel.setText(R.string.room_preview_world_readable_room_not_supported_yet) - } else { - roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview) - } roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback { override fun onButtonClicked() { @@ -100,7 +87,62 @@ class RoomPreviewNoPreviewFragment @Inject constructor( // Quit this screen requireActivity().finish() // Open room - navigator.openRoom(requireActivity(), roomPreviewData.roomId, roomPreviewData.eventId, roomPreviewData.buildTask) + navigator.openRoom(requireActivity(), state.roomId, roomPreviewData.eventId, roomPreviewData.buildTask) + } + + val bestName = state.roomName ?: state.roomAlias ?: state.roomId + when (state.peekingState) { + is Loading -> { + roomPreviewPeekingProgress.isVisible = true + roomPreviewNoPreviewJoin.isVisible = false + } + is Success -> { + roomPreviewPeekingProgress.isVisible = false + when (state.peekingState.invoke()) { + PeekingState.FOUND -> { + // show join buttons + roomPreviewNoPreviewJoin.isVisible = true + renderState(bestName, state.matrixItem(), state.roomTopic) + } + PeekingState.NO_ACCESS -> { + roomPreviewNoPreviewJoin.isVisible = true + roomPreviewNoPreviewLabel.isVisible = true + roomPreviewNoPreviewLabel.setText(R.string.room_preview_no_preview_join) + renderState(bestName, state.matrixItem().takeIf { state.roomAlias != null }, state.roomTopic) + } + else -> { + roomPreviewNoPreviewJoin.isVisible = false + roomPreviewNoPreviewLabel.isVisible = true + roomPreviewNoPreviewLabel.setText(R.string.room_preview_not_found) + renderState(bestName, null, state.roomTopic) + } + } + } + else -> { + // Render with initial state, no peeking + roomPreviewPeekingProgress.isVisible = false + roomPreviewNoPreviewJoin.isVisible = true + renderState(bestName, state.matrixItem(), state.roomTopic) + roomPreviewNoPreviewLabel.isVisible = false + } } } + + private fun renderState(roomName: String, matrixItem: MatrixItem?, topic: String?) { + // Toolbar + if (matrixItem != null) { + roomPreviewNoPreviewToolbarAvatar.isVisible = true + roomPreviewNoPreviewAvatar.isVisible = true + avatarRenderer.render(matrixItem, roomPreviewNoPreviewToolbarAvatar) + avatarRenderer.render(matrixItem, roomPreviewNoPreviewAvatar) + } else { + roomPreviewNoPreviewToolbarAvatar.isVisible = false + roomPreviewNoPreviewAvatar.isVisible = false + } + roomPreviewNoPreviewToolbarTitle.text = roomName + + // Screen + roomPreviewNoPreviewName.text = roomName + roomPreviewNoPreviewTopic.setTextOrHide(topic) + } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 900ba537b5..a18a41285b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -16,8 +16,11 @@ package im.vector.app.features.roomdirectory.roompreview +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -25,12 +28,17 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.roomdirectory.JoinState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.internal.session.room.peeking.PeekResult +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber @@ -56,6 +64,52 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini // Observe joined room (from the sync) observeRoomSummary() observeMembershipChanges() + + if (initialState.shouldPeekFromServer) { + setState { + copy(peekingState = Loading()) + } + viewModelScope.launch(Dispatchers.IO) { + val peekResult = tryOrNull { + awaitCallback { + session.peekRoom(initialState.roomAlias ?: initialState.roomId, it) + } + } + + when (peekResult) { + is PeekResult.Success -> { + setState { + copy( + roomId = peekResult.roomId, + avatarUrl = peekResult.avatarUrl, + roomAlias = peekResult.alias ?: initialState.roomAlias, + roomTopic = peekResult.topic, + homeServers = peekResult.viaServers, + peekingState = Success(PeekingState.FOUND) + ) + } + } + is PeekResult.PeekingNotAllowed -> { + setState { + copy( + roomId = peekResult.roomId, + roomAlias = peekResult.alias ?: initialState.roomAlias, + homeServers = peekResult.viaServers, + peekingState = Success(PeekingState.NO_ACCESS) + ) + } + } + PeekResult.UnknownAlias, + null -> { + setState { + copy( + peekingState = Success(PeekingState.NOT_FOUND) + ) + } + } + } + } + } } private fun observeRoomSummary() { @@ -82,7 +136,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini .subscribe { val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown val joinState = when (changeMembership) { - is ChangeMembershipState.Joining -> JoinState.JOINING + is ChangeMembershipState.Joining -> JoinState.JOINING is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR // Other cases are handled by room summary else -> null diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt index 6816e54481..c0f7591f87 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewState.kt @@ -16,13 +16,31 @@ package im.vector.app.features.roomdirectory.roompreview +import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized import im.vector.app.features.roomdirectory.JoinState +import org.matrix.android.sdk.api.util.MatrixItem + +enum class PeekingState { + UNKNOWN, + FOUND, + NOT_FOUND, + NO_ACCESS +} data class RoomPreviewViewState( + + val peekingState: Async = Uninitialized, // The room id val roomId: String = "", val roomAlias: String? = null, + + val roomName: String? = null, + val roomTopic: String? = null, + val avatarUrl: String? = null, + + val shouldPeekFromServer: Boolean = false, /** * Can be empty when the server is the current user's home server. */ @@ -36,6 +54,14 @@ data class RoomPreviewViewState( constructor(args: RoomPreviewData) : this( roomId = args.roomId, roomAlias = args.roomAlias, - homeServers = args.homeServers + homeServers = args.homeServers, + roomName = args.roomName, + roomTopic = args.topic, + avatarUrl = args.avatarUrl, + shouldPeekFromServer = args.peekFromServer ) + + fun matrixItem() : MatrixItem { + return MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) + } } diff --git a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml index 2f4db6f116..906c7a21ab 100644 --- a/vector/src/main/res/layout/fragment_room_preview_no_preview.xml +++ b/vector/src/main/res/layout/fragment_room_preview_no_preview.xml @@ -54,6 +54,14 @@ + "This room can't be previewed" "The preview of world-readable room is not supported yet in Element" - + This room is not accessible at this time.\nTry again later, or ask a room admin to check if you have access. + "This room can't be previewed. Do you want to join it?" "Rooms" "Direct Messages"