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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.user.model.User
@ -68,13 +67,20 @@ class RxSession(private val session: Session) {
}
}
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<SpaceSummary>> {
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<RoomSummary>> {
return session.spaceService().getSpaceSummariesLive(queryParams).asObservable()
.startWithCallable {
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>> {
return session.getBreadcrumbsLive(queryParams).asObservable()
.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.uploads.UploadsService
import org.matrix.android.sdk.api.session.search.SearchResult
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
/**
@ -90,5 +92,6 @@ interface Room :
limit: Int,
beforeLimit: Int,
afterLimit: Int,
// fun getSpaceParents(): List<SpaceSummary>
includeProfile: Boolean): SearchResult
}

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -197,4 +198,8 @@ interface RoomService {
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.build()
fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List<Membership> = 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.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
return RoomSummaryQueryParams.Builder().apply(init).build()
}
fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams {
return RoomSummaryQueryParams.Builder()
.apply(init)
.apply {
this.includeType = listOf(RoomType.SPACE)
this.excludeType = null
this.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
}.build()
}
enum class RoomCategoryFilter {
ONLY_DM,
ONLY_ROOMS,
ALL
}
/**
* This class can be used to filter room summaries to use with:
* [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
@ -37,7 +53,8 @@ data class RoomSummaryQueryParams(
val memberships: List<Membership>,
val roomCategoryFilter: RoomCategoryFilter?,
val roomTagQueryFilter: RoomTagQueryFilter?
val excludeType: List<String?>
val excludeType: List<String?>?,
val includeType: List<String?>?
) {
class Builder {
@ -49,6 +66,7 @@ data class RoomSummaryQueryParams(
var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
var roomTagQueryFilter: RoomTagQueryFilter? = null
var excludeType: List<String?> = listOf(RoomType.SPACE)
var includeType: List<String?>? = null
fun build() = RoomSummaryQueryParams(
roomId = roomId,
@ -56,8 +74,9 @@ data class RoomSummaryQueryParams(
canonicalAlias = canonicalAlias,
memberships = memberships,
roomCategoryFilter = roomCategoryFilter,
roomTagQueryFilter = roomTagQueryFilter
excludeType = excludeType
roomTagQueryFilter = roomTagQueryFilter,
excludeType = excludeType,
includeType = includeType
)
}
}

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

View File

@ -17,8 +17,16 @@
package org.matrix.android.sdk.api.session.room.model
data class SpaceChildInfo(
val roomSummary: IRoomSummary?,
val childRoomId: String,
// We might not know this child at all,
// i.e we just know it exists but no info on type/name/etc..
val isKnown: Boolean,
val roomType: String?,
val name: String?,
val topic: String?,
val avatarUrl: String?,
val order: String?,
val activeMemberCount: Int?,
val autoJoin: Boolean,
val viaServers: List<String>
)

View File

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -16,16 +16,9 @@
package org.matrix.android.sdk.api.session.room.model
interface IRoomSummary {
val roomId: String
val displayName: String
val name: String
val topic: String
val avatarUrl: String
val canonicalAlias: String?
val aliases: List<String>
val joinedMembersCount: Int?
val invitedMembersCount: Int?
val otherMemberIds: List<String>
val roomType: String?
}
data class SpaceParentInfo(
val parentId: String?,
val roomSummary: RoomSummary?,
val canonical: Boolean?,
val viaServers: List<String>
)

View File

@ -24,9 +24,9 @@ interface Space {
fun asRoom() : Room
/**
* A current snapshot of [RoomSummary] associated with the room
* A current snapshot of [RoomSummary] associated with the space
*/
fun spaceSummary(): SpaceSummary?
fun spaceSummary(): RoomSummary?
suspend fun addChildren(roomId: String, viaServers: List<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.
* @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 {
object Success : JoinSpaceResult()
@ -79,4 +79,14 @@ interface SpaceService {
viaServers: List<String> = emptyList()): JoinSpaceResult
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",
* "content": {
* "via": ["example.com"],
* "present": true,
* "canonical": true,
* }
* }
@ -38,11 +37,6 @@ data class SpaceParentContent(
* Parents where via is not present are ignored.
*/
@Json(name = "via") val via: List<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.
* 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.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.user.model.User
import java.util.Locale
@ -152,8 +151,6 @@ fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatar
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun SpaceSummary.toMatrixItem() = MatrixItem.RoomItem(spaceId, displayName, avatarUrl)
// If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)

View File

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm
import io.realm.FieldAttribute
import io.realm.RealmMigration
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
@ -31,8 +30,8 @@ import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceChildInfoEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
@ -200,21 +199,34 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
fun migrateTo10(realm: DynamicRealm) {
Timber.d("Step 9 -> 10")
realm.schema.create("SpaceChildSummaryEntity")
?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java)
?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java)
?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java)
?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true)
?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
// realm.schema.create("SpaceSummaryEntity")
// ?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY)
// ?.setRequired(SpaceSummaryEntityFields.SPACE_ID, true)
// ?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
// ?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!)
realm.schema.create("SpaceParentSummaryEntity")
?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java)
?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java)
?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true)
// ?.addRealmListField(RoomParentRelationInfoEntityFields.VIA_SERVERS.`$`, String::class.java)
// ?.addRealmObjectField(RoomParentRelationInfoEntityFields.SPACE_SUMMARY_ENTITY.`$`, realm.schema.get("SpaceSummaryEntity")!!)
?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
?.transform { obj ->
// Should I put messaging type here?
obj.setString(RoomSummaryEntityFields.ROOM_TYPE, null)
}
val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity")
?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java)
?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java)
?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
realm.schema.create("SpaceSummaryEntity")
?.addField(SpaceSummaryEntityFields.SPACE_ID, String::class.java, FieldAttribute.PRIMARY_KEY)
?.addRealmObjectField(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
?.addRealmListField(SpaceSummaryEntityFields.CHILDREN.`$`, spaceChildInfoSchema!!)
?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
}
}

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
@ -64,7 +66,29 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId,
hasFailedSending = roomSummaryEntity.hasFailedSending,
roomType = roomSummaryEntity.roomType
roomType = roomSummaryEntity.roomType,
spaceParents = roomSummaryEntity.parents.map { relationInfoEntity ->
SpaceParentInfo(
parentId = relationInfoEntity.parentRoomId,
roomSummary = relationInfoEntity.parentSummaryEntity?.let { map(it) },
canonical = relationInfoEntity.canonical ?: false,
viaServers = relationInfoEntity.viaServers.toList()
)
},
children = roomSummaryEntity.children.map {
SpaceChildInfo(
childRoomId = it.childRoomId ?: "",
isKnown = it.childSummaryEntity != null,
roomType = it.childSummaryEntity?.roomType,
name = it.childSummaryEntity?.name,
topic = it.childSummaryEntity?.topic,
avatarUrl = it.childSummaryEntity?.avatarUrl,
activeMemberCount = it.childSummaryEntity?.joinedMembersCount,
order = it.order,
autoJoin = it.autoJoin ?: false,
viaServers = it.viaServers.toList()
)
}
)
}
}

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) {
membersLoadStatusStr = value.name
}
companion object
}

View File

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

View File

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

View File

@ -22,14 +22,22 @@ import io.realm.RealmObject
/**
* Decorates room summary with space related information.
*/
internal open class SpaceChildInfoEntity(
var viaServers: RealmList<String> = RealmList(),
// Use for alphabetic ordering of this child
internal open class SpaceChildSummaryEntity(
// var isSpace: Boolean = false,
var order: String? = null,
// If true, this child should be join when parent is joined
var autoJoin: Boolean? = null,
// link to the actual room (check type to see if it's a subspace)
var roomSummaryEntity: RoomSummaryEntity? = null
var childRoomId: String? = null,
// Link to the actual space summary if it is known locally
var childSummaryEntity: RoomSummaryEntity? = null,
var viaServers: RealmList<String> = RealmList()
// var owner: RoomSummaryEntity? = null,
// var level: Int = 0
) : RealmObject() {
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.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
import org.matrix.android.sdk.api.session.room.read.ReadService
@ -36,11 +37,16 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
import org.matrix.android.sdk.api.session.search.SearchResult
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.search.SearchTask
import org.matrix.android.sdk.internal.session.space.DefaultSpace
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.awaitCallback
import java.security.InvalidParameterException
import javax.inject.Inject
@ -148,4 +154,9 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
)
)
}
override fun asSpace(): Space? {
if (roomSummary()?.roomType != RoomType.SPACE) return null
return DefaultSpace(this, roomSummaryDataSource)
}
}

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.UpdatableFilterLivePageResult
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -33,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
@ -63,7 +65,9 @@ internal class DefaultRoomService @Inject constructor(
private val peekRoomTask: PeekRoomTask,
private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
private val roomSummaryMapper: RoomSummaryMapper,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
) : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
@ -168,4 +172,18 @@ internal class DefaultRoomService @Inject constructor(
override suspend fun peekRoom(roomIdOrAlias: String): PeekResult {
return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias))
}
override fun getFlattenRoomSummaryChildOf(spaceId: String?, memberships: List<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.space.Space
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.space.DefaultSpace
import org.matrix.android.sdk.internal.session.space.SpaceSummaryDataSource
import javax.inject.Inject
internal interface SpaceGetter {
@ -28,7 +28,7 @@ internal interface SpaceGetter {
internal class DefaultSpaceGetter @Inject constructor(
private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource
private val spaceSummaryDataSource: RoomSummaryDataSource
) : SpaceGetter {
override fun get(spaceId: String): Space? {

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.toModel
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.query.whereType
@ -35,8 +36,8 @@ import timber.log.Timber
*
* - Separately, rooms can claim parents via the m.room.parent state event:
*/
internal class RoomRelationshipHelper(private val realm: Realm,
private val roomId: String
internal class RoomChildRelationInfo(private val realm: Realm,
private val roomId: String
) {
data class SpaceChildInfo(
@ -46,15 +47,24 @@ internal class RoomRelationshipHelper(private val realm: Realm,
val viaServers: List<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.
*/
fun getDirectChildrenDescriptions(): List<SpaceChildInfo> {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
.findAll()
.findAll().also {
Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId")
}
.mapNotNull {
ContentMapper.map(it.root?.content).toModel<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.
scc.via?.let { via ->
SpaceChildInfo(
@ -68,4 +78,25 @@ internal class RoomRelationshipHelper(private val realm: Realm,
}
.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 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -27,9 +28,17 @@ import io.realm.Sort
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult
import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.room.RoomCategoryFilter
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
@ -84,6 +93,36 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
)
}
fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<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> {
return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it, queryParams) },
@ -190,9 +229,134 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
}
}
queryParams.excludeType.forEach {
queryParams.excludeType?.forEach {
query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it)
}
queryParams.includeType?.forEach {
query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it)
}
queryParams.roomCategoryFilter?.let {
when (it) {
RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
RoomCategoryFilter.ALL -> {
// nop
}
}
}
return query
}
fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List<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.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceChildInfoEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.isEventRead
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.clearWith
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.relationship.RoomRelationshipHelper
import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
import timber.log.Timber
@ -62,7 +65,8 @@ internal class RoomSummaryUpdater @Inject constructor(
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver,
private val eventDecryptor: EventDecryptor,
private val crossSigningService: DefaultCrossSigningService) {
private val crossSigningService: DefaultCrossSigningService,
private val stateEventDataSource: StateEventDataSource) {
fun update(realm: Realm,
roomId: String,
@ -163,28 +167,6 @@ internal class RoomSummaryUpdater @Inject constructor(
crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
}
}
if (roomType == RoomType.SPACE) {
Timber.v("## Space: Updating summary for Space $roomId membership: ${roomSummaryEntity.membership}")
val spaceSummaryEntity = SpaceSummaryEntity()
spaceSummaryEntity.spaceId = roomId
spaceSummaryEntity.roomSummaryEntity = roomSummaryEntity
spaceSummaryEntity.children.clear()
spaceSummaryEntity.children.addAll(
RoomRelationshipHelper(realm, roomId).getDirectChildrenDescriptions()
.map {
Timber.v("## Space: Updating summary for room $roomId with info $it")
realm.createObject<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() {
@ -196,4 +178,55 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
}
/**
* Should be called at the end of the room sync, to check and validate all parent/child relations
*/
fun validateSpaceRelationship(realm: Realm) {
// Do level 0 stuffs
realm.where(RoomSummaryEntity::class.java).findAll().forEach { roomSummary ->
if (roomSummary.roomType == RoomType.SPACE) {
roomSummary.children.clearWith { it.deleteFromRealm() }
roomSummary.children.addAll(
RoomChildRelationInfo(realm, roomSummary.roomId).getDirectChildrenDescriptions()
.map {
Timber.v("## Space: Updating summary for room ${roomSummary.roomId} with info $it")
realm.createObject<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.model.create.CreateRoomParams
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.task.Task
@ -44,8 +44,8 @@ internal class DefaultCreateSpaceTask @Inject constructor(
try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(SpaceSummaryEntity::class.java)
.equalTo(SpaceSummaryEntityFields.SPACE_ID, spaceId)
realm.where(RoomSummaryEntity::class.java)
.equalTo(RoomSummaryEntityFields.ROOM_ID, spaceId)
}
} catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout(spaceId)

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.toModel
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: SpaceSummaryDataSource) : Space {
internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: RoomSummaryDataSource) : Space {
override fun asRoom(): Room {
return room
}
override fun spaceSummary(): SpaceSummary? {
override fun spaceSummary(): RoomSummary? {
return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId)
}

View File

@ -19,26 +19,37 @@ package org.matrix.android.sdk.internal.session.space
import android.net.Uri
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.SpaceGetter
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
import java.lang.IllegalArgumentException
import javax.inject.Inject
internal class DefaultSpaceService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
@UserId private val userId: String,
private val createSpaceTask: CreateSpaceTask,
// private val joinRoomTask: JoinRoomTask,
private val joinSpaceTask: JoinSpaceTask,
@ -47,8 +58,9 @@ internal class DefaultSpaceService @Inject constructor(
// private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
// private val roomIdByAliasTask: GetRoomIdByAliasTask,
// private val deleteRoomAliasTask: DeleteRoomAliasTask,
// private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource,
private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val stateEventDataSource: StateEventDataSource,
private val peekSpaceTask: PeekSpaceTask,
private val resolveSpaceInfoTask: ResolveSpaceInfoTask,
private val leaveRoomTask: LeaveRoomTask
@ -73,12 +85,12 @@ internal class DefaultSpaceService @Inject constructor(
return spaceGetter.get(spaceId)
}
override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>> {
return spaceSummaryDataSource.getRoomSummariesLive(queryParams)
override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
return roomSummaryDataSource.getSpaceSummariesLive(queryParams)
}
override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<SpaceSummary> {
return spaceSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> {
return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
}
override suspend fun peekSpace(spaceId: String): SpacePeekResult {
@ -108,21 +120,16 @@ internal class DefaultSpaceService @Inject constructor(
?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
?.content.toModel<SpaceChildContent>()
SpaceChildInfo(
roomSummary = RoomSummary(
roomId = childSummary.roomId,
roomType = childSummary.roomType,
name = childSummary.name ?: "",
displayName = childSummary.name ?: "",
topic = childSummary.topic ?: "",
joinedMembersCount = childSummary.numJoinedMembers,
avatarUrl = childSummary.avatarUrl ?: "",
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false
),
childRoomId = childSummary.roomId,
isKnown = true,
roomType = childSummary.roomType,
name = childSummary.name,
topic = childSummary.topic,
avatarUrl = childSummary.avatarUrl,
order = childStateEv?.order,
autoJoin = childStateEv?.autoJoin ?: false,
viaServers = childStateEv?.via ?: emptyList()
viaServers = childStateEv?.via ?: emptyList(),
activeMemberCount = childSummary.numJoinedMembers
)
} ?: emptyList()
)
@ -138,4 +145,42 @@ internal class DefaultSpaceService @Inject constructor(
override suspend fun rejectInvite(spaceId: String, reason: String?) {
leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
}
// override fun getSpaceParentsOfRoom(roomId: String): List<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.space.SpaceService
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import java.util.concurrent.TimeUnit
@ -45,7 +46,7 @@ internal class DefaultJoinSpaceTask @Inject constructor(
private val joinRoomTask: JoinRoomTask,
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
private val spaceSummaryDataSource: SpaceSummaryDataSource
private val roomSummaryDataSource: RoomSummaryDataSource
) : JoinSpaceTask {
override suspend fun execute(params: JoinSpaceTask.Params): SpaceService.JoinSpaceResult {
@ -65,15 +66,15 @@ internal class DefaultJoinSpaceTask @Inject constructor(
Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...")
try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm ->
realm.where(SpaceSummaryEntity::class.java)
realm.where(RoomSummaryEntity::class.java)
.apply {
if (params.roomIdOrAlias.startsWith("!")) {
equalTo(SpaceSummaryEntityFields.SPACE_ID, params.roomIdOrAlias)
equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias)
} else {
equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.CANONICAL_ALIAS, params.roomIdOrAlias)
equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, params.roomIdOrAlias)
}
}
.equalTo(SpaceSummaryEntityFields.ROOM_SUMMARY_ENTITY.MEMBERSHIP_STR, Membership.JOIN.name)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
}
} catch (exception: TimeoutCancellationException) {
Timber.w("## Space: > Error created with timeout")
@ -83,21 +84,21 @@ internal class DefaultJoinSpaceTask @Inject constructor(
val errors = HashMap<String, Throwable>()
Timber.v("## Space: > Sync done ...")
// after that i should have the children (? do I need to paginate to get state)
val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}")
val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.children?.size}")
summary?.children?.forEach {
val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}")
// val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}")
if (it.autoJoin) {
// I should try to join as well
if (childRoomSummary.roomType == RoomType.SPACE) {
if (it.roomType == RoomType.SPACE) {
// recursively join auto-joined child of this space?
when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.roomSummary.roomId, null, it.viaServers))) {
when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
SpaceService.JoinSpaceResult.Success -> {
// nop
}
is SpaceService.JoinSpaceResult.Fail -> {
errors[it.roomSummary.roomId] = subspaceJoinResult.error
errors[it.childRoomId] = subspaceJoinResult.error
}
is SpaceService.JoinSpaceResult.PartialSuccess -> {
errors.putAll(subspaceJoinResult.failedRooms)
@ -105,15 +106,15 @@ internal class DefaultJoinSpaceTask @Inject constructor(
}
} else {
try {
Timber.v("## Space: Joining room child ${childRoomSummary.roomId}")
Timber.v("## Space: Joining room child ${it.childRoomId}")
joinRoomTask.execute(JoinRoomTask.Params(
roomIdOrAlias = childRoomSummary.roomId,
roomIdOrAlias = it.childRoomId,
reason = "Auto-join parent space",
viaServers = it.viaServers
))
} catch (failure: Throwable) {
errors[it.roomSummary.roomId] = failure
Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}")
errors[it.childRoomId] = failure
Timber.e("## Space: Failed to join room child ${it.childRoomId}")
}
}
}

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.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter)
// post room sync validation
roomSummaryUpdater.validateSpaceRelationship(realm)
}
// PRIVATE METHODS *****************************************************************************

View File

@ -18,9 +18,9 @@ package im.vector.app.features.grouplist
import arrow.core.Option
import im.vector.app.core.utils.BehaviorDataSource
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SelectedSpaceDataSource @Inject constructor() : BehaviorDataSource<Option<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.ServerBackupStatusViewState
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import timber.log.Timber
@ -252,10 +252,10 @@ class HomeDetailFragment @Inject constructor(
}
}
private fun onSpaceChange(spaceSummary: SpaceSummary?) {
private fun onSpaceChange(spaceSummary: RoomSummary?) {
spaceSummary?.let {
// Use GlideApp with activity context to avoid the glideRequests to be paused
if (spaceSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) {
if (spaceSummary.roomId == ALL_COMMUNITIES_GROUP_ID) {
// Special case
views.groupToolbarAvatarImageView.background = ContextCompat.getDrawable(requireContext(), R.drawable.space_home_background)
views.groupToolbarAvatarImageView.scaleType = ImageView.ScaleType.CENTER_INSIDE

View File

@ -22,12 +22,11 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.sync.SyncState
data class HomeDetailViewState(
val groupSummary: Option<GroupSummary> = Option.empty(),
val spaceSummary: Option<SpaceSummary> = Option.empty(),
val spaceSummary: Option<RoomSummary> = Option.empty(),
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE,
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_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS,
EventType.REDACTION,
EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_CANDIDATES,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params)
EventType.STATE_ROOM_POWER_LEVELS,
EventType.STATE_SPACE_CHILD,
EventType.STATE_SPACE_PARENT,
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)

View File

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

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 {
Timber.d("## Nav: Switching to space $spaceId / ${it.roomSummary.name}")
Timber.d("## Nav: Switching to space $spaceId / ${it.name}")
selectedSpaceDataSource.post(Option.just(it))
} ?: kotlin.run {
Timber.d("## Nav: Failed to switch to space $spaceId")

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.provider.MediaStore
import androidx.core.content.edit
import androidx.lifecycle.LiveData
import com.squareup.seismic.ShakeDetector
import im.vector.app.BuildConfig
import im.vector.app.R
@ -312,6 +313,14 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_USE_SPACES, false)
}
fun labSpacesLive(): LiveData<Boolean> {
return SharedPreferenceLiveData.booleanLiveData(
defaultPrefs,
SETTINGS_LABS_USE_SPACES,
false
)
}
fun failFast(): Boolean {
return BuildConfig.DEBUG || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false))
}

View File

@ -17,6 +17,9 @@
package im.vector.app.features.settings
import im.vector.app.R
import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import javax.inject.Inject
class VectorSettingsLabsFragment @Inject constructor(
@ -27,6 +30,11 @@ class VectorSettingsLabsFragment @Inject constructor(
override val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() {
// Nothing to do
findPreference<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() }
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.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.features.home.HomeActivitySharedAction
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
class SpaceListFragment @Inject constructor(
@ -76,11 +76,11 @@ class SpaceListFragment @Inject constructor(
spaceController.update(state)
}
override fun onSpaceSelected(spaceSummary: SpaceSummary) {
override fun onSpaceSelected(spaceSummary: RoomSummary) {
viewModel.handle(SpaceListAction.SelectSpace(spaceSummary))
}
override fun onLeaveSpace(spaceSummary: SpaceSummary) {
override fun onLeaveSpace(spaceSummary: RoomSummary) {
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.home.AvatarRenderer
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 javax.inject.Inject
@ -51,13 +51,13 @@ class SpaceSummaryController @Inject constructor(
buildGroupModels(nonNullViewState.asyncSpaces(), nonNullViewState.selectedSpace)
}
private fun buildGroupModels(summaries: List<SpaceSummary>?, selected: SpaceSummary?) {
private fun buildGroupModels(summaries: List<RoomSummary>?, selected: RoomSummary?) {
if (summaries.isNullOrEmpty()) {
return
}
// show invites on top
summaries.filter { it.roomSummary.membership == Membership.INVITE }
summaries.filter { it.membership == Membership.INVITE }
.let { invites ->
if (invites.isNotEmpty()) {
genericItemHeader {
@ -67,7 +67,7 @@ class SpaceSummaryController @Inject constructor(
invites.forEach {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(it.spaceId)
id(it.roomId)
matrixItem(it.toMatrixItem())
selected(false)
listener { callback?.onSpaceSelected(it) }
@ -87,19 +87,19 @@ class SpaceSummaryController @Inject constructor(
}
summaries
.filter { it.roomSummary.membership == Membership.JOIN }
.filter { it.membership == Membership.JOIN }
.forEach { groupSummary ->
val isSelected = groupSummary.spaceId == selected?.spaceId
if (groupSummary.spaceId == ALL_COMMUNITIES_GROUP_ID) {
val isSelected = groupSummary.roomId == selected?.roomId
if (groupSummary.roomId == ALL_COMMUNITIES_GROUP_ID) {
homeSpaceSummaryItem {
id(groupSummary.spaceId)
id(groupSummary.roomId)
selected(isSelected)
listener { callback?.onSpaceSelected(groupSummary) }
}
} else {
spaceSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.spaceId)
id(groupSummary.roomId)
matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
onLeave { callback?.onLeaveSpace(groupSummary) }
@ -119,8 +119,8 @@ class SpaceSummaryController @Inject constructor(
}
interface Callback {
fun onSpaceSelected(spaceSummary: SpaceSummary)
fun onLeaveSpace(spaceSummary: SpaceSummary)
fun onSpaceSelected(spaceSummary: RoomSummary)
fun onLeaveSpace(spaceSummary: RoomSummary)
fun onAddSpaceSelected()
}
}

View File

@ -18,6 +18,8 @@ package im.vector.app.features.spaces
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@ -37,6 +39,8 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var onLeave: (() -> Unit)? = null
@EpoxyAttribute var toggleExpand: (() -> Unit)? = null
@EpoxyAttribute var expanded: Boolean? = null
override fun bind(holder: Holder) {
super.bind(holder)
@ -52,6 +56,26 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
} else {
holder.leaveView.isVisible = false
}
when (expanded) {
null -> {
holder.collapseIndicator.isGone = true
}
else -> {
holder.collapseIndicator.isVisible = true
holder.collapseIndicator.setImageDrawable(
ContextCompat.getDrawable(holder.view.context,
if (expanded!!) R.drawable.ic_expand_less else R.drawable.ic_expand_more
)
)
holder.collapseIndicator.setOnClickListener(
DebouncedClickListener({ _ ->
toggleExpand?.invoke()
})
)
}
}
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
}
@ -65,5 +89,6 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
val groupNameView by bind<TextView>(R.id.groupNameView)
val rootView by bind<CheckableConstraintLayout>(R.id.itemGroupLayout)
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.RoomSummary
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID"
sealed class SpaceListAction : VectorViewModelAction {
data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction()
data class LeaveSpace(val spaceSummary: SpaceSummary) : SpaceListAction()
data class SelectSpace(val spaceSummary: RoomSummary) : SpaceListAction()
data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction()
object AddSpace : SpaceListAction()
}
@ -64,8 +63,8 @@ sealed class SpaceListViewEvents : VectorViewEvents {
}
data class SpaceListViewState(
val asyncSpaces: Async<List<SpaceSummary>> = Uninitialized,
val selectedSpace: SpaceSummary? = null
val asyncSpaces: Async<List<RoomSummary>> = Uninitialized,
val selectedSpace: RoomSummary? = null
) : MvRxState
class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: SpaceListViewState,
@ -96,7 +95,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
selectedSpaceDataSource
.observe()
.subscribe {
if (currentGroupId != it.orNull()?.spaceId) {
if (currentGroupId != it.orNull()?.roomId) {
setState {
copy(
selectedSpace = it.orNull()
@ -111,8 +110,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
selectSubscribe(SpaceListViewState::selectedSpace) { spaceSummary ->
if (spaceSummary != null) {
// We only want to open group if the updated selectedGroup is a different one.
if (currentGroupId != spaceSummary.spaceId) {
currentGroupId = spaceSummary.spaceId
if (currentGroupId != spaceSummary.roomId) {
currentGroupId = spaceSummary.roomId
_viewEvents.post(SpaceListViewEvents.OpenSpace)
}
val optionGroup = Option.just(spaceSummary)
@ -120,7 +119,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
} else {
// If selected group is null we force to default. It can happens when leaving the selected group.
setState {
copy(selectedSpace = this.asyncSpaces()?.find { it.spaceId == ALL_COMMUNITIES_GROUP_ID })
copy(selectedSpace = this.asyncSpaces()?.find { it.roomId == ALL_COMMUNITIES_GROUP_ID })
}
}
}
@ -138,17 +137,17 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state ->
// get uptodate version of the space
val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.spaceId) })
val summary = session.spaceService().getSpaceSummaries(roomSummaryQueryParams { roomId = QueryStringValue.Equals(action.spaceSummary.roomId) })
.firstOrNull()
if (summary?.roomSummary?.membership == Membership.INVITE) {
_viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomSummary.roomId))
if (summary?.membership == Membership.INVITE) {
_viewEvents.post(SpaceListViewEvents.OpenSpaceSummary(summary.roomId))
// viewModelScope.launch(Dispatchers.IO) {
// tryOrNull { session.spaceService().peekSpace(action.spaceSummary.spaceId) }.let {
// Timber.d("PEEK RESULT/ $it")
// }
// }
} else {
if (state.selectedSpace?.spaceId != action.spaceSummary.spaceId) {
if (state.selectedSpace?.roomId != action.spaceSummary.roomId) {
// state.selectedSpace?.let {
// selectedSpaceDataSource.post(Option.just(state.selectedSpace))
// }
@ -160,8 +159,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
private fun handleLeaveSpace(action: SpaceListAction.LeaveSpace) {
viewModelScope.launch {
awaitCallback {
tryOrNull("Failed to leave space ${action.spaceSummary.spaceId}") {
session.spaceService().getSpace(action.spaceSummary.spaceId)?.asRoom()?.leave(null, it)
tryOrNull("Failed to leave space ${action.spaceSummary.roomId}") {
session.spaceService().getSpace(action.spaceSummary.roomId)?.asRoom()?.leave(null, it)
}
}
}
@ -178,23 +177,19 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
excludeType = listOf(/**RoomType.MESSAGING,$*/
null)
}
Observable.combineLatest<SpaceSummary, List<SpaceSummary>, List<SpaceSummary>>(
Observable.combineLatest<RoomSummary, List<RoomSummary>, List<RoomSummary>>(
session
.rx()
.liveUser(session.myUserId)
.map { optionalUser ->
SpaceSummary(
spaceId = ALL_COMMUNITIES_GROUP_ID,
roomSummary = RoomSummary(
roomId = ALL_COMMUNITIES_GROUP_ID,
membership = Membership.JOIN,
displayName = stringProvider.getString(R.string.group_all_communities),
avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "",
encryptionEventTs = 0,
isEncrypted = false,
typingUsers = emptyList()
),
children = emptyList()
RoomSummary(
roomId = ALL_COMMUNITIES_GROUP_ID,
membership = Membership.JOIN,
displayName = stringProvider.getString(R.string.group_all_communities),
avatarUrl = optionalUser.getOrNull()?.avatarUrl ?: "",
encryptionEventTs = 0,
isEncrypted = false,
typingUsers = emptyList()
)
},
session
@ -205,9 +200,9 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
}
)
.execute { async ->
val currentSelectedGroupId = selectedSpace?.spaceId
val currentSelectedGroupId = selectedSpace?.roomId
val newSelectedGroup = if (currentSelectedGroupId != null) {
async()?.find { it.spaceId == currentSelectedGroupId }
async()?.find { it.roomId == currentSelectedGroupId }
} else {
async()?.firstOrNull()
}

View File

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

View File

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

View File

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

View File

@ -35,11 +35,28 @@
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
app:layout_constraintEnd_toStartOf="@+id/groupTmpLeave"
app:layout_constraintEnd_toStartOf="@+id/groupChildrenCollapse"
app:layout_constraintStart_toEndOf="@+id/groupAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
<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
android:id="@+id/groupTmpLeave"
android:clickable="true"
@ -61,6 +78,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="21dp"
android:importantForAccessibility="no"
android:visibility="gone"
android:src="@drawable/ic_arrow_right"
app:layout_constraintBottom_toTopOf="@+id/groupBottomSeparator"
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_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>

View File

@ -49,6 +49,7 @@
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
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>