diff --git a/changelog.d/5443.misc b/changelog.d/5443.misc new file mode 100644 index 0000000000..f9fd715403 --- /dev/null +++ b/changelog.d/5443.misc @@ -0,0 +1 @@ +Adds stable room hierarchy endpoint with a fallback to the unstable one diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 441b031753..2b2c38e22a 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -174,7 +174,7 @@ dependencies { // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 testImplementation libs.mockk.mockk testImplementation libs.tests.kluent - implementation libs.jetbrains.coroutinesAndroid + testImplementation libs.jetbrains.coroutinesTest // Plant Timber tree for test testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1' // Transitively required for mocking realm as monarchy doesn't expose Rx diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt index 1ab1042129..5aec7db66c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt @@ -21,6 +21,7 @@ internal object NetworkConstants { private const val URI_API_PREFIX_PATH = "_matrix/client" const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/" const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/" + const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/" const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/" // Media diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index c18055e089..e764ab551a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -113,71 +113,108 @@ internal class DefaultSpaceService @Inject constructor( return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId)) } - override suspend fun querySpaceChildren(spaceId: String, - suggestedOnly: Boolean?, - limit: Int?, - from: String?, - knownStateList: List?): SpaceHierarchyData { - return resolveSpaceInfoTask.execute( - ResolveSpaceInfoTask.Params( - spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly - ) - ).let { response -> - val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId } - val root = RoomSummary( - roomId = spaceDesc?.roomId ?: spaceId, - roomType = spaceDesc?.roomType, - name = spaceDesc?.name ?: "", - displayName = spaceDesc?.name ?: "", - topic = spaceDesc?.topic ?: "", - joinedMembersCount = spaceDesc?.numJoinedMembers, - avatarUrl = spaceDesc?.avatarUrl ?: "", - encryptionEventTs = null, - typingUsers = emptyList(), - isEncrypted = false, - flattenParentIds = emptyList(), - canonicalAlias = spaceDesc?.canonicalAlias, - joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true } - ) - val children = response.rooms - ?.filter { it.roomId != spaceId } - ?.flatMap { childSummary -> - (spaceDesc?.childrenState ?: knownStateList) - ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } - ?.mapNotNull { childStateEv -> - // create a child entry for everytime this room is the child of a space - // beware that a room could appear then twice in this list - childStateEv.content.toModel()?.let { childStateEvContent -> - SpaceChildInfo( - childRoomId = childSummary.roomId, - isKnown = true, - roomType = childSummary.roomType, - name = childSummary.name, - topic = childSummary.topic, - avatarUrl = childSummary.avatarUrl, - order = childStateEvContent.order, -// autoJoin = childStateEvContent.autoJoin ?: false, - viaServers = childStateEvContent.via.orEmpty(), - activeMemberCount = childSummary.numJoinedMembers, - parentRoomId = childStateEv.roomId, - suggested = childStateEvContent.suggested, - canonicalAlias = childSummary.canonicalAlias, - aliases = childSummary.aliases, - worldReadable = childSummary.worldReadable - ) - } - }.orEmpty() - } - .orEmpty() - SpaceHierarchyData( - rootSummary = root, - children = children, - childrenState = spaceDesc?.childrenState.orEmpty(), - nextToken = response.nextBatch - ) - } + override suspend fun querySpaceChildren( + spaceId: String, + suggestedOnly: Boolean?, + limit: Int?, + from: String?, + knownStateList: List? + ): SpaceHierarchyData { + val spacesResponse = getSpacesResponse(spaceId, suggestedOnly, limit, from) + val spaceRootResponse = spacesResponse.getRoot(spaceId) + val spaceRoot = spaceRootResponse?.toRoomSummary() ?: createBlankRoomSummary(spaceId) + val spaceChildren = spacesResponse.rooms.mapSpaceChildren(spaceId, spaceRootResponse, knownStateList) + + return SpaceHierarchyData( + rootSummary = spaceRoot, + children = spaceChildren, + childrenState = spaceRootResponse?.childrenState.orEmpty(), + nextToken = spacesResponse.nextBatch + ) } + private suspend fun getSpacesResponse(spaceId: String, suggestedOnly: Boolean?, limit: Int?, from: String?) = + resolveSpaceInfoTask.execute( + ResolveSpaceInfoTask.Params(spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly) + ) + + private fun SpacesResponse.getRoot(spaceId: String) = rooms?.firstOrNull { it.roomId == spaceId } + + private fun SpaceChildSummaryResponse.toRoomSummary() = RoomSummary( + roomId = roomId, + roomType = roomType, + name = name ?: "", + displayName = name ?: "", + topic = topic ?: "", + joinedMembersCount = numJoinedMembers, + avatarUrl = avatarUrl ?: "", + encryptionEventTs = null, + typingUsers = emptyList(), + isEncrypted = false, + flattenParentIds = emptyList(), + canonicalAlias = canonicalAlias, + joinRules = RoomJoinRules.PUBLIC.takeIf { isWorldReadable } + ) + + private fun createBlankRoomSummary(spaceId: String) = RoomSummary( + roomId = spaceId, + joinedMembersCount = null, + encryptionEventTs = null, + typingUsers = emptyList(), + isEncrypted = false, + flattenParentIds = emptyList(), + canonicalAlias = null, + joinRules = null + ) + + private fun List?.mapSpaceChildren( + spaceId: String, + spaceRootResponse: SpaceChildSummaryResponse?, + knownStateList: List?, + ) = this?.filterIdIsNot(spaceId) + ?.toSpaceChildInfoList(spaceId, spaceRootResponse, knownStateList) + .orEmpty() + + private fun List.filterIdIsNot(spaceId: String) = filter { it.roomId != spaceId } + + private fun List.toSpaceChildInfoList( + spaceId: String, + rootRoomResponse: SpaceChildSummaryResponse?, + knownStateList: List?, + ) = flatMap { spaceChildSummary -> + (rootRoomResponse?.childrenState ?: knownStateList) + ?.filter { it.isChildOf(spaceChildSummary) } + ?.mapNotNull { childStateEvent -> childStateEvent.toSpaceChildInfo(spaceId, spaceChildSummary) } + .orEmpty() + } + + private fun Event.isChildOf(space: SpaceChildSummaryResponse) = stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD + + private fun Event.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse) = content.toModel()?.let { content -> + createSpaceChildInfo(spaceId, summary, content) + } + + private fun createSpaceChildInfo( + spaceId: String, + summary: SpaceChildSummaryResponse, + content: SpaceChildContent + ) = SpaceChildInfo( + childRoomId = summary.roomId, + isKnown = true, + roomType = summary.roomType, + name = summary.name, + topic = summary.topic, + avatarUrl = summary.avatarUrl, + order = content.order, + viaServers = content.via.orEmpty(), + activeMemberCount = summary.numJoinedMembers, + parentRoomId = spaceId, + suggested = content.suggested, + canonicalAlias = summary.canonicalAlias, + aliases = summary.aliases, + worldReadable = summary.isWorldReadable + ) + override suspend fun joinSpace(spaceIdOrAlias: String, reason: String?, viaServers: List): JoinSpaceResult { @@ -192,10 +229,6 @@ internal class DefaultSpaceService @Inject constructor( leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason)) } -// override fun getSpaceParentsOfRoom(roomId: String): List { -// return spaceSummaryDataSource.getParentsOfRoom(roomId) -// } - override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List) { // Should we perform some validation here?, // and if client want to bypass, it could use sendStateEvent directly? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt index 2a396d6ee7..d59ca06c2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task +import retrofit2.HttpException import javax.inject.Inject internal interface ResolveSpaceInfoTask : Task { @@ -28,7 +29,6 @@ internal interface ResolveSpaceInfoTask : Task(500, "".toResponseBody())) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt new file mode 100644 index 0000000000..d4fc986791 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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.test.fakes + +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.space.SpaceApi +import org.matrix.android.sdk.internal.session.space.SpacesResponse +import org.matrix.android.sdk.test.fixtures.ResolveSpaceInfoTaskParamsFixture + +internal class FakeSpaceApi { + + val instance: SpaceApi = mockk() + val params = ResolveSpaceInfoTaskParamsFixture.aResolveSpaceInfoTaskParams() + + fun givenStableEndpointReturns(response: SpacesResponse) { + coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response + } + + fun givenStableEndpointThrows(throwable: Throwable) { + coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws throwable + } + + fun givenUnstableEndpointReturns(response: SpacesResponse) { + coEvery { instance.getSpaceHierarchyUnstable(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt new file mode 100644 index 0000000000..28f8c3637d --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2021 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.test.fixtures + +import org.matrix.android.sdk.internal.session.space.ResolveSpaceInfoTask + +internal object ResolveSpaceInfoTaskParamsFixture { + fun aResolveSpaceInfoTaskParams( + spaceId: String = "", + limit: Int? = null, + maxDepth: Int? = null, + from: String? = null, + suggestedOnly: Boolean? = null, + ) = ResolveSpaceInfoTask.Params( + spaceId, + limit, + maxDepth, + from, + suggestedOnly, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt new file mode 100644 index 0000000000..0a08331114 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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.test.fixtures + +import org.matrix.android.sdk.internal.session.space.SpaceChildSummaryResponse +import org.matrix.android.sdk.internal.session.space.SpacesResponse + +internal object SpacesResponseFixture { + fun aSpacesResponse( + nextBatch: String? = null, + rooms: List? = null, + ) = SpacesResponse( + nextBatch, + rooms, + ) +}