Space hierachy SDK updates

This commit is contained in:
Valere 2021-03-11 22:35:11 +01:00
parent 80f1c6cb2d
commit 0c5ca9f51b
57 changed files with 1211 additions and 475 deletions

View File

@ -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.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.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.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.user.model.User 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<List<SpaceSummary>> { fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<RoomSummary>> {
return session.spaceService().getSpaceSummariesLive(queryParams).asObservable() return session.spaceService().getSpaceSummariesLive(queryParams).asObservable()
.startWithCallable { .startWithCallable {
session.spaceService().getSpaceSummaries(queryParams) session.spaceService().getSpaceSummaries(queryParams)
} }
} }
fun liveFlattenRoomSummaryChildOf(spaceId: String?): Observable<List<RoomSummary>> {
return session.getFlattenRoomSummaryChildOfLive(spaceId).asObservable()
.startWithCallable {
session.getFlattenRoomSummaryChildOf(spaceId)
}
}
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> { fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.getBreadcrumbsLive(queryParams).asObservable() return session.getBreadcrumbsLive(queryParams).asObservable()
.startWithCallable { .startWithCallable {

View File

@ -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<String> {
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<String> { 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<String> { cb ->
session.createRoom(CreateRoomParams().apply { name = "O1" }, cb)
}
val orphan2 = commonTestHelper.doSync<String> { 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<String> { 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<List<RoomSummary>> {
override fun onChanged(children: List<RoomSummary>?) {
// 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<List<RoomSummary>> {
override fun onChanged(children: List<RoomSummary>?) {
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<Unit> {
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<String>
)
private fun createPublicSpace(session: Session,
spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>>
/** 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<String> { 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)
}
}

View File

@ -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.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService 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.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.api.util.Optional
/** /**
@ -90,5 +92,6 @@ interface Room :
limit: Int, limit: Int,
beforeLimit: Int, beforeLimit: Int,
afterLimit: Int, afterLimit: Int,
// fun getSpaceParents(): List<SpaceSummary>
includeProfile: Boolean): SearchResult includeProfile: Boolean): SearchResult
} }

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.events.model.Event 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.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.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -197,4 +198,8 @@ interface RoomService {
.setEnablePlaceholders(false) .setEnablePlaceholders(false)
.setPrefetchDistance(10) .setPrefetchDistance(10)
.build() .build()
fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List<Membership> = Membership.activeMemberships()) : List<RoomSummary>
fun getFlattenRoomSummaryChildOfLive(spaceId: String?, memberships: List<Membership> = Membership.activeMemberships()): LiveData<List<RoomSummary>>
} }

View File

@ -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.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.room.model.Membership 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.room.model.RoomType
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
return RoomSummaryQueryParams.Builder().apply(init).build() 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: * 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] * [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<Membership>, val memberships: List<Membership>,
val roomCategoryFilter: RoomCategoryFilter?, val roomCategoryFilter: RoomCategoryFilter?,
val roomTagQueryFilter: RoomTagQueryFilter? val roomTagQueryFilter: RoomTagQueryFilter?
val excludeType: List<String?> val excludeType: List<String?>?,
val includeType: List<String?>?
) { ) {
class Builder { class Builder {
@ -49,6 +66,7 @@ data class RoomSummaryQueryParams(
var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
var roomTagQueryFilter: RoomTagQueryFilter? = null var roomTagQueryFilter: RoomTagQueryFilter? = null
var excludeType: List<String?> = listOf(RoomType.SPACE) var excludeType: List<String?> = listOf(RoomType.SPACE)
var includeType: List<String?>? = null
fun build() = RoomSummaryQueryParams( fun build() = RoomSummaryQueryParams(
roomId = roomId, roomId = roomId,
@ -56,8 +74,9 @@ data class RoomSummaryQueryParams(
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
memberships = memberships, memberships = memberships,
roomCategoryFilter = roomCategoryFilter, roomCategoryFilter = roomCategoryFilter,
roomTagQueryFilter = roomTagQueryFilter roomTagQueryFilter = roomTagQueryFilter,
excludeType = excludeType excludeType = excludeType,
includeType = includeType
) )
} }
} }

View File

@ -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] * 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( data class RoomSummary constructor(
override val roomId: String, val roomId: String,
// Computed display name // Computed display name
override val displayName: String = "", val displayName: String = "",
override val name: String = "", val name: String = "",
override val topic: String = "", val topic: String = "",
override val avatarUrl: String = "", val avatarUrl: String = "",
override val canonicalAlias: String? = null, val canonicalAlias: String? = null,
override val aliases: List<String> = emptyList(), val aliases: List<String> = emptyList(),
override val joinedMembersCount: Int? = 0, val joinedMembersCount: Int? = 0,
override val invitedMembersCount: Int? = 0, val invitedMembersCount: Int? = 0,
val latestPreviewableEvent: TimelineEvent? = null, val latestPreviewableEvent: TimelineEvent? = null,
override val otherMemberIds: List<String> = emptyList(), val otherMemberIds: List<String> = emptyList(),
val isDirect: Boolean = false, val isDirect: Boolean = false,
val notificationCount: Int = 0, val notificationCount: Int = 0,
val highlightCount: Int = 0, val highlightCount: Int = 0,
@ -55,8 +55,10 @@ data class RoomSummary constructor(
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
val hasFailedSending: Boolean = false, val hasFailedSending: Boolean = false,
override val roomType: String? = null val roomType: String? = null,
) : IRoomSummary { val spaceParents: List<SpaceParentInfo>? = null,
val children: List<SpaceChildInfo>? = null
) {
val isVersioned: Boolean val isVersioned: Boolean
get() = versioningState != VersioningState.NONE get() = versioningState != VersioningState.NONE

View File

@ -17,8 +17,16 @@
package org.matrix.android.sdk.api.session.room.model package org.matrix.android.sdk.api.session.room.model
data class SpaceChildInfo( 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 order: String?,
val activeMemberCount: Int?,
val autoJoin: Boolean, val autoJoin: Boolean,
val viaServers: List<String> val viaServers: List<String>
) )

View File

@ -16,16 +16,9 @@
package org.matrix.android.sdk.api.session.room.model package org.matrix.android.sdk.api.session.room.model
interface IRoomSummary { data class SpaceParentInfo(
val roomId: String val parentId: String?,
val displayName: String val roomSummary: RoomSummary?,
val name: String val canonical: Boolean?,
val topic: String val viaServers: List<String>
val avatarUrl: String )
val canonicalAlias: String?
val aliases: List<String>
val joinedMembersCount: Int?
val invitedMembersCount: Int?
val otherMemberIds: List<String>
val roomType: String?
}

View File

@ -24,9 +24,9 @@ interface Space {
fun asRoom() : Room 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<String>, order: String?, autoJoin: Boolean = false) suspend fun addChildren(roomId: String, viaServers: List<String>, order: String?, autoJoin: Boolean = false)

View File

@ -60,9 +60,9 @@ interface SpaceService {
* Get a live list of space summaries. This list is refreshed as soon as the data changes. * Get a live list of space summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of List[SpaceSummary] * @return the [LiveData] of List[SpaceSummary]
*/ */
fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>> fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>>
fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary> fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary>
sealed class JoinSpaceResult { sealed class JoinSpaceResult {
object Success : JoinSpaceResult() object Success : JoinSpaceResult()
@ -79,4 +79,14 @@ interface SpaceService {
viaServers: List<String> = emptyList()): JoinSpaceResult viaServers: List<String> = emptyList()): JoinSpaceResult
suspend fun rejectInvite(spaceId: String, reason: String?) suspend fun rejectInvite(spaceId: String, reason: String?)
// fun getSpaceParentsOfRoom(roomId: String) : List<SpaceSummary>
/**
* 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<String>)
} }

View File

@ -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<SpaceChildInfo>
) : IRoomSummary by roomSummary

View File

@ -26,7 +26,6 @@ import com.squareup.moshi.JsonClass
* "state_key": "!space:example.com", * "state_key": "!space:example.com",
* "content": { * "content": {
* "via": ["example.com"], * "via": ["example.com"],
* "present": true,
* "canonical": true, * "canonical": true,
* } * }
* } * }
@ -38,11 +37,6 @@ data class SpaceParentContent(
* Parents where via is not present are ignored. * Parents where via is not present are ignored.
*/ */
@Json(name = "via") val via: List<String>? = null, @Json(name = "via") val via: List<String>? = 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. * 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 * When a user joins a room with a canonical parent, clients may switch to view the room

View File

@ -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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom 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.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.user.model.User
import java.util.Locale 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 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 // If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)

View File

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.FieldAttribute
import io.realm.RealmMigration import io.realm.RealmMigration
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields 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.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields 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.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceChildInfoEntityFields import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -200,21 +199,34 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
fun migrateTo10(realm: DynamicRealm) { fun migrateTo10(realm: DynamicRealm) {
Timber.d("Step 9 -> 10") 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") realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java) ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
?.transform { obj -> ?.transform { obj ->
// Should I put messaging type here? // Should I put messaging type here?
obj.setString(RoomSummaryEntityFields.ROOM_TYPE, null) obj.setString(RoomSummaryEntityFields.ROOM_TYPE, null)
} }
?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity") ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
?.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!!)
} }
} }

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.database.mapper 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.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.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
@ -64,7 +66,29 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId, inviterId = roomSummaryEntity.inviterId,
hasFailedSending = roomSummaryEntity.hasFailedSending, 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()
)
}
) )
} }
} }

View File

@ -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
)
}
)
}
}

View File

@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
set(value) { set(value) {
membersLoadStatusStr = value.name membersLoadStatusStr = value.name
} }
companion object companion object
} }

View File

@ -28,6 +28,9 @@ import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
internal open class RoomSummaryEntity( internal open class RoomSummaryEntity(
@PrimaryKey var roomId: String = "" @PrimaryKey var roomId: String = ""
var roomType: String? = null,
var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
var children: RealmList<SpaceChildSummaryEntity> = RealmList()
) : RealmObject() { ) : RealmObject() {
var displayName: String? = "" var displayName: String? = ""
@ -244,6 +247,5 @@ internal open class RoomSummaryEntity(
roomEncryptionTrustLevelStr = value?.name roomEncryptionTrustLevelStr = value?.name
} }
} }
companion object companion object
} }

View File

@ -62,7 +62,7 @@ import io.realm.annotations.RealmModule
UserAccountDataEntity::class, UserAccountDataEntity::class,
ScalarTokenEntity::class, ScalarTokenEntity::class,
WellknownIntegrationManagerConfigEntity::class, WellknownIntegrationManagerConfigEntity::class,
SpaceSummaryEntity::class, SpaceChildSummaryEntity::class,
SpaceChildInfoEntity::class SpaceParentSummaryEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -22,14 +22,22 @@ import io.realm.RealmObject
/** /**
* Decorates room summary with space related information. * Decorates room summary with space related information.
*/ */
internal open class SpaceChildInfoEntity( internal open class SpaceChildSummaryEntity(
var viaServers: RealmList<String> = RealmList(), // var isSpace: Boolean = false,
// Use for alphabetic ordering of this child
var order: String? = null, var order: String? = null,
// If true, this child should be join when parent is joined
var autoJoin: Boolean? = null, 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<String> = RealmList()
// var owner: RoomSummaryEntity? = null,
// var level: Int = 0
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -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<String> = RealmList()
// var child: RoomSummaryEntity? = null,
// var level: Int = 0
) : RealmObject() {
companion object
}

View File

@ -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<SpaceChildInfoEntity> = 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
}

View File

@ -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<SpaceSummaryEntity> {
val query = realm.where<SpaceSummaryEntity>()
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<SpaceSummaryEntity>()
.isNotNull(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`)
.equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, roomAlias)
.findFirst()
if (spaceSummary != null) {
return spaceSummary
}
return realm.where<SpaceSummaryEntity>()
.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<SpaceSummaryEntity>(roomId).also {
it.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
}
}

View File

@ -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.call.RoomCallService
import org.matrix.android.sdk.api.session.room.members.MembershipService 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.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.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
import org.matrix.android.sdk.api.session.room.read.ReadService 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.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService 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.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.api.util.Optional
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource 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.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 org.matrix.android.sdk.internal.util.awaitCallback
import java.security.InvalidParameterException import java.security.InvalidParameterException
import javax.inject.Inject 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)
}
} }

View File

@ -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.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult 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.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.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.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.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional 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.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
@ -63,7 +65,9 @@ internal class DefaultRoomService @Inject constructor(
private val peekRoomTask: PeekRoomTask, private val peekRoomTask: PeekRoomTask,
private val roomGetter: RoomGetter, private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource private val roomSummaryMapper: RoomSummaryMapper,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
) : RoomService { ) : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable { override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
@ -168,4 +172,18 @@ internal class DefaultRoomService @Inject constructor(
override suspend fun peekRoom(roomIdOrAlias: String): PeekResult { override suspend fun peekRoom(roomIdOrAlias: String): PeekResult {
return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias)) return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias))
} }
override fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List<Membership>): List<RoomSummary> {
if (spaceId == null) {
return roomSummaryDataSource.getFlattenOrphanRooms()
}
return roomSummaryDataSource.getAllRoomSummaryChildOf(spaceId, memberships)
}
override fun getFlattenRoomSummaryChildOfLive(spaceId: String?, memberships: List<Membership>): LiveData<List<RoomSummary>> {
if (spaceId == null) {
return roomSummaryDataSource.getFlattenOrphanRoomsLive()
}
return roomSummaryDataSource.getAllRoomSummaryChildOfLive(spaceId, memberships)
}
} }

View File

@ -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.room.model.RoomType
import org.matrix.android.sdk.api.session.space.Space 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.DefaultSpace
import org.matrix.android.sdk.internal.session.space.SpaceSummaryDataSource
import javax.inject.Inject import javax.inject.Inject
internal interface SpaceGetter { internal interface SpaceGetter {
@ -28,7 +28,7 @@ internal interface SpaceGetter {
internal class DefaultSpaceGetter @Inject constructor( internal class DefaultSpaceGetter @Inject constructor(
private val roomGetter: RoomGetter, private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource private val spaceSummaryDataSource: RoomSummaryDataSource
) : SpaceGetter { ) : SpaceGetter {
override fun get(spaceId: String): Space? { override fun get(spaceId: String): Space? {

View File

@ -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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.database.query.whereType
@ -35,7 +36,7 @@ import timber.log.Timber
* *
* - Separately, rooms can claim parents via the m.room.parent state event: * - Separately, rooms can claim parents via the m.room.parent state event:
*/ */
internal class RoomRelationshipHelper(private val realm: Realm, internal class RoomChildRelationInfo(private val realm: Realm,
private val roomId: String private val roomId: String
) { ) {
@ -46,15 +47,24 @@ internal class RoomRelationshipHelper(private val realm: Realm,
val viaServers: List<String> val viaServers: List<String>
) )
data class SpaceParentInfo(
val roomId: String,
val canonical: Boolean,
val viaServers: List<String>,
val stateEventSender: String
)
/** /**
* Gets the ordered list of valid child description. * Gets the ordered list of valid child description.
*/ */
fun getDirectChildrenDescriptions(): List<SpaceChildInfo> { fun getDirectChildrenDescriptions(): List<SpaceChildInfo> {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) 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 { .mapNotNull {
ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc -> ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.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. // Children where via is not present are ignored.
scc.via?.let { via -> scc.via?.let { via ->
SpaceChildInfo( SpaceChildInfo(
@ -68,4 +78,25 @@ internal class RoomRelationshipHelper(private val realm: Realm,
} }
.sortedBy { it.order } .sortedBy { it.order }
} }
fun getParentDescriptions(): List<SpaceParentInfo> {
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<SpaceParentContent>()?.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 ?: ""
)
}
}
}
}
} }

View File

@ -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<Membership>,
val roomSummaryDataSource: RoomSummaryDataSource) {
private val sources = HashMap<String, LiveData<Optional<RoomSummary>>>()
private val mediatorLiveData = MediatorLiveData<List<String>>()
fun liveData() = mediatorLiveData
init {
onChange()
}
private fun parentsToCheck(): List<RoomSummary> {
val spaces = ArrayList<RoomSummary>()
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<RoomSummary>()
roomSummaryDataSource.flattenChild(spaceSummary, emptyList(), results, memberships)
mediatorLiveData.postValue(results.map { it.roomId })
}
}
}

View File

@ -1,5 +1,6 @@
/* /*
* Copyright 2020 The Matrix.org Foundation C.I.C. * 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.query.RoomCategoryFilter
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams 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.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.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.model.VersioningState
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount 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.Optional
import org.matrix.android.sdk.api.util.toOptional 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.RoomSummaryMapper
@ -84,6 +93,36 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
) )
} }
fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
return getRoomSummariesLive(queryParams)
}
fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? {
return getRoomSummary(roomIdOrAlias).let {
it?.takeIf { it.roomType == RoomType.SPACE }
}
}
fun getSpaceSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> {
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<RoomSummary> {
return getRoomSummaries(spaceSummaryQueryParams)
}
fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> { fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return monarchy.fetchAllMappedSync( return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it, queryParams) }, { 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) 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 return query
} }
fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List<Membership>): List<RoomSummary> {
val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList()
val result = ArrayList<RoomSummary>()
flattenChild(space, emptyList(), result, memberShips)
return result
}
fun getAllRoomSummaryChildOfLive(spaceId: String, memberShips: List<Membership>): LiveData<List<RoomSummary>> {
// 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<RoomSummaryEntity>()
.`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<RoomSummary> {
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<List<RoomSummary>> {
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<String>, output: MutableList<RoomSummary>, memberShips: List<Membership>) {
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<String>, output: MutableList<RoomSummary>, memberShips: List<Membership>) {
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)
}
}
}
}
}
}
} }

View File

@ -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.EventEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields 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.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceChildInfoEntity import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity 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.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates 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.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull 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.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.database.query.whereType
import org.matrix.android.sdk.internal.di.UserId 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.RoomAvatarResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver 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.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.RoomSyncSummary
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
import timber.log.Timber import timber.log.Timber
@ -62,7 +65,8 @@ internal class RoomSummaryUpdater @Inject constructor(
private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver, private val roomAvatarResolver: RoomAvatarResolver,
private val eventDecryptor: EventDecryptor, private val eventDecryptor: EventDecryptor,
private val crossSigningService: DefaultCrossSigningService) { private val crossSigningService: DefaultCrossSigningService,
private val stateEventDataSource: StateEventDataSource) {
fun update(realm: Realm, fun update(realm: Realm,
roomId: String, roomId: String,
@ -163,28 +167,6 @@ internal class RoomSummaryUpdater @Inject constructor(
crossSigningService.onUsersDeviceUpdate(otherRoomMembers) 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<SpaceChildInfoEntity>().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() { private fun RoomSummaryEntity.updateHasFailedSending() {
@ -196,4 +178,55 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) 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<SpaceChildSummaryEntity>().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<SpaceParentSummaryEntity>().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 {
//
// }
} }

View File

@ -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.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams 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.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
@ -44,8 +44,8 @@ internal class DefaultCreateSpaceTask @Inject constructor(
try { try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(SpaceSummaryEntity::class.java) realm.where(RoomSummaryEntity::class.java)
.equalTo(SpaceSummaryEntityFields.SPACE_ID, spaceId) .equalTo(RoomSummaryEntityFields.ROOM_ID, spaceId)
} }
} catch (exception: TimeoutCancellationException) { } catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout(spaceId) throw CreateRoomFailure.CreatedWithTimeout(spaceId)

View File

@ -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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.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.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 { override fun asRoom(): Room {
return room return room
} }
override fun spaceSummary(): SpaceSummary? { override fun spaceSummary(): RoomSummary? {
return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId) return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId)
} }

View File

@ -19,26 +19,37 @@ package org.matrix.android.sdk.internal.session.space
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy 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.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.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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo 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.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.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.Space 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.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.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent 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.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.SpaceGetter
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask 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.PeekSpaceTask
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
import java.lang.IllegalArgumentException
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSpaceService @Inject constructor( internal class DefaultSpaceService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
@UserId private val userId: String,
private val createSpaceTask: CreateSpaceTask, private val createSpaceTask: CreateSpaceTask,
// private val joinRoomTask: JoinRoomTask, // private val joinRoomTask: JoinRoomTask,
private val joinSpaceTask: JoinSpaceTask, private val joinSpaceTask: JoinSpaceTask,
@ -47,8 +58,9 @@ internal class DefaultSpaceService @Inject constructor(
// private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, // private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
// private val roomIdByAliasTask: GetRoomIdByAliasTask, // private val roomIdByAliasTask: GetRoomIdByAliasTask,
// private val deleteRoomAliasTask: DeleteRoomAliasTask, // private val deleteRoomAliasTask: DeleteRoomAliasTask,
// private val roomGetter: RoomGetter, private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val stateEventDataSource: StateEventDataSource,
private val peekSpaceTask: PeekSpaceTask, private val peekSpaceTask: PeekSpaceTask,
private val resolveSpaceInfoTask: ResolveSpaceInfoTask, private val resolveSpaceInfoTask: ResolveSpaceInfoTask,
private val leaveRoomTask: LeaveRoomTask private val leaveRoomTask: LeaveRoomTask
@ -73,12 +85,12 @@ internal class DefaultSpaceService @Inject constructor(
return spaceGetter.get(spaceId) return spaceGetter.get(spaceId)
} }
override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>> { override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
return spaceSummaryDataSource.getRoomSummariesLive(queryParams) return roomSummaryDataSource.getSpaceSummariesLive(queryParams)
} }
override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary> { override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> {
return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams) return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
} }
override suspend fun peekSpace(spaceId: String): SpacePeekResult { 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 } ?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
?.content.toModel<SpaceChildContent>() ?.content.toModel<SpaceChildContent>()
SpaceChildInfo( SpaceChildInfo(
roomSummary = RoomSummary( childRoomId = childSummary.roomId,
roomId = childSummary.roomId, isKnown = true,
roomType = childSummary.roomType, roomType = childSummary.roomType,
name = childSummary.name ?: "", name = childSummary.name,
displayName = childSummary.name ?: "", topic = childSummary.topic,
topic = childSummary.topic ?: "", avatarUrl = childSummary.avatarUrl,
joinedMembersCount = childSummary.numJoinedMembers,
avatarUrl = childSummary.avatarUrl ?: "",
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false
),
order = childStateEv?.order, order = childStateEv?.order,
autoJoin = childStateEv?.autoJoin ?: false, autoJoin = childStateEv?.autoJoin ?: false,
viaServers = childStateEv?.via ?: emptyList() viaServers = childStateEv?.via ?: emptyList(),
activeMemberCount = childSummary.numJoinedMembers
) )
} ?: emptyList() } ?: emptyList()
) )
@ -138,4 +145,42 @@ internal class DefaultSpaceService @Inject constructor(
override suspend fun rejectInvite(spaceId: String, reason: String?) { override suspend fun rejectInvite(spaceId: String, reason: String?) {
leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason)) leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
} }
// override fun getSpaceParentsOfRoom(roomId: String): List<SpaceSummary> {
// return spaceSummaryDataSource.getParentsOfRoom(roomId)
// }
override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>) {
// 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<PowerLevelsContent>()
?: 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()
)
}
} }

View File

@ -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.room.model.RoomType
import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult 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.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase 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.RoomAPI
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask 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 org.matrix.android.sdk.internal.task.Task
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -45,7 +46,7 @@ internal class DefaultJoinSpaceTask @Inject constructor(
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
@SessionDatabase @SessionDatabase
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val spaceSummaryDataSource: SpaceSummaryDataSource private val roomSummaryDataSource: RoomSummaryDataSource
) : JoinSpaceTask { ) : JoinSpaceTask {
override suspend fun execute(params: JoinSpaceTask.Params): SpaceService.JoinSpaceResult { 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} ...") Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...")
try { try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm ->
realm.where(SpaceSummaryEntity::class.java) realm.where(RoomSummaryEntity::class.java)
.apply { .apply {
if (params.roomIdOrAlias.startsWith("!")) { if (params.roomIdOrAlias.startsWith("!")) {
equalTo(SpaceSummaryEntityFields.SPACE_ID, params.roomIdOrAlias) equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias)
} else { } 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) { } catch (exception: TimeoutCancellationException) {
Timber.w("## Space: > Error created with timeout") Timber.w("## Space: > Error created with timeout")
@ -83,21 +84,21 @@ internal class DefaultJoinSpaceTask @Inject constructor(
val errors = HashMap<String, Throwable>() val errors = HashMap<String, Throwable>()
Timber.v("## Space: > Sync done ...") Timber.v("## Space: > Sync done ...")
// after that i should have the children (? do I need to paginate to get state) // after that i should have the children (? do I need to paginate to get state)
val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias) val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}") Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.children?.size}")
summary?.children?.forEach { summary?.children?.forEach {
val childRoomSummary = it.roomSummary ?: return@forEach // val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}") Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}")
if (it.autoJoin) { if (it.autoJoin) {
// I should try to join as well // 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? // 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 -> { SpaceService.JoinSpaceResult.Success -> {
// nop // nop
} }
is SpaceService.JoinSpaceResult.Fail -> { is SpaceService.JoinSpaceResult.Fail -> {
errors[it.roomSummary.roomId] = subspaceJoinResult.error errors[it.childRoomId] = subspaceJoinResult.error
} }
is SpaceService.JoinSpaceResult.PartialSuccess -> { is SpaceService.JoinSpaceResult.PartialSuccess -> {
errors.putAll(subspaceJoinResult.failedRooms) errors.putAll(subspaceJoinResult.failedRooms)
@ -105,15 +106,15 @@ internal class DefaultJoinSpaceTask @Inject constructor(
} }
} else { } else {
try { try {
Timber.v("## Space: Joining room child ${childRoomSummary.roomId}") Timber.v("## Space: Joining room child ${it.childRoomId}")
joinRoomTask.execute(JoinRoomTask.Params( joinRoomTask.execute(JoinRoomTask.Params(
roomIdOrAlias = childRoomSummary.roomId, roomIdOrAlias = it.childRoomId,
reason = "Auto-join parent space", reason = "Auto-join parent space",
viaServers = it.viaServers viaServers = it.viaServers
)) ))
} catch (failure: Throwable) { } catch (failure: Throwable) {
errors[it.roomSummary.roomId] = failure errors[it.childRoomId] = failure
Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}") Timber.e("## Space: Failed to join room child ${it.childRoomId}")
} }
} }
} }

View File

@ -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<Optional<SpaceSummary>> {
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<SpaceSummary> {
return monarchy.fetchAllMappedSync(
{ spaceSummariesQuery(it, queryParams) },
{ spaceSummaryMapper.map(it) }
)
}
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<SpaceSummary>> {
return monarchy.findAllMappedWithChanges(
{ spaceSummariesQuery(it, queryParams) },
{ spaceSummaryMapper.map(it) }
)
}
private fun spaceSummariesQuery(realm: Realm, queryParams: SpaceSummaryQueryParams): RealmQuery<SpaceSummaryEntity> {
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
}
}

View File

@ -95,6 +95,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter) handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter)
// post room sync validation
roomSummaryUpdater.validateSpaceRelationship(realm)
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************

View File

@ -18,9 +18,9 @@ package im.vector.app.features.grouplist
import arrow.core.Option import arrow.core.Option
import im.vector.app.core.utils.BehaviorDataSource 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource<Option<SpaceSummary>>(Option.empty()) class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource<Option<RoomSummary>>(Option.empty())

View File

@ -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.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState 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.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.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import timber.log.Timber import timber.log.Timber
@ -252,10 +252,10 @@ class HomeDetailFragment @Inject constructor(
} }
} }
private fun onSpaceChange(spaceSummary: SpaceSummary?) { private fun onSpaceChange(spaceSummary: RoomSummary?) {
spaceSummary?.let { spaceSummary?.let {
// Use GlideApp with activity context to avoid the glideRequests to be paused // 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 // Special case
views.groupToolbarAvatarImageView.background = ContextCompat.getDrawable(requireContext(), R.drawable.space_home_background) views.groupToolbarAvatarImageView.background = ContextCompat.getDrawable(requireContext(), R.drawable.space_home_background)
views.groupToolbarAvatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE views.groupToolbarAvatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE

View File

@ -22,12 +22,11 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.group.model.GroupSummary 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.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
data class HomeDetailViewState( data class HomeDetailViewState(
val groupSummary: Option<GroupSummary> = Option.empty(), val groupSummary: Option<GroupSummary> = Option.empty(),
val spaceSummary: Option<SpaceSummary> = Option.empty(), val spaceSummary: Option<RoomSummary> = Option.empty(),
val asyncRooms: Async<List<RoomSummary>> = Uninitialized, val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE,
val notificationCountCatchup: Int = 0, val notificationCountCatchup: Int = 0,

View File

@ -60,19 +60,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_GUEST_ACCESS,
EventType.REDACTION,
EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_CANDIDATES, EventType.CALL_CANDIDATES,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_REPLACES, EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER, EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE, EventType.CALL_NEGOTIATE,
EventType.REACTION, 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_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)

View File

@ -107,6 +107,8 @@ class NoticeEventFormatter @Inject constructor(
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_READY,
EventType.STATE_SPACE_CHILD,
EventType.STATE_SPACE_PARENT,
EventType.REDACTION -> formatDebug(timelineEvent.root) EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")

View File

@ -212,6 +212,6 @@ class MatrixToRoomSpaceFragment @Inject constructor(
} }
} }
fun secondaryButtonClicked() = withState(sharedViewModel) { state -> private fun secondaryButtonClicked() = withState(sharedViewModel) { _ ->
} }
} }

View File

@ -111,7 +111,7 @@ class DefaultNavigator @Inject constructor(
} }
sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId)?.spaceSummary()?.let { 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)) selectedSpaceDataSource.post(Option.just(it))
} ?: kotlin.run { } ?: kotlin.run {
Timber.d("## Nav: Failed to switch to space $spaceId") Timber.d("## Nav: Failed to switch to space $spaceId")

View File

@ -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<T>(protected val sharedPrefs: SharedPreferences,
protected val key: String,
private val defValue: T) : LiveData<T>() {
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<Boolean> {
return object : SharedPreferenceLiveData<Boolean>(sharedPrefs, key, defaultValue) {
override fun getValueFromPreferences(key: String, defValue: Boolean): Boolean {
return this.sharedPrefs.getBoolean(key, defValue)
}
}
}
}
}

View File

@ -20,6 +20,7 @@ import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.LiveData
import com.squareup.seismic.ShakeDetector import com.squareup.seismic.ShakeDetector
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.R 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) return defaultPrefs.getBoolean(SETTINGS_LABS_USE_SPACES, false)
} }
fun labSpacesLive(): LiveData<Boolean> {
return SharedPreferenceLiveData.booleanLiveData(
defaultPrefs,
SETTINGS_LABS_USE_SPACES,
false
)
}
fun failFast(): Boolean { fun failFast(): Boolean {
return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false)) return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false))
} }

View File

@ -17,6 +17,9 @@
package im.vector.app.features.settings package im.vector.app.features.settings
import im.vector.app.R 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 import javax.inject.Inject
class VectorSettingsLabsFragment @Inject constructor( class VectorSettingsLabsFragment @Inject constructor(
@ -27,6 +30,11 @@ class VectorSettingsLabsFragment @Inject constructor(
override val preferenceXmlRes = R.xml.vector_settings_labs override val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() { override fun bindPref() {
// Nothing to do findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_USE_SPACES)!!.let { pref ->
pref.setOnPreferenceChangeListener { _, _ ->
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = false))
true
}
}
} }
} }

View File

@ -61,7 +61,7 @@ class ShareSpaceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpa
?: return Unit.also { dismiss() } ?: return Unit.also { dismiss() }
val summary = activeSessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(args.spaceId)?.spaceSummary() val summary = activeSessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(args.spaceId)?.spaceSummary()
val spaceName = summary?.roomSummary?.name val spaceName = summary?.name
views.descriptionText.text = getString(R.string.invite_people_to_your_space_desc, spaceName) views.descriptionText.text = getString(R.string.invite_people_to_your_space_desc, spaceName)
views.inviteByMailButton.debouncedClicks { views.inviteByMailButton.debouncedClicks {

View File

@ -32,7 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentGroupListBinding import im.vector.app.databinding.FragmentGroupListBinding
import im.vector.app.features.home.HomeActivitySharedAction import im.vector.app.features.home.HomeActivitySharedAction
import im.vector.app.features.home.HomeSharedActionViewModel import im.vector.app.features.home.HomeSharedActionViewModel
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.Inject
class SpaceListFragment @Inject constructor( class SpaceListFragment @Inject constructor(
@ -76,11 +76,11 @@ class SpaceListFragment @Inject constructor(
spaceController.update(state) spaceController.update(state)
} }
override fun onSpaceSelected(spaceSummary: SpaceSummary) { override fun onSpaceSelected(spaceSummary: RoomSummary) {
viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) viewModel.handle(SpaceListAction.SelectSpace(spaceSummary))
} }
override fun onLeaveSpace(spaceSummary: SpaceSummary) { override fun onLeaveSpace(spaceSummary: RoomSummary) {
viewModel.handle(SpaceListAction.LeaveSpace(spaceSummary)) viewModel.handle(SpaceListAction.LeaveSpace(spaceSummary))
} }

View File

@ -26,7 +26,7 @@ import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.grouplist.homeSpaceSummaryItem import im.vector.app.features.grouplist.homeSpaceSummaryItem
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
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.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -51,13 +51,13 @@ class SpaceSummaryController @Inject constructor(
buildGroupModels(nonNullViewState.asyncSpaces(), nonNullViewState.selectedSpace) buildGroupModels(nonNullViewState.asyncSpaces(), nonNullViewState.selectedSpace)
} }
private fun buildGroupModels(summaries: List<SpaceSummary>?, selected: SpaceSummary?) { private fun buildGroupModels(summaries: List<RoomSummary>?, selected: RoomSummary?) {
if (summaries.isNullOrEmpty()) { if (summaries.isNullOrEmpty()) {
return return
} }
// show invites on top // show invites on top
summaries.filter { it.roomSummary.membership == Membership.INVITE } summaries.filter { it.membership == Membership.INVITE }
.let { invites -> .let { invites ->
if (invites.isNotEmpty()) { if (invites.isNotEmpty()) {
genericItemHeader { genericItemHeader {
@ -67,7 +67,7 @@ class SpaceSummaryController @Inject constructor(
invites.forEach { invites.forEach {
spaceSummaryItem { spaceSummaryItem {
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
id(it.spaceId) id(it.roomId)
matrixItem(it.toMatrixItem()) matrixItem(it.toMatrixItem())
selected(false) selected(false)
listener { callback?.onSpaceSelected(it) } listener { callback?.onSpaceSelected(it) }
@ -87,19 +87,19 @@ class SpaceSummaryController @Inject constructor(
} }
summaries summaries
.filter { it.roomSummary.membership == Membership.JOIN } .filter { it.membership == Membership.JOIN }
.forEach { groupSummary -> .forEach { groupSummary ->
val isSelected = groupSummary.spaceId == selected?.spaceId val isSelected = groupSummary.roomId == selected?.roomId
if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) { if (groupSummary.roomId == ALL_COMMUNITIES_GROUP_ID) {
homeSpaceSummaryItem { homeSpaceSummaryItem {
id(groupSummary.spaceId) id(groupSummary.roomId)
selected(isSelected) selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) } listener { callback?.onSpaceSelected(groupSummary) }
} }
} else { } else {
spaceSummaryItem { spaceSummaryItem {
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
id(groupSummary.spaceId) id(groupSummary.roomId)
matrixItem(groupSummary.toMatrixItem()) matrixItem(groupSummary.toMatrixItem())
selected(isSelected) selected(isSelected)
onLeave { callback?.onLeaveSpace(groupSummary) } onLeave { callback?.onLeaveSpace(groupSummary) }
@ -119,8 +119,8 @@ class SpaceSummaryController @Inject constructor(
} }
interface Callback { interface Callback {
fun onSpaceSelected(spaceSummary: SpaceSummary) fun onSpaceSelected(spaceSummary: RoomSummary)
fun onLeaveSpace(spaceSummary: SpaceSummary) fun onLeaveSpace(spaceSummary: RoomSummary)
fun onAddSpaceSelected() fun onAddSpaceSelected()
} }
} }

View File

@ -18,6 +18,8 @@ package im.vector.app.features.spaces
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
@ -37,6 +39,8 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var onLeave: (() -> Unit)? = null @EpoxyAttribute var onLeave: (() -> Unit)? = null
@EpoxyAttribute var toggleExpand: (() -> Unit)? = null
@EpoxyAttribute var expanded: Boolean? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
@ -52,6 +56,26 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
} else { } else {
holder.leaveView.isVisible = false 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) avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
} }
@ -65,5 +89,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
val groupNameView by bind<TextView>(R.id.groupNameView) val groupNameView by bind<TextView>(R.id.groupNameView)
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout) val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
val leaveView by bind<ImageView>(R.id.groupTmpLeave) val leaveView by bind<ImageView>(R.id.groupTmpLeave)
val collapseIndicator by bind<ImageView>(R.id.groupChildrenCollapse)
} }
} }

View File

@ -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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID" const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID"
sealed class SpaceListAction : VectorViewModelAction { sealed class SpaceListAction : VectorViewModelAction {
data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction() data class SelectSpace(val spaceSummary: RoomSummary) : SpaceListAction()
data class LeaveSpace(val spaceSummary: SpaceSummary) : SpaceListAction() data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction()
object AddSpace : SpaceListAction() object AddSpace : SpaceListAction()
} }
@ -64,8 +63,8 @@ sealed class SpaceListViewEvents : VectorViewEvents {
} }
data class SpaceListViewState( data class SpaceListViewState(
val asyncSpaces: Async<List<SpaceSummary>> = Uninitialized, val asyncSpaces: Async<List<RoomSummary>> = Uninitialized,
val selectedSpace: SpaceSummary? = null val selectedSpace: RoomSummary? = null
) : MvRxState ) : MvRxState
class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState, class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState,
@ -96,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
selectedSpaceDataSource selectedSpaceDataSource
.observe() .observe()
.subscribe { .subscribe {
if (currentGroupId != it.orNull()?.spaceId) { if (currentGroupId != it.orNull()?.roomId) {
setState { setState {
copy( copy(
selectedSpace = it.orNull() selectedSpace = it.orNull()
@ -111,8 +110,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
selectSubscribe(SpaceListViewState::selectedSpace) { spaceSummary -> selectSubscribe(SpaceListViewState::selectedSpace) { spaceSummary ->
if (spaceSummary != null) { if (spaceSummary != null) {
// We only want to open group if the updated selectedGroup is a different one. // We only want to open group if the updated selectedGroup is a different one.
if (currentGroupId != spaceSummary.spaceId) { if (currentGroupId != spaceSummary.roomId) {
currentGroupId = spaceSummary.spaceId currentGroupId = spaceSummary.roomId
_viewEvents.post(SpaceListViewEvents.OpenSpace) _viewEvents.post(SpaceListViewEvents.OpenSpace)
} }
val optionGroup = Option.just(spaceSummary) val optionGroup = Option.just(spaceSummary)
@ -120,7 +119,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
} else { } else {
// If selected group is null we force to default. It can happens when leaving the selected group. // If selected group is null we force to default. It can happens when leaving the selected group.
setState { 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 -> private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state ->
// get uptodate version of the space // 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() .firstOrNull()
if (summary?.roomSummary?.membership == Membership.INVITE) { if (summary?.membership == Membership.INVITE) {
_viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomSummary.roomId)) _viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomId))
// viewModelScope.launch(Dispatchers.IO) { // viewModelScope.launch(Dispatchers.IO) {
// tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let { // tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let {
// Timber.d("PEEK RESULT/ $it") // Timber.d("PEEK RESULT/ $it")
// } // }
// } // }
} else { } else {
if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) { if (state.selectedSpace?.roomId != action.spaceSummary.roomId) {
// state.selectedSpace?.let { // state.selectedSpace?.let {
// selectedSpaceDataSource.post(Option.just(state.selectedSpace)) // selectedSpaceDataSource.post(Option.just(state.selectedSpace))
// } // }
@ -160,8 +159,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) { private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) {
viewModelScope.launch { viewModelScope.launch {
awaitCallback { awaitCallback {
tryOrNull("Failed to leave space ${action.spaceSummary.spaceId}") { tryOrNull("Failed to leave space ${action.spaceSummary.roomId}") {
session.spaceService().getSpace(action.spaceSummary.spaceId)?.asRoom()?.leave(null, it) session.spaceService().getSpace(action.spaceSummary.roomId)?.asRoom()?.leave(null, it)
} }
} }
} }
@ -178,14 +177,12 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
excludeType = listOf(/**RoomType.MESSAGING,$*/ excludeType = listOf(/**RoomType.MESSAGING,$*/
null) null)
} }
Observable.combineLatest<SpaceSummary, List<SpaceSummary>, List<SpaceSummary>>( Observable.combineLatest<RoomSummary, List<RoomSummary>, List<RoomSummary>>(
session session
.rx() .rx()
.liveUser(session.myUserId) .liveUser(session.myUserId)
.map { optionalUser -> .map { optionalUser ->
SpaceSummary( RoomSummary(
spaceId = ALL_COMMUNITIES_GROUP_ID,
roomSummary = RoomSummary(
roomId = ALL_COMMUNITIES_GROUP_ID, roomId = ALL_COMMUNITIES_GROUP_ID,
membership = Membership.JOIN, membership = Membership.JOIN,
displayName = stringProvider.getString(R.string.group_all_communities), displayName = stringProvider.getString(R.string.group_all_communities),
@ -193,8 +190,6 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
encryptionEventTs = 0, encryptionEventTs = 0,
isEncrypted = false, isEncrypted = false,
typingUsers = emptyList() typingUsers = emptyList()
),
children = emptyList()
) )
}, },
session session
@ -205,9 +200,9 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
} }
) )
.execute { async -> .execute { async ->
val currentSelectedGroupId = selectedSpace?.spaceId val currentSelectedGroupId = selectedSpace?.roomId
val newSelectedGroup = if (currentSelectedGroupId != null) { val newSelectedGroup = if (currentSelectedGroupId != null) {
async()?.find { it.spaceId == currentSelectedGroupId } async()?.find { it.roomId == currentSelectedGroupId }
} else { } else {
async()?.firstOrNull() async()?.firstOrNull()
} }

View File

@ -35,8 +35,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session 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.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
@ -45,7 +45,7 @@ data class SpaceDirectoryState(
// The current filter // The current filter
val spaceId: String, val spaceId: String,
val currentFilter: String = "", val currentFilter: String = "",
val summary: Async<SpaceSummary> = Uninitialized, val summary: Async<RoomSummary> = Uninitialized,
// True if more result are available server side // True if more result are available server side
val hasMore: Boolean = false, val hasMore: Boolean = false,
// Set of joined roomId / spaces, // Set of joined roomId / spaces,

View File

@ -156,12 +156,12 @@ class SpacePreviewViewModel @AssistedInject constructor(
childInfoList = Success( childInfoList = Success(
resolveResult.second.map { resolveResult.second.map {
ChildInfo( ChildInfo(
roomId = it.roomSummary?.roomId ?: "", roomId = it.childRoomId,
avatarUrl = it.roomSummary?.avatarUrl, avatarUrl = it.avatarUrl,
name = it.roomSummary?.name, name = it.name,
topic = it.roomSummary?.topic, topic = it.topic,
memberCount = it.roomSummary?.joinedMembersCount, memberCount = it.activeMemberCount,
isSubSpace = it.roomSummary?.roomType == RoomType.SPACE, isSubSpace = it.roomType == RoomType.SPACE,
children = Uninitialized, children = Uninitialized,
viaServers = null viaServers = null
) )

View File

@ -5,7 +5,7 @@
android:id="@+id/itemGroupLayout" android:id="@+id/itemGroupLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="65dp" android:layout_height="65dp"
android:background="@drawable/bg_group_item" android:background="@drawable/bg_space_item"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground"> android:foreground="?attr/selectableItemBackground">

View File

@ -35,11 +35,28 @@
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" 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_constraintStart_toEndOf="@+id/groupAvatarImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/groupChildrenCollapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
tools:src="@drawable/ic_expand_more_white"
android:src="@drawable/ic_expand_less_white"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
<ImageView <ImageView
android:id="@+id/groupTmpLeave" android:id="@+id/groupTmpLeave"
android:clickable="true" android:clickable="true"
@ -61,6 +78,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="21dp" android:layout_marginEnd="21dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:visibility="gone"
android:src="@drawable/ic_arrow_right" android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator" app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -2186,7 +2186,8 @@
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string> <string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
<string name="labs_show_unread_notifications_as_tab">Add a dedicated tab for unread notifications on main screen.</string> <string name="labs_show_unread_notifications_as_tab">Add a dedicated tab for unread notifications on main screen.</string>
<string name="labs_experimental_spaces">Enable Spaces (formerly known as groups as rooms) to allow users to organise rooms into more useful groups.</string> <string name="labs_experimental_spaces">Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.</string>
<string name="labs_experimental_spaces_desc">Warning: This will trigger a clear cache and initial sync</string>
<string name="link_copied_to_clipboard">Link copied to clipboard</string> <string name="link_copied_to_clipboard">Link copied to clipboard</string>

View File

@ -49,6 +49,7 @@
<im.vector.app.core.preference.VectorSwitchPreference <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="SETTINGS_LABS_USE_SPACES" android:key="SETTINGS_LABS_USE_SPACES"
android:title="@string/labs_experimental_spaces" /> android:title="@string/labs_experimental_spaces"
android:summary="@string/labs_experimental_spaces_desc"/>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>