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 7cc0d69bb9..38b0a3a343 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 @@ -39,7 +39,6 @@ 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.space.SpaceSummary import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.user.model.User @@ -68,13 +67,20 @@ class RxSession(private val session: Session) { } } - fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable> { + fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable> { return session.spaceService().getSpaceSummariesLive(queryParams).asObservable() .startWithCallable { session.spaceService().getSpaceSummaries(queryParams) } } + fun liveFlattenRoomSummaryChildOf(spaceId: String?): Observable> { + return session.getFlattenRoomSummaryChildOfLive(spaceId).asObservable() + .startWithCallable { + session.getFlattenRoomSummaryChildOf(spaceId) + } + } + fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable> { return session.getBreadcrumbsLive(queryParams).asObservable() .startWithCallable { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt new file mode 100644 index 0000000000..b512983ea6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -0,0 +1,373 @@ +/* + * 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.session.space + +import android.util.Log +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.SessionTestParams +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class SpaceHierarchyTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun createCanonicalChildRelation() { + val session = commonTestHelper.createAccount("Jhon", SessionTestParams(true)) + val spaceName = "My Space" + val topic = "A public space for test" + val spaceId: String + runBlocking { + spaceId = session.spaceService().createSpace(spaceName, topic, null, true) + // wait a bit to let the summry update it self :/ + delay(400) + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + + val roomId = commonTestHelper.doSync { + session.createRoom(CreateRoomParams().apply { name = "General" }, it) + } + + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, true) + } + + runBlocking { + session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) + } + + Thread.sleep(9000) + + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + + parents?.forEach { + Log.d("## TEST", "parent : $it") + } + + assertNotNull(parents) + assertEquals(1, parents.size) + assertEquals(spaceName, parents.first().roomSummary?.name) + + assertNotNull(canonicalParents) + assertEquals(1, canonicalParents.size) + assertEquals(spaceName, canonicalParents.first().roomSummary?.name) + } + + @Test + fun testCreateChildRelations() { + val session = commonTestHelper.createAccount("Jhon", SessionTestParams(true)) + val spaceName = "My Space" + val topic = "A public space for test" + Log.d("## TEST", "Before") + val spaceId = runBlocking { + session.spaceService().createSpace(spaceName, topic, null, true) + } + + Log.d("## TEST", "created space $spaceId ${Thread.currentThread()}") + val syncedSpace = session.spaceService().getSpace(spaceId) + + val children = listOf("General" to true /*canonical*/, "Random" to false) + + val roomIdList = children.map { + commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = it.first }, cb) + } to it.second + } + + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + runBlocking { + roomIdList.forEach { entry -> + syncedSpace!!.addChildren(entry.first, viaServers, null, true) + } + } + + runBlocking { + roomIdList.forEach { + session.spaceService().setSpaceParent(it.first, spaceId, it.second, viaServers) + } + delay(400) + } + + roomIdList.forEach { + val parents = session.getRoom(it.first)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(it.first)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + + assertNotNull(parents) + assertEquals(1, parents.size, "Unexpected number of parent") + assertEquals(spaceName, parents.first().roomSummary?.name, "Unexpected parent name ") + assertEquals(if (it.second) 1 else 0, canonicalParents?.size ?: 0, "Parent of ${it.first} should be canonical ${it.second}") + } + } + + @Test + fun testFilteringBySpace() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + // Create orphan rooms + + val orphan1 = commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = "O1" }, cb) + } + val orphan2 = commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = "O2" }, cb) + } + + val allRooms = session.getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) + + assertEquals(9, allRooms.size, "Unexpected number of rooms") + + val orphans = session.getFlattenRoomSummaryChildOf(null) + + assertEquals(2, orphans.size, "Unexpected number of orphan rooms") + assertTrue(orphans.indexOfFirst { it.roomId == orphan1 } != -1, "O1 should be an orphan") + assertTrue(orphans.indexOfFirst { it.roomId == orphan2 } != -1, "O2 should be an orphan ${orphans.map { it.name }}") + + val aChildren = session.getFlattenRoomSummaryChildOf(spaceAInfo.spaceId) + + assertEquals(4, aChildren.size, "Unexpected number of flatten child rooms") + assertTrue(aChildren.indexOfFirst { it.name == "A1" } != -1, "A1 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "A2" } != -1, "A2 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C1" } != -1, "CA should be a grand child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C2" } != -1, "A1 should be a grand child of A") + + // Add a non canonical child and check that it does not appear as orphan + val a3 = commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = "A3" }, cb) + } + runBlocking { + spaceA!!.addChildren(a3, viaServers, null, false) + delay(400) + // here we do not set the parent!! + } + + val orphansUpdate = session.getFlattenRoomSummaryChildOf(null) + assertEquals(2, orphansUpdate.size, "Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}") + } + + @Test + fun testBreakCycle() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + // add back A as subspace of C + runBlocking { + val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) + spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) + } + + Thread.sleep(1000) + + // A -> C -> A + + val aChildren = session.getFlattenRoomSummaryChildOf(spaceAInfo.spaceId) + + assertEquals(4, aChildren.size, "Unexpected number of flatten child rooms ${aChildren.map { it.name }}") + assertTrue(aChildren.indexOfFirst { it.name == "A1" } != -1, "A1 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "A2" } != -1, "A2 should be a child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C1" } != -1, "CA should be a grand child of A") + assertTrue(aChildren.indexOfFirst { it.name == "C2" } != -1, "A1 should be a grand child of A") + } + + @Test + fun testLiveFlatChildren() { + val session = commonTestHelper.createAccount("John", SessionTestParams(true)) + + val spaceAInfo = createPublicSpace(session, "SpaceA", listOf( + Triple("A1", true /*auto-join*/, true/*canonical*/), + Triple("A2", true, true) + )) + + val spaceBInfo = createPublicSpace(session, "SpaceB", listOf( + Triple("B1", true /*auto-join*/, true/*canonical*/), + Triple("B2", true, true), + Triple("B3", true, true) + )) + + // add B as a subspace of A + val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + runBlocking { + spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) + } + + val flatAChildren = runBlocking(Dispatchers.Main) { + session.getFlattenRoomSummaryChildOfLive(spaceAInfo.spaceId) + } + + commonTestHelper.waitWithLatch { latch -> + + val childObserver = object : Observer> { + override fun onChanged(children: List?) { +// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") + System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") + if (children?.indexOfFirst { it.name == "C1" } != -1 + && children?.indexOfFirst { it.name == "C2" } != -1 + ) { + // B1 has been added live! + latch.countDown() + flatAChildren.removeObserver(this) + } + } + } + + val spaceCInfo = createPublicSpace(session, "SpaceC", listOf( + Triple("C1", true /*auto-join*/, true/*canonical*/), + Triple("C2", true, true) + )) + + // add C as subspace of B + runBlocking { + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + } + + // C1 and C2 should be in flatten child of A now + + GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } + } + + // Test part one of the rooms + + val bRoomId = spaceBInfo.roomIds.first() + val bRoom = session.getRoom(bRoomId) + + commonTestHelper.waitWithLatch { latch -> + + val childObserver = object : Observer> { + override fun onChanged(children: List?) { + System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") + if (children?.any { it.roomId == bRoomId } == false) { + // B1 has been added live! + latch.countDown() + flatAChildren.removeObserver(this) + } + } + } + + // part from b room + commonTestHelper.doSync { + bRoom!!.leave(null, it) + } + // The room should have disapear from flat children + GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) } + } + } + + data class TestSpaceCreationResult( + val spaceId: String, + val roomIds: List + ) + + private fun createPublicSpace(session: Session, + spaceName: String, + childInfo: List> + /** Name, auto-join, canonical*/ + ): TestSpaceCreationResult { + val spaceId = runBlocking { + session.spaceService().createSpace(spaceName, "Test Topic", null, true) + } + + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + + val roomIds = + childInfo.map { entry -> + commonTestHelper.doSync { cb -> + session.createRoom(CreateRoomParams().apply { name = entry.first }, cb) + } + } + + roomIds.forEachIndexed { index, roomId -> + runBlocking { + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) + } + } + } + return TestSpaceCreationResult(spaceId, roomIds) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 257c83564e..5f2bc716f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -34,6 +34,8 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional /** @@ -90,5 +92,6 @@ interface Room : limit: Int, beforeLimit: Int, afterLimit: Int, +// fun getSpaceParents(): List includeProfile: Boolean): SearchResult } 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 22045366cb..5296ad58b0 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 @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.paging.PagedList 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.Membership 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 @@ -197,4 +198,8 @@ interface RoomService { .setEnablePlaceholders(false) .setPrefetchDistance(10) .build() + + fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List = Membership.activeMemberships()) : List + + fun getFlattenRoomSummaryChildOfLive(spaceId: String?, memberships: List = Membership.activeMemberships()): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index c8d52302e9..47618e31c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -21,11 +21,27 @@ import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { return RoomSummaryQueryParams.Builder().apply(init).build() } +fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams { + return RoomSummaryQueryParams.Builder() + .apply(init) + .apply { + this.includeType = listOf(RoomType.SPACE) + this.excludeType = null + this.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + }.build() +} + +enum class RoomCategoryFilter { + ONLY_DM, + ONLY_ROOMS, + ALL +} /** * This class can be used to filter room summaries to use with: * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] @@ -37,7 +53,8 @@ data class RoomSummaryQueryParams( val memberships: List, val roomCategoryFilter: RoomCategoryFilter?, val roomTagQueryFilter: RoomTagQueryFilter? - val excludeType: List + val excludeType: List?, + val includeType: List? ) { class Builder { @@ -49,6 +66,7 @@ data class RoomSummaryQueryParams( var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL var roomTagQueryFilter: RoomTagQueryFilter? = null var excludeType: List = listOf(RoomType.SPACE) + var includeType: List? = null fun build() = RoomSummaryQueryParams( roomId = roomId, @@ -56,8 +74,9 @@ data class RoomSummaryQueryParams( canonicalAlias = canonicalAlias, memberships = memberships, roomCategoryFilter = roomCategoryFilter, - roomTagQueryFilter = roomTagQueryFilter - excludeType = excludeType + roomTagQueryFilter = roomTagQueryFilter, + excludeType = excludeType, + includeType = includeType ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index ac87a16911..f08f605a24 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -27,18 +27,18 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService] */ data class RoomSummary constructor( - override val roomId: String, + val roomId: String, // Computed display name - override val displayName: String = "", - override val name: String = "", - override val topic: String = "", - override val avatarUrl: String = "", - override val canonicalAlias: String? = null, - override val aliases: List = emptyList(), - override val joinedMembersCount: Int? = 0, - override val invitedMembersCount: Int? = 0, + val displayName: String = "", + val name: String = "", + val topic: String = "", + val avatarUrl: String = "", + val canonicalAlias: String? = null, + val aliases: List = emptyList(), + val joinedMembersCount: Int? = 0, + val invitedMembersCount: Int? = 0, val latestPreviewableEvent: TimelineEvent? = null, - override val otherMemberIds: List = emptyList(), + val otherMemberIds: List = emptyList(), val isDirect: Boolean = false, val notificationCount: Int = 0, val highlightCount: Int = 0, @@ -55,8 +55,10 @@ data class RoomSummary constructor( val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, val hasFailedSending: Boolean = false, - override val roomType: String? = null -) : IRoomSummary { + val roomType: String? = null, + val spaceParents: List? = null, + val children: List? = null +) { val isVersioned: Boolean get() = versioningState != VersioningState.NONE diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 38d9f1e74e..1302a34daa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -17,8 +17,16 @@ package org.matrix.android.sdk.api.session.room.model data class SpaceChildInfo( - val roomSummary: IRoomSummary?, + val childRoomId: String, + // We might not know this child at all, + // i.e we just know it exists but no info on type/name/etc.. + val isKnown: Boolean, + val roomType: String?, + val name: String?, + val topic: String?, + val avatarUrl: String?, val order: String?, + val activeMemberCount: Int?, val autoJoin: Boolean, val viaServers: List ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt similarity index 61% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt index 1724f00c99..5ed81b0646 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/IRoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -16,16 +16,9 @@ package org.matrix.android.sdk.api.session.room.model -interface IRoomSummary { - val roomId: String - val displayName: String - val name: String - val topic: String - val avatarUrl: String - val canonicalAlias: String? - val aliases: List - val joinedMembersCount: Int? - val invitedMembersCount: Int? - val otherMemberIds: List - val roomType: String? -} +data class SpaceParentInfo( + val parentId: String?, + val roomSummary: RoomSummary?, + val canonical: Boolean?, + val viaServers: List +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt index 0172b3701b..cfbffc128c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt @@ -24,9 +24,9 @@ interface Space { fun asRoom() : Room /** - * A current snapshot of [RoomSummary] associated with the room + * A current snapshot of [RoomSummary] associated with the space */ - fun spaceSummary(): SpaceSummary? + fun spaceSummary(): RoomSummary? suspend fun addChildren(roomId: String, viaServers: List, order: String?, autoJoin: Boolean = false) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index 3e30f14748..15b6f7d852 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -60,9 +60,9 @@ interface SpaceService { * Get a live list of space summaries. This list is refreshed as soon as the data changes. * @return the [LiveData] of List[SpaceSummary] */ - fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> - fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List sealed class JoinSpaceResult { object Success : JoinSpaceResult() @@ -79,4 +79,14 @@ interface SpaceService { viaServers: List = emptyList()): JoinSpaceResult suspend fun rejectInvite(spaceId: String, reason: String?) + +// fun getSpaceParentsOfRoom(roomId: String) : List + + /** + * Let this room declare that it has a parent. + * @param canonical true if it should be the main parent of this room + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering. + */ + suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt deleted file mode 100644 index 1473ed7a96..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceSummary.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.space - -import org.matrix.android.sdk.api.session.room.model.IRoomSummary -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo - -data class SpaceSummary( - val spaceId: String, - val roomSummary: RoomSummary, - val children: List -) : IRoomSummary by roomSummary diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt index b3f7267580..871a494914 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt @@ -26,7 +26,6 @@ import com.squareup.moshi.JsonClass * "state_key": "!space:example.com", * "content": { * "via": ["example.com"], - * "present": true, * "canonical": true, * } * } @@ -38,11 +37,6 @@ data class SpaceParentContent( * Parents where via is not present are ignored. */ @Json(name = "via") val via: List? = null, - /** - * present: true key is included to distinguish from a deleted state event - * Parent where present is not present (sic) or is not set to true are ignored. - */ - @Json(name = "present") val present: Boolean? = false, /** * Canonical determines whether this is the main parent for the space. * When a user joins a room with a canonical parent, clients may switch to view the room diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index a792248764..a904b43faa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -22,7 +22,6 @@ 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.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.sender.SenderInfo -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.session.user.model.User import java.util.Locale @@ -152,8 +151,6 @@ fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatar fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) -fun SpaceSummary.toMatrixItem() = MatrixItem.RoomItem(spaceId, displayName, avatarUrl) - // If no name is available, use room alias as Riot-Web does fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index b14ed9e9c7..9dd8cda8b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm -import io.realm.FieldAttribute import io.realm.RealmMigration import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields @@ -31,8 +30,8 @@ import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.model.SpaceChildInfoEntityFields -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -200,21 +199,34 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { fun migrateTo10(realm: DynamicRealm) { Timber.d("Step 9 -> 10") + realm.schema.create("SpaceChildSummaryEntity") + ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java) + ?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java) + ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true) + ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + +// realm.schema.create("SpaceSummaryEntity") +// ?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY) +// ?.setRequired(SpaceSummaryEntityFields.SPACE_ID, true) +// ?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) +// ?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!) + + realm.schema.create("SpaceParentSummaryEntity") + ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java) + ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java) + ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true) +// ?.addRealmListField(RoomParentRelationInfoEntityFields.VIA_SERVERS.`$`, String::class.java) +// ?.addRealmObjectField(RoomParentRelationInfoEntityFields.SPACE_SUMMARY_ENTITY.`$`, realm.schema.get("SpaceSummaryEntity")!!) + ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) + realm.schema.get("RoomSummaryEntity") ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java) ?.transform { obj -> // Should I put messaging type here? obj.setString(RoomSummaryEntityFields.ROOM_TYPE, null) } - - val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity") - ?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java) - ?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java) - ?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) - - realm.schema.create("SpaceSummaryEntity") - ?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY) - ?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) - ?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!) + ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!) + ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index c74eb4460d..410500ed8e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker @@ -64,7 +66,29 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, inviterId = roomSummaryEntity.inviterId, hasFailedSending = roomSummaryEntity.hasFailedSending, - roomType = roomSummaryEntity.roomType + roomType = roomSummaryEntity.roomType, + spaceParents = roomSummaryEntity.parents.map { relationInfoEntity -> + SpaceParentInfo( + parentId = relationInfoEntity.parentRoomId, + roomSummary = relationInfoEntity.parentSummaryEntity?.let { map(it) }, + canonical = relationInfoEntity.canonical ?: false, + viaServers = relationInfoEntity.viaServers.toList() + ) + }, + children = roomSummaryEntity.children.map { + SpaceChildInfo( + childRoomId = it.childRoomId ?: "", + isKnown = it.childSummaryEntity != null, + roomType = it.childSummaryEntity?.roomType, + name = it.childSummaryEntity?.name, + topic = it.childSummaryEntity?.topic, + avatarUrl = it.childSummaryEntity?.avatarUrl, + activeMemberCount = it.childSummaryEntity?.joinedMembersCount, + order = it.order, + autoJoin = it.autoJoin ?: false, + viaServers = it.viaServers.toList() + ) + } ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt deleted file mode 100644 index d08528598d..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/SpaceSummaryMapper.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.database.mapper - -import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo -import org.matrix.android.sdk.api.session.space.SpaceSummary -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity -import javax.inject.Inject - -internal class SpaceSummaryMapper @Inject constructor(private val roomSummaryMapper: RoomSummaryMapper) { - - fun map(spaceSummaryEntity: SpaceSummaryEntity): SpaceSummary { - return SpaceSummary( - spaceId = spaceSummaryEntity.spaceId, - roomSummary = roomSummaryMapper.map(spaceSummaryEntity.roomSummaryEntity!!), - children = spaceSummaryEntity.children.map { - SpaceChildInfo( - roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) }, - autoJoin = it.autoJoin ?: false, - viaServers = it.viaServers.map { it }, - order = it.order - ) - } - ) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 3ff2532604..58297776f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", set(value) { membersLoadStatusStr = value.name } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index c87ac15a78..85e4595da5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -28,6 +28,9 @@ import org.matrix.android.sdk.api.session.room.model.tag.RoomTag internal open class RoomSummaryEntity( @PrimaryKey var roomId: String = "" + var roomType: String? = null, + var parents: RealmList = RealmList(), + var children: RealmList = RealmList() ) : RealmObject() { var displayName: String? = "" @@ -244,6 +247,5 @@ internal open class RoomSummaryEntity( roomEncryptionTrustLevelStr = value?.name } } - companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 76116be1a8..72ae512fa5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -62,7 +62,7 @@ import io.realm.annotations.RealmModule UserAccountDataEntity::class, ScalarTokenEntity::class, WellknownIntegrationManagerConfigEntity::class, - SpaceSummaryEntity::class, - SpaceChildInfoEntity::class + SpaceChildSummaryEntity::class, + SpaceParentSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt similarity index 69% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt index 7862207901..982c9ece6a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt @@ -22,14 +22,22 @@ import io.realm.RealmObject /** * Decorates room summary with space related information. */ -internal open class SpaceChildInfoEntity( - var viaServers: RealmList = RealmList(), - // Use for alphabetic ordering of this child +internal open class SpaceChildSummaryEntity( +// var isSpace: Boolean = false, + var order: String? = null, - // If true, this child should be join when parent is joined + var autoJoin: Boolean? = null, - // link to the actual room (check type to see if it's a subspace) - var roomSummaryEntity: RoomSummaryEntity? = null + + var childRoomId: String? = null, + // Link to the actual space summary if it is known locally + var childSummaryEntity: RoomSummaryEntity? = null, + + var viaServers: RealmList = RealmList() +// var owner: RoomSummaryEntity? = null, + +// var level: Int = 0 + ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt new file mode 100644 index 0000000000..af32cd2b83 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt @@ -0,0 +1,49 @@ +/* + * 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Decorates room summary with space related information. + */ +internal open class SpaceParentSummaryEntity( + /** + * Determines whether this is the main parent for the space + * When a user joins a room with a canonical parent, clients may switch to view the room in the context of that space, + * peeking into it in order to find other rooms and group them together. + * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced: + * if multiple are present the client should select the one with the lowest room ID, + * as determined via a lexicographic utf-8 ordering. + */ + var canonical: Boolean? = null, + + var parentRoomId: String? = null, + // Link to the actual space summary if it is known locally + var parentSummaryEntity: RoomSummaryEntity? = null, + + var viaServers: RealmList = RealmList() + +// var child: RoomSummaryEntity? = null, + +// var level: Int = 0 + +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt deleted file mode 100644 index e63b5b9d55..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceSummaryEntity.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.database.model - -import io.realm.RealmList -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey - -internal open class SpaceSummaryEntity(@PrimaryKey var spaceId: String = "", - var roomSummaryEntity: RoomSummaryEntity? = null, - var children: RealmList = RealmList() - // TODO public / private .. and more -) : RealmObject() { - - // Do we want to denormalize that ? - -// private var membershipStr: String = Membership.NONE.name -// var membership: Membership -// get() { -// return Membership.valueOf(membershipStr) -// } -// set(value) { -// membershipStr = value.name -// } - - companion object -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt deleted file mode 100644 index b6403c596f..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/SpaceSummaryEntityQueries.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.database.query - -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.kotlin.createObject -import io.realm.kotlin.where -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields - -internal fun SpaceSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { - val query = realm.where() - query.isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`) - if (roomId != null) { - query.equalTo(SpaceSummaryEntityFields.SPACE_ID, roomId) - } - query.sort(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.DISPLAY_NAME) - return query -} - -internal fun SpaceSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): SpaceSummaryEntity? { - val spaceSummary = realm.where() - .isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`) - .equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, roomAlias) - .findFirst() - if (spaceSummary != null) { - return spaceSummary - } - return realm.where() - .isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`) - .contains(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.FLAT_ALIASES, "|$roomAlias") - .findFirst() -} - -internal fun SpaceSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): SpaceSummaryEntity { - return where(realm, roomId).findFirst() ?: realm.createObject(roomId).also { - it.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 1d8eb6c95e..1c22faa7b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.members.MembershipService import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService import org.matrix.android.sdk.api.session.room.read.ReadService @@ -36,11 +37,16 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.session.room.state.SendStateTask import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.search.SearchTask +import org.matrix.android.sdk.internal.session.space.DefaultSpace +import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.awaitCallback import java.security.InvalidParameterException import javax.inject.Inject @@ -148,4 +154,9 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, ) ) } + + override fun asSpace(): Space? { + if (roomSummary()?.roomType != RoomType.SPACE) return null + return DefaultSpace(this, roomSummaryDataSource) + } } 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 4724167e87..cd39e633dd 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 @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult 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.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams @@ -33,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase @@ -63,7 +65,9 @@ internal class DefaultRoomService @Inject constructor( private val peekRoomTask: PeekRoomTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, - private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource + private val roomSummaryMapper: RoomSummaryMapper, + private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, + private val taskExecutor: TaskExecutor ) : RoomService { override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable { @@ -168,4 +172,18 @@ internal class DefaultRoomService @Inject constructor( override suspend fun peekRoom(roomIdOrAlias: String): PeekResult { return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias)) } + + override fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List): List { + if (spaceId == null) { + return roomSummaryDataSource.getFlattenOrphanRooms() + } + return roomSummaryDataSource.getAllRoomSummaryChildOf(spaceId, memberships) + } + + override fun getFlattenRoomSummaryChildOfLive(spaceId: String?, memberships: List): LiveData> { + if (spaceId == null) { + return roomSummaryDataSource.getFlattenOrphanRoomsLive() + } + return roomSummaryDataSource.getAllRoomSummaryChildOfLive(spaceId, memberships) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt index f440a67710..0f64bb60ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.space.Space +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.space.DefaultSpace -import org.matrix.android.sdk.internal.session.space.SpaceSummaryDataSource import javax.inject.Inject internal interface SpaceGetter { @@ -28,7 +28,7 @@ internal interface SpaceGetter { internal class DefaultSpaceGetter @Inject constructor( private val roomGetter: RoomGetter, - private val spaceSummaryDataSource: SpaceSummaryDataSource + private val spaceSummaryDataSource: RoomSummaryDataSource ) : SpaceGetter { override fun get(spaceId: String): Space? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt similarity index 64% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt index c04f5d3948..06ab21d8db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomRelationshipHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt @@ -20,6 +20,7 @@ import io.realm.Realm 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.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceParentContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.query.whereType @@ -35,8 +36,8 @@ import timber.log.Timber * * - Separately, rooms can claim parents via the m.room.parent state event: */ -internal class RoomRelationshipHelper(private val realm: Realm, - private val roomId: String +internal class RoomChildRelationInfo(private val realm: Realm, + private val roomId: String ) { data class SpaceChildInfo( @@ -46,15 +47,24 @@ internal class RoomRelationshipHelper(private val realm: Realm, val viaServers: List ) + data class SpaceParentInfo( + val roomId: String, + val canonical: Boolean, + val viaServers: List, + val stateEventSender: String + ) + /** * Gets the ordered list of valid child description. */ fun getDirectChildrenDescriptions(): List { return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) - .findAll() + .findAll().also { + Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId") + } .mapNotNull { ContentMapper.map(it.root?.content).toModel()?.let { scc -> - Timber.d("## Space child desc state event $scc") + Timber.v("## Space child desc state event $scc") // Children where via is not present are ignored. scc.via?.let { via -> SpaceChildInfo( @@ -68,4 +78,25 @@ internal class RoomRelationshipHelper(private val realm: Realm, } .sortedBy { it.order } } + + fun getParentDescriptions(): List { + return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT) + .findAll().also { + Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId") + } + .mapNotNull { + ContentMapper.map(it.root?.content).toModel()?.let { scc -> + Timber.v("## Space parent desc state event $scc") + // Parent where via is not present are ignored. + scc.via?.let { via -> + SpaceParentInfo( + roomId = it.stateKey, + canonical = scc.canonical ?: false, + viaServers = via, + stateEventSender = it.root?.sender ?: "" + ) + } + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt new file mode 100644 index 0000000000..ced1ad963e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt @@ -0,0 +1,67 @@ +/* + * 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.internal.session.room.summary + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.Optional + +internal class HierarchyLiveDataHelper( + val spaceId: String, + val memberships: List, + val roomSummaryDataSource: RoomSummaryDataSource) { + + private val sources = HashMap>>() + private val mediatorLiveData = MediatorLiveData>() + + fun liveData() = mediatorLiveData + + init { + onChange() + } + + private fun parentsToCheck(): List { + val spaces = ArrayList() + roomSummaryDataSource.getSpaceSummary(spaceId)?.let { + roomSummaryDataSource.flattenSubSpace(it, emptyList(), spaces, memberships) + } + return spaces + } + + private fun onChange() { + val existingSources = sources.keys.toList() + val newSources = parentsToCheck().map { it.roomId } + val addedSources = newSources.filter { !existingSources.contains(it) } + val removedSource = existingSources.filter { !newSources.contains(it) } + addedSources.forEach { + val liveData = roomSummaryDataSource.getSpaceSummaryLive(it) + mediatorLiveData.addSource(liveData) { onChange() } + sources[it] = liveData + } + + removedSource.forEach { + sources[it]?.let { mediatorLiveData.removeSource(it) } + } + + sources[spaceId]?.value?.getOrNull()?.let { spaceSummary -> + val results = ArrayList() + roomSummaryDataSource.flattenChild(spaceSummary, emptyList(), results, memberships) + mediatorLiveData.postValue(results.map { it.roomId }) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 576e7f4eba..2468661ada 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -1,5 +1,6 @@ /* * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. @@ -27,9 +28,17 @@ import io.realm.Sort import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.RoomCategoryFilter +import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams +import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper @@ -84,6 +93,36 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat ) } + fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { + return getRoomSummariesLive(queryParams) + } + + fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? { + return getRoomSummary(roomIdOrAlias).let { + it?.takeIf { it.roomType == RoomType.SPACE } + } + } + + fun getSpaceSummaryLive(roomId: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> + RoomSummaryEntity.where(realm, roomId) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE) + }, + { + roomSummaryMapper.map(it) + } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + + fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { + return getRoomSummaries(spaceSummaryQueryParams) + } + fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List { return monarchy.fetchAllMappedSync( { breadcrumbsQuery(it, queryParams) }, @@ -190,9 +229,134 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat } } - queryParams.excludeType.forEach { + queryParams.excludeType?.forEach { query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it) } + queryParams.includeType?.forEach { + query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it) + } + queryParams.roomCategoryFilter?.let { + when (it) { + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ALL -> { + // nop + } + } + } return query } + + fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List): List { + val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList() + val result = ArrayList() + flattenChild(space, emptyList(), result, memberShips) + return result + } + + fun getAllRoomSummaryChildOfLive(spaceId: String, memberShips: List): LiveData> { + // we want to listen to all spaces in hierarchy and on change compute back all childs + // and switch map to listen thoose? + val mediatorLiveData = HierarchyLiveDataHelper(spaceId, memberShips, this).liveData() + + return Transformations.switchMap(mediatorLiveData) { allIds -> + monarchy.findAllMappedWithChanges( + { + it.where() + .`in`(RoomSummaryEntityFields.ROOM_ID, allIds.toTypedArray()) + .`in`(RoomSummaryEntityFields.MEMBERSHIP_STR, memberShips.map { it.name }.toTypedArray()) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + }, + { + roomSummaryMapper.map(it) + }) + } + } + + fun getFlattenOrphanRooms(): List { + return getRoomSummaries(roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + }).filter { + // we need to check if orphan + isOrphan(it) + } + } + + fun getFlattenOrphanRoomsLive(): LiveData> { + return Transformations.map( + getRoomSummariesLive(roomSummaryQueryParams { + memberships = Membership.activeMemberships() + excludeType = listOf(RoomType.SPACE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + })) { + it.filter { + isOrphan(it) + } + } + } + + private fun isOrphan(roomSummary: RoomSummary): Boolean { + if (roomSummary.roomType == RoomType.SPACE && roomSummary.membership.isActive()) { + return false + } + // all parents line should be orphan + roomSummary.spaceParents?.forEach { info -> + if (info.roomSummary != null && !info.roomSummary.membership.isLeft()) { + if (!isOrphan(info.roomSummary)) { + return false + } + } + } + + // it may not have a parent relation but could be a child of some other.... + for (spaceSummary in getSpaceSummaries(spaceSummaryQueryParams { memberships = Membership.activeMemberships() })) { + if (spaceSummary.children?.any { it.childRoomId == roomSummary.roomId } == true) { + return false + } + } + + return true + } + + fun flattenChild(current: RoomSummary, parenting: List, output: MutableList, memberShips: List) { + current.children?.sortedBy { it.order ?: it.name }?.forEach { childInfo -> + if (childInfo.roomType == RoomType.SPACE) { + // Add recursive + if (!parenting.contains(childInfo.childRoomId)) { // avoid cycles! + getSpaceSummary(childInfo.childRoomId)?.let { subSpace -> + if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) { + flattenChild(subSpace, parenting + listOf(current.roomId), output, memberShips) + } + } + } + } else if (childInfo.isKnown) { + getRoomSummary(childInfo.childRoomId)?.let { + if (memberShips.isEmpty() || memberShips.contains(it.membership)) { + if (!it.isDirect) { + output.add(it) + } + } + } + } + } + } + + fun flattenSubSpace(current: RoomSummary, parenting: List, output: MutableList, memberShips: List) { + output.add(current) + current.children?.sortedBy { it.order ?: it.name }?.forEach { + if (it.roomType == RoomType.SPACE) { + // Add recursive + if (!parenting.contains(it.childRoomId)) { // avoid cycles! + getSpaceSummary(it.childRoomId)?.let { subSpace -> + if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) { + output.add(subSpace) + flattenSubSpace(subSpace, parenting + listOf(current.roomId), output, memberShips) + } + } + } + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 3d01021811..e7cb9688dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -39,19 +39,22 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.SpaceChildInfoEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntity +import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.isEventRead +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.session.room.relationship.RoomRelationshipHelper +import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber @@ -62,7 +65,8 @@ internal class RoomSummaryUpdater @Inject constructor( private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, private val eventDecryptor: EventDecryptor, - private val crossSigningService: DefaultCrossSigningService) { + private val crossSigningService: DefaultCrossSigningService, + private val stateEventDataSource: StateEventDataSource) { fun update(realm: Realm, roomId: String, @@ -163,28 +167,6 @@ internal class RoomSummaryUpdater @Inject constructor( crossSigningService.onUsersDeviceUpdate(otherRoomMembers) } } - - if (roomType == RoomType.SPACE) { - Timber.v("## Space: Updating summary for Space $roomId membership: ${roomSummaryEntity.membership}") - val spaceSummaryEntity = SpaceSummaryEntity() - spaceSummaryEntity.spaceId = roomId - spaceSummaryEntity.roomSummaryEntity = roomSummaryEntity - spaceSummaryEntity.children.clear() - spaceSummaryEntity.children.addAll( - RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions() - .map { - Timber.v("## Space: Updating summary for room $roomId with info $it") - realm.createObject().apply { - this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId) - this.order = it.order - this.autoJoin = it.autoJoin - }.also { - Timber.v("## Space: Updating summary for room $roomId with children $it") - } - } - ) - realm.insertOrUpdate(spaceSummaryEntity) - } } private fun RoomSummaryEntity.updateHasFailedSending() { @@ -196,4 +178,55 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } + + /** + * Should be called at the end of the room sync, to check and validate all parent/child relations + */ + fun validateSpaceRelationship(realm: Realm) { + // Do level 0 stuffs + + realm.where(RoomSummaryEntity::class.java).findAll().forEach { roomSummary -> + if (roomSummary.roomType == RoomType.SPACE) { + roomSummary.children.clearWith { it.deleteFromRealm() } + roomSummary.children.addAll( + RoomChildRelationInfo(realm, roomSummary.roomId).getDirectChildrenDescriptions() + .map { + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with info $it") + realm.createObject().apply { + this.childRoomId = it.roomId + this.childSummaryEntity = RoomSummaryEntity.where(realm, it.roomId).findFirst() + this.order = it.order + this.autoJoin = it.autoJoin + this.viaServers.addAll(it.viaServers) +// this.level = 0 + }.also { + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with children $it") + } + } + ) + } + + // check parents + roomSummary.parents.clearWith { it.deleteFromRealm() } + roomSummary.parents.addAll( + RoomChildRelationInfo(realm, roomSummary.roomId).getParentDescriptions() + .map { parentInfo -> + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with parent info $parentInfo") + realm.createObject().apply { + this.parentRoomId = parentInfo.roomId + this.parentSummaryEntity = RoomSummaryEntity.where(realm, parentInfo.roomId).findFirst() + this.canonical = parentInfo.canonical + this.viaServers.addAll(parentInfo.viaServers) +// this.level = 0 + }.also { + Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with parent $it") + } + } + ) + } + } + +// private fun isValidCanonical() : Boolean { +// +// } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt index 5f174587d0..826fa76f77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/CreateSpaceTask.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.TimeoutCancellationException import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.task.Task @@ -44,8 +44,8 @@ internal class DefaultCreateSpaceTask @Inject constructor( try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - realm.where(SpaceSummaryEntity::class.java) - .equalTo(SpaceSummaryEntityFields.SPACE_ID, spaceId) + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, spaceId) } } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout(spaceId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt index efba103ab7..13b9465d58 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -21,17 +21,18 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.space.Space -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource -internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: SpaceSummaryDataSource) : Space { +internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: RoomSummaryDataSource) : Space { override fun asRoom(): Room { return room } - override fun spaceSummary(): SpaceSummary? { + override fun spaceSummary(): RoomSummary? { return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId) } 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 0210c81e5d..cdd2673fde 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 @@ -19,26 +19,37 @@ package org.matrix.android.sdk.internal.session.space import android.net.Uri import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.session.space.SpaceService -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.session.space.model.SpaceChildContent +import org.matrix.android.sdk.api.session.space.model.SpaceParentContent import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.RoomGetter import org.matrix.android.sdk.internal.session.room.SpaceGetter import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult +import java.lang.IllegalArgumentException import javax.inject.Inject internal class DefaultSpaceService @Inject constructor( @SessionDatabase private val monarchy: Monarchy, + @UserId private val userId: String, private val createSpaceTask: CreateSpaceTask, // private val joinRoomTask: JoinRoomTask, private val joinSpaceTask: JoinSpaceTask, @@ -47,8 +58,9 @@ internal class DefaultSpaceService @Inject constructor( // private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, // private val roomIdByAliasTask: GetRoomIdByAliasTask, // private val deleteRoomAliasTask: DeleteRoomAliasTask, -// private val roomGetter: RoomGetter, - private val spaceSummaryDataSource: SpaceSummaryDataSource, + private val roomGetter: RoomGetter, + private val roomSummaryDataSource: RoomSummaryDataSource, + private val stateEventDataSource: StateEventDataSource, private val peekSpaceTask: PeekSpaceTask, private val resolveSpaceInfoTask: ResolveSpaceInfoTask, private val leaveRoomTask: LeaveRoomTask @@ -73,12 +85,12 @@ internal class DefaultSpaceService @Inject constructor( return spaceGetter.get(spaceId) } - override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { - return spaceSummaryDataSource.getRoomSummariesLive(queryParams) + override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData> { + return roomSummaryDataSource.getSpaceSummariesLive(queryParams) } - override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { - return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) + override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List { + return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) } override suspend fun peekSpace(spaceId: String): SpacePeekResult { @@ -108,21 +120,16 @@ internal class DefaultSpaceService @Inject constructor( ?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD } ?.content.toModel() SpaceChildInfo( - roomSummary = RoomSummary( - roomId = childSummary.roomId, - roomType = childSummary.roomType, - name = childSummary.name ?: "", - displayName = childSummary.name ?: "", - topic = childSummary.topic ?: "", - joinedMembersCount = childSummary.numJoinedMembers, - avatarUrl = childSummary.avatarUrl ?: "", - encryptionEventTs = null, - typingUsers = emptyList(), - isEncrypted = false - ), + childRoomId = childSummary.roomId, + isKnown = true, + roomType = childSummary.roomType, + name = childSummary.name, + topic = childSummary.topic, + avatarUrl = childSummary.avatarUrl, order = childStateEv?.order, autoJoin = childStateEv?.autoJoin ?: false, - viaServers = childStateEv?.via ?: emptyList() + viaServers = childStateEv?.via ?: emptyList(), + activeMemberCount = childSummary.numJoinedMembers ) } ?: emptyList() ) @@ -138,4 +145,42 @@ internal class DefaultSpaceService @Inject constructor( override suspend fun rejectInvite(spaceId: String, reason: String?) { 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? + if (canonical) { + // check that we can send m.child in the parent room + if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) { + throw UnsupportedOperationException("Cannot add canonical child if not member of parent") + } + val powerLevelsEvent = stateEventDataSource.getStateEvent( + roomId = parentSpaceId, + eventType = EventType.STATE_ROOM_POWER_LEVELS, + stateKey = QueryStringValue.NoCondition + ) + val powerLevelsContent = powerLevelsEvent?.content?.toModel() + ?: throw UnsupportedOperationException("Cannot add canonical child, not enough power level") + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { + throw UnsupportedOperationException("Cannot add canonical child, not enough power level") + } + } + + val room = roomGetter.getRoom(childRoomId) + ?: throw IllegalArgumentException("Unknown Room $childRoomId") + + room.sendStateEvent( + eventType = EventType.STATE_SPACE_PARENT, + stateKey = parentSpaceId, + body = SpaceParentContent( + via = viaServers, + canonical = canonical + ).toContent() + ) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt index 6ee3652761..9379c0d650 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt @@ -22,11 +22,12 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.internal.database.awaitNotEmptyResult -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask +import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.task.Task import timber.log.Timber import java.util.concurrent.TimeUnit @@ -45,7 +46,7 @@ internal class DefaultJoinSpaceTask @Inject constructor( private val joinRoomTask: JoinRoomTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, - private val spaceSummaryDataSource: SpaceSummaryDataSource + private val roomSummaryDataSource: RoomSummaryDataSource ) : JoinSpaceTask { override suspend fun execute(params: JoinSpaceTask.Params): SpaceService.JoinSpaceResult { @@ -65,15 +66,15 @@ internal class DefaultJoinSpaceTask @Inject constructor( Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...") try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm -> - realm.where(SpaceSummaryEntity::class.java) + realm.where(RoomSummaryEntity::class.java) .apply { if (params.roomIdOrAlias.startsWith("!")) { - equalTo(SpaceSummaryEntityFields.SPACE_ID, params.roomIdOrAlias) + equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias) } else { - equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, params.roomIdOrAlias) + equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, params.roomIdOrAlias) } } - .equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.MEMBERSHIP_STR, Membership.JOIN.name) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { Timber.w("## Space: > Error created with timeout") @@ -83,21 +84,21 @@ internal class DefaultJoinSpaceTask @Inject constructor( val errors = HashMap() Timber.v("## Space: > Sync done ...") // after that i should have the children (? do I need to paginate to get state) - val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) - Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}") + val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) + Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.children?.size}") summary?.children?.forEach { - val childRoomSummary = it.roomSummary ?: return@forEach - Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}") +// val childRoomSummary = it.roomSummary ?: return@forEach + Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}") if (it.autoJoin) { // I should try to join as well - if (childRoomSummary.roomType == RoomType.SPACE) { + if (it.roomType == RoomType.SPACE) { // recursively join auto-joined child of this space? - when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.roomSummary.roomId, null, it.viaServers))) { + when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) { SpaceService.JoinSpaceResult.Success -> { // nop } is SpaceService.JoinSpaceResult.Fail -> { - errors[it.roomSummary.roomId] = subspaceJoinResult.error + errors[it.childRoomId] = subspaceJoinResult.error } is SpaceService.JoinSpaceResult.PartialSuccess -> { errors.putAll(subspaceJoinResult.failedRooms) @@ -105,15 +106,15 @@ internal class DefaultJoinSpaceTask @Inject constructor( } } else { try { - Timber.v("## Space: Joining room child ${childRoomSummary.roomId}") + Timber.v("## Space: Joining room child ${it.childRoomId}") joinRoomTask.execute(JoinRoomTask.Params( - roomIdOrAlias = childRoomSummary.roomId, + roomIdOrAlias = it.childRoomId, reason = "Auto-join parent space", viaServers = it.viaServers )) } catch (failure: Throwable) { - errors[it.roomSummary.roomId] = failure - Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}") + errors[it.childRoomId] = failure + Timber.e("## Space: Failed to join room child ${it.childRoomId}") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt deleted file mode 100644 index c15e81c287..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryDataSource.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.space - -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations -import com.zhuinden.monarchy.Monarchy -import io.realm.Realm -import io.realm.RealmQuery -import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.model.VersioningState -import org.matrix.android.sdk.api.session.space.SpaceSummary -import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.database.mapper.SpaceSummaryMapper -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity -import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields -import org.matrix.android.sdk.internal.database.query.findByAlias -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.query.process -import org.matrix.android.sdk.internal.util.fetchCopyMap -import javax.inject.Inject - -internal class SpaceSummaryDataSource @Inject constructor( - @SessionDatabase private val monarchy: Monarchy, - private val spaceSummaryMapper: SpaceSummaryMapper -) { - - fun getSpaceSummary(roomIdOrAlias: String): SpaceSummary? { - return monarchy - .fetchCopyMap({ - if (roomIdOrAlias.startsWith("!")) { - // It's a roomId - SpaceSummaryEntity.where(it, roomId = roomIdOrAlias).findFirst() - } else { - // Assume it's a room alias - SpaceSummaryEntity.findByAlias(it, roomIdOrAlias) - } - }, { entity, _ -> - spaceSummaryMapper.map(entity) - }) - } - - fun getSpaceSummaryLive(roomId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { realm -> SpaceSummaryEntity.where(realm, roomId).isNotEmpty(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.DISPLAY_NAME) }, - { spaceSummaryMapper.map(it) } - ) - return Transformations.map(liveData) { results -> - results.firstOrNull().toOptional() - } - } - - fun getSpaceSummaries(queryParams: SpaceSummaryQueryParams): List { - return monarchy.fetchAllMappedSync( - { spaceSummariesQuery(it, queryParams) }, - { spaceSummaryMapper.map(it) } - ) - } - - fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData> { - return monarchy.findAllMappedWithChanges( - { spaceSummariesQuery(it, queryParams) }, - { spaceSummaryMapper.map(it) } - ) - } - - private fun spaceSummariesQuery(realm: Realm, queryParams: SpaceSummaryQueryParams): RealmQuery { - val query = SpaceSummaryEntity.where(realm) - query.process(SpaceSummaryEntityFields.SPACE_ID, queryParams.roomId) - query.process(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.DISPLAY_NAME, queryParams.displayName) - query.process(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, queryParams.canonicalAlias) - query.process(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.MEMBERSHIP_STR, queryParams.memberships) - query.notEqualTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) - return query - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 95fbb2f1b0..5a1de41b95 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -95,6 +95,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter) + + // post room sync validation + roomSummaryUpdater.validateSpaceRelationship(realm) } // PRIVATE METHODS ***************************************************************************** diff --git a/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt b/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt index d95251c271..e8293d7e99 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/SelectedSpaceDataSource.kt @@ -18,9 +18,9 @@ package im.vector.app.features.grouplist import arrow.core.Option import im.vector.app.core.utils.BehaviorDataSource -import org.matrix.android.sdk.api.session.space.SpaceSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject import javax.inject.Singleton @Singleton -class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource>(Option.empty()) +class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource>(Option.empty()) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 6bc3f27fd4..faf85124d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -55,7 +55,7 @@ import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState import org.matrix.android.sdk.api.session.group.model.GroupSummary -import org.matrix.android.sdk.api.session.space.SpaceSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import timber.log.Timber @@ -252,10 +252,10 @@ class HomeDetailFragment @Inject constructor( } } - private fun onSpaceChange(spaceSummary: SpaceSummary?) { + private fun onSpaceChange(spaceSummary: RoomSummary?) { spaceSummary?.let { // Use GlideApp with activity context to avoid the glideRequests to be paused - if (spaceSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { + if (spaceSummary.roomId == ALL_COMMUNITIES_GROUP_ID) { // Special case views.groupToolbarAvatarImageView.background = ContextCompat.getDrawable(requireContext(), R.drawable.space_home_background) views.groupToolbarAvatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index dd316dcece..d2ca7e9115 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -22,12 +22,11 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.session.sync.SyncState data class HomeDetailViewState( val groupSummary: Option = Option.empty(), - val spaceSummary: Option = Option.empty(), + val spaceSummary: Option = Option.empty(), val asyncRooms: Async> = Uninitialized, val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, val notificationCountCatchup: Int = 0, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 47bc60eb75..06541c1fc3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -60,19 +60,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, - EventType.REDACTION, EventType.STATE_ROOM_ALIASES, - EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_MAC, EventType.CALL_CANDIDATES, + EventType.KEY_VERIFICATION_MAC, EventType.CALL_REPLACES, EventType.CALL_SELECT_ANSWER, EventType.CALL_NEGOTIATE, EventType.REACTION, - EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params) + EventType.STATE_ROOM_POWER_LEVELS, + EventType.STATE_SPACE_CHILD, + EventType.STATE_SPACE_PARENT, + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 878cec0a07..71504daeff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -107,6 +107,8 @@ class NoticeEventFormatter @Inject constructor( EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, + EventType.STATE_SPACE_CHILD, + EventType.STATE_SPACE_PARENT, EventType.REDACTION -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 31dc537395..a7b5543157 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -212,6 +212,6 @@ class MatrixToRoomSpaceFragment @Inject constructor( } } - fun secondaryButtonClicked() = withState(sharedViewModel) { state -> + private fun secondaryButtonClicked() = withState(sharedViewModel) { _ -> } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 27c29ae42b..548f0b986d 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -111,7 +111,7 @@ class DefaultNavigator @Inject constructor( } sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId)?.spaceSummary()?.let { - Timber.d("## Nav: Switching to space $spaceId / ${it.roomSummary.name}") + Timber.d("## Nav: Switching to space $spaceId / ${it.name}") selectedSpaceDataSource.post(Option.just(it)) } ?: kotlin.run { Timber.d("## Nav: Failed to switch to space $spaceId") diff --git a/vector/src/main/java/im/vector/app/features/settings/SharedPreferenceLiveData.kt b/vector/src/main/java/im/vector/app/features/settings/SharedPreferenceLiveData.kt new file mode 100644 index 0000000000..08e83258a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/SharedPreferenceLiveData.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 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.app.features.settings + +import android.content.SharedPreferences +import androidx.lifecycle.LiveData + +abstract class SharedPreferenceLiveData(protected val sharedPrefs: SharedPreferences, + protected val key: String, + private val defValue: T) : LiveData() { + + private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == this.key) { + value = getValueFromPreferences(key, defValue) + } + } + + abstract fun getValueFromPreferences(key: String, defValue: T): T + + override fun onActive() { + super.onActive() + value = getValueFromPreferences(key, defValue) + sharedPrefs.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + } + + override fun onInactive() { + sharedPrefs.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener) + super.onInactive() + } + + companion object { + fun booleanLiveData(sharedPrefs: SharedPreferences, key: String, defaultValue: Boolean): SharedPreferenceLiveData { + return object : SharedPreferenceLiveData(sharedPrefs, key, defaultValue) { + override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean { + return this.sharedPrefs.getBoolean(key, defValue) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 222c8da6b7..4d1c9b5728 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -20,6 +20,7 @@ import android.media.RingtoneManager import android.net.Uri import android.provider.MediaStore import androidx.core.content.edit +import androidx.lifecycle.LiveData import com.squareup.seismic.ShakeDetector import im.vector.app.BuildConfig import im.vector.app.R @@ -312,6 +313,14 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_LABS_USE_SPACES, false) } + fun labSpacesLive(): LiveData { + return SharedPreferenceLiveData.booleanLiveData( + defaultPrefs, + SETTINGS_LABS_USE_SPACES, + false + ) + } + fun failFast(): Boolean { return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt index 6a4ea83d68..cc2ca4eb4d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsLabsFragment.kt @@ -17,6 +17,9 @@ package im.vector.app.features.settings import im.vector.app.R +import im.vector.app.core.preference.VectorSwitchPreference +import im.vector.app.features.MainActivity +import im.vector.app.features.MainActivityArgs import javax.inject.Inject class VectorSettingsLabsFragment @Inject constructor( @@ -27,6 +30,11 @@ class VectorSettingsLabsFragment @Inject constructor( override val preferenceXmlRes = R.xml.vector_settings_labs override fun bindPref() { - // Nothing to do + findPreference(VectorPreferences.SETTINGS_LABS_USE_SPACES)!!.let { pref -> + pref.setOnPreferenceChangeListener { _, _ -> + MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = false)) + true + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt index d3fb225083..2a6e3dace9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/ShareSpaceBottomSheet.kt @@ -61,7 +61,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment?, selected: SpaceSummary?) { + private fun buildGroupModels(summaries: List?, selected: RoomSummary?) { if (summaries.isNullOrEmpty()) { return } // show invites on top - summaries.filter { it.roomSummary.membership == Membership.INVITE } + summaries.filter { it.membership == Membership.INVITE } .let { invites -> if (invites.isNotEmpty()) { genericItemHeader { @@ -67,7 +67,7 @@ class SpaceSummaryController @Inject constructor( invites.forEach { spaceSummaryItem { avatarRenderer(avatarRenderer) - id(it.spaceId) + id(it.roomId) matrixItem(it.toMatrixItem()) selected(false) listener { callback?.onSpaceSelected(it) } @@ -87,19 +87,19 @@ class SpaceSummaryController @Inject constructor( } summaries - .filter { it.roomSummary.membership == Membership.JOIN } + .filter { it.membership == Membership.JOIN } .forEach { groupSummary -> - val isSelected = groupSummary.spaceId == selected?.spaceId - if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { + val isSelected = groupSummary.roomId == selected?.roomId + if (groupSummary.roomId == ALL_COMMUNITIES_GROUP_ID) { homeSpaceSummaryItem { - id(groupSummary.spaceId) + id(groupSummary.roomId) selected(isSelected) listener { callback?.onSpaceSelected(groupSummary) } } } else { spaceSummaryItem { avatarRenderer(avatarRenderer) - id(groupSummary.spaceId) + id(groupSummary.roomId) matrixItem(groupSummary.toMatrixItem()) selected(isSelected) onLeave { callback?.onLeaveSpace(groupSummary) } @@ -119,8 +119,8 @@ class SpaceSummaryController @Inject constructor( } interface Callback { - fun onSpaceSelected(spaceSummary: SpaceSummary) - fun onLeaveSpace(spaceSummary: SpaceSummary) + fun onSpaceSelected(spaceSummary: RoomSummary) + fun onLeaveSpace(spaceSummary: RoomSummary) fun onAddSpaceSelected() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt index 3f1971d9dc..ffd27446f4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryItem.kt @@ -18,6 +18,8 @@ package im.vector.app.features.spaces import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -37,6 +39,8 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var onLeave: (() -> Unit)? = null + @EpoxyAttribute var toggleExpand: (() -> Unit)? = null + @EpoxyAttribute var expanded: Boolean? = null override fun bind(holder: Holder) { super.bind(holder) @@ -52,6 +56,26 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { } else { holder.leaveView.isVisible = false } + + when (expanded) { + null -> { + holder.collapseIndicator.isGone = true + } + else -> { + holder.collapseIndicator.isVisible = true + holder.collapseIndicator.setImageDrawable( + ContextCompat.getDrawable(holder.view.context, + if (expanded!!) R.drawable.ic_expand_less else R.drawable.ic_expand_more + ) + ) + holder.collapseIndicator.setOnClickListener( + DebouncedClickListener({ _ -> + toggleExpand?.invoke() + }) + ) + } + } + avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) } @@ -65,5 +89,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel() { val groupNameView by bind(R.id.groupNameView) val rootView by bind(R.id.itemGroupLayout) val leaveView by bind(R.id.groupTmpLeave) + val collapseIndicator by bind(R.id.groupChildrenCollapse) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt index 28bc358e36..de5ca6a137 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -42,15 +42,14 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID" sealed class SpaceListAction : VectorViewModelAction { - data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction() - data class LeaveSpace(val spaceSummary: SpaceSummary) : SpaceListAction() + data class SelectSpace(val spaceSummary: RoomSummary) : SpaceListAction() + data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction() object AddSpace : SpaceListAction() } @@ -64,8 +63,8 @@ sealed class SpaceListViewEvents : VectorViewEvents { } data class SpaceListViewState( - val asyncSpaces: Async> = Uninitialized, - val selectedSpace: SpaceSummary? = null + val asyncSpaces: Async> = Uninitialized, + val selectedSpace: RoomSummary? = null ) : MvRxState class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState, @@ -96,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp selectedSpaceDataSource .observe() .subscribe { - if (currentGroupId != it.orNull()?.spaceId) { + if (currentGroupId != it.orNull()?.roomId) { setState { copy( selectedSpace = it.orNull() @@ -111,8 +110,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp selectSubscribe(SpaceListViewState::selectedSpace) { spaceSummary -> if (spaceSummary != null) { // We only want to open group if the updated selectedGroup is a different one. - if (currentGroupId != spaceSummary.spaceId) { - currentGroupId = spaceSummary.spaceId + if (currentGroupId != spaceSummary.roomId) { + currentGroupId = spaceSummary.roomId _viewEvents.post(SpaceListViewEvents.OpenSpace) } val optionGroup = Option.just(spaceSummary) @@ -120,7 +119,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } else { // If selected group is null we force to default. It can happens when leaving the selected group. setState { - copy(selectedSpace = this.asyncSpaces()?.find { it.spaceId == ALL_COMMUNITIES_GROUP_ID }) + copy(selectedSpace = this.asyncSpaces()?.find { it.roomId == ALL_COMMUNITIES_GROUP_ID }) } } } @@ -138,17 +137,17 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state -> // get uptodate version of the space - val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.spaceId) }) + val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.roomId) }) .firstOrNull() - if (summary?.roomSummary?.membership == Membership.INVITE) { - _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomSummary.roomId)) + if (summary?.membership == Membership.INVITE) { + _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomId)) // viewModelScope.launch(Dispatchers.IO) { // tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let { // Timber.d("PEEK RESULT/ $it") // } // } } else { - if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) { + if (state.selectedSpace?.roomId != action.spaceSummary.roomId) { // state.selectedSpace?.let { // selectedSpaceDataSource.post(Option.just(state.selectedSpace)) // } @@ -160,8 +159,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) { viewModelScope.launch { awaitCallback { - tryOrNull("Failed to leave space ${action.spaceSummary.spaceId}") { - session.spaceService().getSpace(action.spaceSummary.spaceId)?.asRoom()?.leave(null, it) + tryOrNull("Failed to leave space ${action.spaceSummary.roomId}") { + session.spaceService().getSpace(action.spaceSummary.roomId)?.asRoom()?.leave(null, it) } } } @@ -178,23 +177,19 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp excludeType = listOf(/**RoomType.MESSAGING,$*/ null) } - Observable.combineLatest, List>( + Observable.combineLatest, List>( session .rx() .liveUser(session.myUserId) .map { optionalUser -> - SpaceSummary( - spaceId = ALL_COMMUNITIES_GROUP_ID, - roomSummary = RoomSummary( - roomId = ALL_COMMUNITIES_GROUP_ID, - membership = Membership.JOIN, - displayName = stringProvider.getString(R.string.group_all_communities), - avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "", - encryptionEventTs = 0, - isEncrypted = false, - typingUsers = emptyList() - ), - children = emptyList() + RoomSummary( + roomId = ALL_COMMUNITIES_GROUP_ID, + membership = Membership.JOIN, + displayName = stringProvider.getString(R.string.group_all_communities), + avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "", + encryptionEventTs = 0, + isEncrypted = false, + typingUsers = emptyList() ) }, session @@ -205,9 +200,9 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } ) .execute { async -> - val currentSelectedGroupId = selectedSpace?.spaceId + val currentSelectedGroupId = selectedSpace?.roomId val newSelectedGroup = if (currentSelectedGroupId != null) { - async()?.find { it.spaceId == currentSelectedGroupId } + async()?.find { it.roomId == currentSelectedGroupId } } else { async()?.firstOrNull() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index a5d1e16101..31f7162c2a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -35,8 +35,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.space.SpaceSummary import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap @@ -45,7 +45,7 @@ data class SpaceDirectoryState( // The current filter val spaceId: String, val currentFilter: String = "", - val summary: Async = Uninitialized, + val summary: Async = Uninitialized, // True if more result are available server side val hasMore: Boolean = false, // Set of joined roomId / spaces, diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt index 987884d8c9..66a702f685 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewViewModel.kt @@ -156,12 +156,12 @@ class SpacePreviewViewModel @AssistedInject constructor( childInfoList = Success( resolveResult.second.map { ChildInfo( - roomId = it.roomSummary?.roomId ?: "", - avatarUrl = it.roomSummary?.avatarUrl, - name = it.roomSummary?.name, - topic = it.roomSummary?.topic, - memberCount = it.roomSummary?.joinedMembersCount, - isSubSpace = it.roomSummary?.roomType == RoomType.SPACE, + roomId = it.childRoomId, + avatarUrl = it.avatarUrl, + name = it.name, + topic = it.topic, + memberCount = it.activeMemberCount, + isSubSpace = it.roomType == RoomType.SPACE, children = Uninitialized, viaServers = null ) diff --git a/vector/src/main/res/layout/item_group.xml b/vector/src/main/res/layout/item_group.xml index 6c2721ff33..9cd07f3215 100644 --- a/vector/src/main/res/layout/item_group.xml +++ b/vector/src/main/res/layout/item_group.xml @@ -5,7 +5,7 @@ android:id="@+id/itemGroupLayout" android:layout_width="match_parent" android:layout_height="65dp" - android:background="@drawable/bg_group_item" + android:background="@drawable/bg_space_item" android:clickable="true" android:focusable="true" android:foreground="?attr/selectableItemBackground"> diff --git a/vector/src/main/res/layout/item_space.xml b/vector/src/main/res/layout/item_space.xml index 25b685b999..92f0fcc1d5 100644 --- a/vector/src/main/res/layout/item_space.xml +++ b/vector/src/main/res/layout/item_space.xml @@ -35,11 +35,28 @@ android:textSize="15sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" - app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave" + app:layout_constraintEnd_toStartOf="@+id/groupChildrenCollapse" app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/lorem/random" /> + + + Enable swipe to reply in timeline Add a dedicated tab for unread notifications on main screen. - Enable Spaces (formerly known as ‘groups as rooms’) to allow users to organise rooms into more useful groups. + Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features. + Warning: This will trigger a clear cache and initial sync Link copied to clipboard diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index ed1ea222a2..aa55a3c5ec 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -49,6 +49,7 @@ + android:title="@string/labs_experimental_spaces" + android:summary="@string/labs_experimental_spaces_desc"/> \ No newline at end of file