diff --git a/CHANGES.md b/CHANGES.md index befb9d3adf..7bbeb0443d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,11 +11,13 @@ Features ✨: Improvements 🙌: - Add Setting Item to Change PIN (#2462) - Improve room history visibility setting UX (#1579) + - Matrix.to deeplink custom scheme support Bugfix 🐛: - Fix cancellation of sending event (#2438) - Double bottomsheet effect after verify with passphrase - EditText cursor jumps to the start while typing fast (#2469) + - No known servers error is given when joining rooms on new Gitter bridge (#2516) - Show preview when sending attachment from the keyboard (#2440) - Do not compress GIFs (#1616, #1254) 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..5f02b77a1e 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.session.room.peeking.PeekResult 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 /** * 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,16 @@ interface RoomService { * @return a LiveData of the optional found room member */ fun getRoomMemberLive(userId: String, roomId: String): LiveData> + + /** + * Get some state events about a room + */ + 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/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt new file mode 100644 index 0000000000..db70dadef3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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.api.session.room.peeking + +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() +} 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..383dd876d3 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 @@ -27,6 +28,7 @@ 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.session.room.peeking.PeekResult import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -35,10 +37,13 @@ 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.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..aa92c1cb3b 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 @@ -183,7 +183,7 @@ internal interface RoomAPI { @Body body: ThreePidInviteBody): Call /** - * Send a generic state events + * Send a generic state event * * @param roomId the room id. * @param stateEventType the state event type @@ -195,7 +195,7 @@ internal interface RoomAPI { @Body params: JsonDict): Call /** - * Send a generic state events + * Send a generic state event * * @param roomId the room id. * @param stateEventType the state event type @@ -208,6 +208,13 @@ internal interface RoomAPI { @Path("state_key") stateKey: String, @Body params: JsonDict): Call + /** + * Get state events of a room + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") + fun getRoomState(@Path("roomId") roomId: String) : Call> + /** * Send a relation event to a room. * 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..543d605707 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 { - var roomId = Realm.getInstance(monarchy.realmConfiguration).use { + override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { + val 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..5a82d74537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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.api.session.room.peeking.PeekResult +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 + +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 = MatrixPatterns.isRoomAlias(params.roomIdOrAlias) + if (isAlias) { + // get alias description + val aliasDescription = getRoomIdByAliasTask + .execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true)) + .getOrNull() + ?: return PeekResult.UnknownAlias + + roomId = aliasDescription.roomId + serverList = aliasDescription.servers + } else { + 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 } + + // 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..03ea2408f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 63a3fad109..fc510e585c 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===83 +enum class===84 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e9bd03cb4b..bf839b807c 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -81,8 +81,9 @@ android:resource="@xml/shortcuts" /> - + - + + + + + 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..e1837ccb1b 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 @@ -162,11 +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 = when { + deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink + deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { + // This is a bit ugly, but for now just convert to matrix.to link for compatibility + when { + deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) + deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length) + else -> null + }?.let { + activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) + } + } + else -> null + } - permalinkHandler.launch(this, deepLink, + permalinkHandler.launch( + context = this, + deepLink = resolvedLink, navigationInterceptor = this, - buildTask = true) + buildTask = true + ) // .delay(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { isHandled -> @@ -345,11 +361,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } - R.id.menu_home_setting -> { + R.id.menu_home_setting -> { navigator.openSettings(this) return true } @@ -390,5 +406,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet putExtra(MvRx.KEY_ARG, args) } } + + private const val ROOM_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" + private const val USER_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" } } 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..a7d69c783c 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 @@ -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..72c4c58a42 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.peeking.PeekResult import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber @@ -56,6 +64,56 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini // Observe joined room (from the sync) observeRoomSummary() observeMembershipChanges() + + if (initialState.shouldPeekFromServer) { + peekRoomFromServer() + } + } + + private fun peekRoomFromServer() { + 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() { 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..5a73f29ca3 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,23 @@ 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 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 +46,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"