diff --git a/CHANGES.md b/CHANGES.md index e752e1879c..363398a7a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,8 @@ Features ✨: - Implement soft logout (#281) Improvements 🙌: - - + - Handle navigation to room via room alias (#201) + - Open matrix.to link in RiotX (#57) Other changes: - Use same default room colors than Riot-Web diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt deleted file mode 100644 index cf0e955b00..0000000000 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackCompletable.kt +++ /dev/null @@ -1,38 +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.matrix.rx - -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.util.Cancelable -import io.reactivex.CompletableEmitter - -internal class MatrixCallbackCompletable(private val completableEmitter: CompletableEmitter) : MatrixCallback { - - override fun onSuccess(data: T) { - completableEmitter.onComplete() - } - - override fun onFailure(failure: Throwable) { - completableEmitter.tryOnError(failure) - } -} - -fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) { - completableEmitter.setCancellable { - this.cancel() - } -} diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxCallbackBuilders.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxCallbackBuilders.kt new file mode 100644 index 0000000000..92886777af --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxCallbackBuilders.kt @@ -0,0 +1,54 @@ +/* + * 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.matrix.rx + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable +import io.reactivex.Completable +import io.reactivex.Single + +fun singleBuilder(builder: (callback: MatrixCallback) -> Cancelable): Single = Single.create { + val callback: MatrixCallback = object : MatrixCallback { + override fun onSuccess(data: T) { + it.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + it.tryOnError(failure) + } + } + val cancelable = builder(callback) + it.setCancellable { + cancelable.cancel() + } +} + +fun completableBuilder(builder: (callback: MatrixCallback) -> Cancelable): Completable = Completable.create { + val callback: MatrixCallback = object : MatrixCallback { + override fun onSuccess(data: T) { + it.onComplete() + } + + override fun onFailure(failure: Throwable) { + it.tryOnError(failure) + } + } + val cancelable = builder(callback) + it.setCancellable { + cancelable.cancel() + } +} 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 bc0a866117..e5ebc536ff 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 @@ -53,13 +53,13 @@ class RxRoom(private val room: Room) { return room.getMyReadReceiptLive().asObservable() } - fun loadRoomMembersIfNeeded(): Single = Single.create { - room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) + fun loadRoomMembersIfNeeded(): Single = singleBuilder { + room.loadRoomMembersIfNeeded(it) } fun joinRoom(reason: String? = null, - viaServers: List = emptyList()): Single = Single.create { - room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) + viaServers: List = emptyList()): Single = singleBuilder { + room.join(reason, viaServers, it) } fun liveEventReadReceipts(eventId: String): Observable> { 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 5a42dbb804..c9381b861d 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 @@ -66,20 +66,25 @@ class RxSession(private val session: Session) { return session.livePagedUsers(filter).asObservable() } - fun createRoom(roomParams: CreateRoomParams): Single = Single.create { - session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) + fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { + session.createRoom(roomParams, it) } fun searchUsersDirectory(search: String, limit: Int, - excludedUserIds: Set): Single> = Single.create { - session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) + excludedUserIds: Set): Single> = singleBuilder { + session.searchUsersDirectory(search, limit, excludedUserIds, it) } fun joinRoom(roomId: String, reason: String? = null, - viaServers: List = emptyList()): Single = Single.create { - session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) + viaServers: List = emptyList()): Single = singleBuilder { + session.joinRoom(roomId, reason, viaServers, it) + } + + fun getRoomIdByAlias(roomAlias: String, + searchOnServer: Boolean): Single> = singleBuilder { + session.getRoomIdByAlias(roomAlias, searchOnServer, it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkData.kt index 47f8bf4505..ecdbe86b98 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkData.kt @@ -24,9 +24,7 @@ import android.net.Uri */ sealed class PermalinkData { - data class EventLink(val roomIdOrAlias: String, val eventId: String) : PermalinkData() - - data class RoomLink(val roomIdOrAlias: String) : PermalinkData() + data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData() data class UserLink(val userId: String) : PermalinkData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt index 531f4ae523..d10152f4fe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkParser.kt @@ -60,16 +60,21 @@ object PermalinkParser { return PermalinkData.FallbackLink(uri) } return when { - MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) - MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) - MatrixPatterns.isRoomId(identifier) -> { - if (!extraParameter.isNullOrEmpty() && MatrixPatterns.isEventId(extraParameter)) { - PermalinkData.EventLink(roomIdOrAlias = identifier, eventId = extraParameter) - } else { - PermalinkData.RoomLink(roomIdOrAlias = identifier) + MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) + MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) + MatrixPatterns.isRoomId(identifier) -> { + val eventId = extraParameter.takeIf { + !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId) } - else -> PermalinkData.FallbackLink(uri) + MatrixPatterns.isRoomAlias(identifier) -> { + val eventId = extraParameter.takeIf { + !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) + } + PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId) + } + else -> PermalinkData.FallbackLink(uri) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 98abce5898..afe7cf8bc3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional /** * This interface defines methods to get rooms. It's implemented at the session level. @@ -74,4 +75,11 @@ interface RoomService { */ fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable + + /** + * Resolve a room alias to a room ID. + */ + fun getRoomIdByAlias(roomAlias: String, + searchOnServer: Boolean, + callback: MatrixCallback>): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index b750c5347e..34af2cf572 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -59,7 +59,6 @@ interface MembershipService { /** * Join the room, or accept an invitation. */ - fun join(reason: String? = null, viaServers: List = emptyList(), callback: MatrixCallback): Cancelable diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 447ba563de..129c35a17e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -29,6 +29,8 @@ data class RoomSummary( val displayName: String = "", val topic: String = "", val avatarUrl: String = "", + val canonicalAlias: String? = null, + val aliases: List = emptyList(), val isDirect: Boolean = false, val latestPreviewableEvent: TimelineEvent? = null, val otherMemberIds: List = emptyList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 2577bec581..eeb340eacb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -68,7 +68,9 @@ internal class RoomSummaryMapper @Inject constructor( membership = roomSummaryEntity.membership, versioningState = roomSummaryEntity.versioningState, readMarkerId = roomSummaryEntity.readMarkerId, - userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList() + userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), + canonicalAlias = roomSummaryEntity.canonicalAlias, + aliases = roomSummaryEntity.aliases.toList() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 47904380a0..406c8700b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -39,7 +39,10 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var hasUnreadMessages: Boolean = false, var tags: RealmList = RealmList(), var userDrafts: UserDraftsEntity? = null, - var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS + var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, + var canonicalAlias: String? = null, + var aliases: RealmList = RealmList(), + var flatAliases: String = "" ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index 79473f3b10..1f242ce83a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -32,6 +32,18 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n return query } +internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? { + val roomSummary = realm.where() + .equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias) + .findFirst() + if (roomSummary != null) { + return roomSummary + } + return realm.where() + .contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias") + .findFirst() +} + internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { return where(realm, roomId).findFirst() ?: realm.createObject(roomId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 22caf76eaf..de60e6e7e4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -25,11 +25,13 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask @@ -45,6 +47,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona private val joinRoomTask: JoinRoomTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, + private val roomIdByAliasTask: GetRoomIdByAliasTask, private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { @@ -111,4 +114,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona } .executeBy(taskExecutor) } + + override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { + return roomIdByAliasTask + .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 40164d1697..c5b3f03d35 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams @@ -258,4 +259,12 @@ internal interface RoomAPI { fun reportContent(@Path("roomId") roomId: String, @Path("eventId") eventId: String, @Body body: ReportContentBody): Call + + /** + * Get the room ID associated to the room alias. + * + * @param roomAlias the room alias. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}") + fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 1aca492b94..cc786a7493 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.SessionScope +import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask +import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask @@ -133,4 +135,7 @@ internal abstract class RoomModule { @Binds abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask + + @Binds + abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 1158c08984..126d13c5db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -20,6 +20,8 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomAliasesContent +import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity @@ -68,7 +70,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId unreadNotifications: RoomSyncUnreadNotifications? = null, updateMembers: Boolean = false) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { @@ -91,15 +93,24 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() + val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev() + val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 - // avoid this call if we are sure there are unread events - || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) + // avoid this call if we are sure there are unread events + || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel()?.topic roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent + roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel() + ?.canonicalAlias + + val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases ?: emptyList() + roomSummaryEntity.aliases.clear() + roomSummaryEntity.aliases.addAll(roomAliases) + roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") if (updateMembers) { val otherRoomMembers = RoomMembers(realm, roomId) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt new file mode 100644 index 0000000000..1a726c3fe5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -0,0 +1,54 @@ +/* + * 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.matrix.android.internal.session.room.alias + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.findByAlias +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import io.realm.Realm +import javax.inject.Inject + +internal interface GetRoomIdByAliasTask : Task> { + data class Params( + val roomAlias: String, + val searchOnServer: Boolean + ) +} + +internal class DefaultGetRoomIdByAliasTask @Inject constructor(private val monarchy: Monarchy, + private val roomAPI: RoomAPI) : GetRoomIdByAliasTask { + + 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) + } else if (!params.searchOnServer) { + Optional.from(null) + } else { + roomId = executeRequest { + apiCall = roomAPI.getRoomIdByAlias(params.roomAlias) + }.roomId + Optional.from(roomId) + } + } +} diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/RoomAliasDescription.kt similarity index 50% rename from matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/RoomAliasDescription.kt index d638354dfd..c7ddae0e10 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/MatrixCallbackSingle.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/alias/RoomAliasDescription.kt @@ -14,25 +14,20 @@ * limitations under the License. */ -package im.vector.matrix.rx +package im.vector.matrix.android.internal.session.room.alias -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.util.Cancelable -import io.reactivex.SingleEmitter +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass -internal class MatrixCallbackSingle(private val singleEmitter: SingleEmitter) : MatrixCallback { +@JsonClass(generateAdapter = true) +internal data class RoomAliasDescription( + /** + * The room ID for this alias. + */ + @Json(name = "room_id") val roomId: String, - override fun onSuccess(data: T) { - singleEmitter.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - singleEmitter.tryOnError(failure) - } -} - -fun Cancelable.toSingle(singleEmitter: SingleEmitter) { - singleEmitter.setCancellable { - this.cancel() - } -} + /** + * A list of servers that are aware of this room ID. + */ + @Json(name = "servers") val servers: List = emptyList() +) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 61eebc99db..068e7423d8 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -65,7 +65,13 @@ - + + + @@ -102,6 +108,18 @@ + + + + + + + + + + + + { - if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) { - openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId) - } - - true - } - is PermalinkData.RoomLink -> { - if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias) != true) { - openRoom(context, permalinkData.roomIdOrAlias) - } - - true - } - is PermalinkData.GroupLink -> { - navigator.openGroupDetail(permalinkData.groupId, context) - true - } - is PermalinkData.UserLink -> { - navigator.openUserDetail(permalinkData.userId, context) - true - } - is PermalinkData.FallbackLink -> { - false - } - } - } - - /** - * Open room either joined, or not unknown - */ - private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) { - if (session.getRoom(roomIdOrAlias) != null) { - navigator.openRoom(context, roomIdOrAlias, eventId) - } else { - navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId) - } - } -} - -interface NavigateToRoomInterceptor { - - /** - * Return true if the navigation has been intercepted - */ - fun navToRoom(roomId: String, eventId: String? = null): Boolean -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 9af0e946a0..a491dd14ba 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -86,8 +86,8 @@ import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.NavigateToRoomInterceptor -import im.vector.riotx.features.home.PermalinkHandler +import im.vector.riotx.features.permalink.NavigateToRoomInterceptor +import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.composer.TextComposerAction import im.vector.riotx.features.home.room.detail.composer.TextComposerView @@ -113,6 +113,8 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.themes.ThemeUtils +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* @@ -851,30 +853,33 @@ class RoomDetailFragment @Inject constructor( // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { - val managed = permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - // Same room? - if (roomId == roomDetailArgs.roomId) { - // Navigation to same room - if (eventId == null) { - showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) - } else { - // Highlight and scroll to this event - roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + permalinkHandler + .launch(requireActivity(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String?, eventId: String?): Boolean { + // Same room? + if (roomId == roomDetailArgs.roomId) { + // Navigation to same room + if (eventId == null) { + showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) + } else { + // Highlight and scroll to this event + roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) + } + return true + } + // Not handled + return false + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { managed -> + if (!managed) { + // Open in external browser, in a new Tab + openUrlInExternalBrowser(requireContext(), url) } - return true } - - // Not handled - return false - } - }) - - if (!managed) { - // Open in external browser, in a new Tab - openUrlInExternalBrowser(requireContext(), url) - } - + .disposeOnDestroyView() // In fact it is always managed return true } @@ -1026,12 +1031,15 @@ class RoomDetailFragment @Inject constructor( } override fun onRoomCreateLinkClicked(url: String) { - permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { - override fun navToRoom(roomId: String, eventId: String?): Boolean { - requireActivity().finish() - return false - } - }) + permalinkHandler + .launch(requireContext(), url, object : NavigateToRoomInterceptor { + override fun navToRoom(roomId: String?, eventId: String?): Boolean { + requireActivity().finish() + return false + } + }) + .subscribe() + .disposeOnDestroyView() } override fun onReadReceiptsClicked(readReceipts: List) { 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 685fa04fef..19f6aece58 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 @@ -38,14 +38,45 @@ import im.vector.riotx.features.share.SharedData import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import androidx.core.app.TaskStackBuilder @Singleton class DefaultNavigator @Inject constructor() : Navigator { - override fun openRoom(context: Context, roomId: String, eventId: String?) { + override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) { val args = RoomDetailArgs(roomId, eventId) val intent = RoomDetailActivity.newIntent(context, args) - context.startActivity(intent) + if (buildTask) { + val stackBuilder = TaskStackBuilder.create(context) + stackBuilder.addNextIntentWithParentStack(intent) + stackBuilder.startActivities() + } else { + context.startActivity(intent) + } + } + + override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) { + if (context is VectorBaseActivity) { + context.notImplemented("Open not joined room") + } else { + context.toast(R.string.not_implemented) + } + } + + override fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean) { + if (context is VectorBaseActivity) { + context.notImplemented("Open group detail") + } else { + context.toast(R.string.not_implemented) + } + } + + override fun openUserDetail(userId: String, context: Context, buildTask: Boolean) { + if (context is VectorBaseActivity) { + context.notImplemented("Open user detail") + } else { + context.toast(R.string.not_implemented) + } } override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) { @@ -55,14 +86,6 @@ class DefaultNavigator @Inject constructor() : Navigator { activity.finish() } - override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) { - if (context is VectorBaseActivity) { - context.notImplemented("Open not joined room") - } else { - context.toast(R.string.not_implemented) - } - } - override fun openRoomPreview(publicRoom: PublicRoom, context: Context) { val intent = RoomPreviewActivity.getIntent(context, publicRoom) context.startActivity(intent) @@ -105,14 +128,6 @@ class DefaultNavigator @Inject constructor() : Navigator { context.startActivity(KeysBackupManageActivity.intent(context)) } - override fun openGroupDetail(groupId: String, context: Context) { - Timber.v("Open group detail $groupId") - } - - override fun openUserDetail(userId: String, context: Context) { - Timber.v("Open user detail $userId") - } - override fun openRoomSettings(context: Context, roomId: String) { Timber.v("Open room settings$roomId") } 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 83c4f7ce20..278c8fdba0 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 @@ -23,11 +23,11 @@ import im.vector.riotx.features.share.SharedData interface Navigator { - fun openRoom(context: Context, roomId: String, eventId: String? = null) + fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false) fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) - fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) + fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false) fun openRoomPreview(publicRoom: PublicRoom, context: Context) @@ -47,9 +47,9 @@ interface Navigator { fun openKeysBackupManager(context: Context) - fun openGroupDetail(groupId: String, context: Context) + fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false) - fun openUserDetail(userId: String, context: Context) + fun openUserDetail(userId: String, context: Context, buildTask: Boolean = false) fun openRoomSettings(context: Context, roomId: String) } diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt new file mode 100644 index 0000000000..c849166738 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt @@ -0,0 +1,107 @@ +/* + * 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.permalink + +import android.content.Context +import android.net.Uri +import im.vector.matrix.android.api.permalinks.PermalinkData +import im.vector.matrix.android.api.permalinks.PermalinkParser +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.rx.rx +import im.vector.riotx.features.navigation.Navigator +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PermalinkHandler @Inject constructor(private val session: Session, + private val navigator: Navigator) { + + fun launch( + context: Context, + deepLink: String?, + navigateToRoomInterceptor: NavigateToRoomInterceptor? = null, + buildTask: Boolean = false + ): Single { + val uri = deepLink?.let { Uri.parse(it) } + return launch(context, uri, navigateToRoomInterceptor, buildTask) + } + + fun launch( + context: Context, + deepLink: Uri?, + navigateToRoomInterceptor: NavigateToRoomInterceptor? = null, + buildTask: Boolean = false + ): Single { + if (deepLink == null) { + return Single.just(false) + } + return when (val permalinkData = PermalinkParser.parse(deepLink)) { + is PermalinkData.RoomLink -> { + permalinkData.getRoomId() + .observeOn(AndroidSchedulers.mainThread()) + .map { + val roomId = it.getOrNull() + if (navigateToRoomInterceptor?.navToRoom(roomId) != true) { + openRoom(context, roomId, permalinkData.eventId, buildTask) + } + true + } + } + is PermalinkData.GroupLink -> { + navigator.openGroupDetail(permalinkData.groupId, context, buildTask) + Single.just(true) + } + is PermalinkData.UserLink -> { + navigator.openUserDetail(permalinkData.userId, context, buildTask) + Single.just(true) + } + is PermalinkData.FallbackLink -> { + Single.just(false) + } + } + } + + private fun PermalinkData.RoomLink.getRoomId(): Single> { + return if (isRoomAlias) { + // At the moment we are not fetching on the server as we don't handle not join room + session.rx().getRoomIdByAlias(roomIdOrAlias, false).subscribeOn(Schedulers.io()) + } else { + Single.just(Optional.from(roomIdOrAlias)) + } + } + + /** + * Open room either joined, or not unknown + */ + private fun openRoom(context: Context, roomId: String?, eventId: String? = null, buildTask: Boolean) { + return if (roomId != null && session.getRoom(roomId) != null) { + navigator.openRoom(context, roomId, eventId, buildTask) + } else { + navigator.openNotJoinedRoom(context, roomId, eventId, buildTask) + } + } +} + +interface NavigateToRoomInterceptor { + + /** + * Return true if the navigation has been intercepted + */ + fun navToRoom(roomId: String?, eventId: String? = null): Boolean +} diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandlerActivity.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandlerActivity.kt new file mode 100644 index 0000000000..08e09fa48d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandlerActivity.kt @@ -0,0 +1,74 @@ +/* + * 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.permalink + +import android.content.Intent +import android.os.Bundle +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.replaceFragment +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.home.LoadingFragment +import im.vector.riotx.features.login.LoginActivity +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.debug.activity_test_material_theme.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class PermalinkHandlerActivity : VectorBaseActivity() { + + @Inject lateinit var permalinkHandler: PermalinkHandler + @Inject lateinit var sessionHolder: ActiveSessionHolder + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_simple) + if (isFirstCreation()) { + replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java) + } + // If we are not logged in, open login screen. + // In the future, we might want to relaunch the process after login. + if (!sessionHolder.hasActiveSession()) { + startLoginActivity() + return + } + val uri = intent.dataString + permalinkHandler.launch(this, uri, buildTask = true) + .delay(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { isHandled -> + if (!isHandled) { + toast(R.string.permalink_malformed) + } + finish() + } + .disposeOnDestroy() + } + + private fun startLoginActivity() { + val intent = LoginActivity.newIntent(this, null) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + finish() + } +} diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 93e81e01ec..c19d0802f0 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -163,6 +163,7 @@ Clear data The current session is for user %1$s and you provide credentials for user %2$s. This is not supported by RiotX.\nPlease first clear data, then sign in again on another account. + Your matrix.to link was malformed Verification Request Verification Sent