mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge pull request #2840 from vector-im/feature/bca/spaces_sdk
Spaces support - beta
This commit is contained in:
commit
751efb57fc
312 changed files with 14432 additions and 1165 deletions
|
@ -2,7 +2,7 @@ Changes in Element 1.1.7 (2021-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Spaces beta
|
||||
|
||||
Improvements 🙌:
|
||||
- Add ability to install APK from directly from Element (#2381)
|
||||
|
|
|
@ -39,6 +39,7 @@ 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.SpaceSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
|
@ -66,6 +67,13 @@ class RxSession(private val session: Session) {
|
|||
}
|
||||
}
|
||||
|
||||
fun liveSpaceSummaries(queryParams: SpaceSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.spaceService().getSpaceSummariesLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.spaceService().getSpaceSummaries(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.getBreadcrumbsLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* 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 kotlinx.coroutines.delay
|
||||
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.query.QueryStringValue
|
||||
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.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
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.RoomGuestAccessContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
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.model.create.CreateRoomPreset
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class SpaceCreationTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
|
||||
@Test
|
||||
fun createSimplePublicSpace() {
|
||||
val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
|
||||
val roomName = "My Space"
|
||||
val topic = "A public space for test"
|
||||
val spaceId: String
|
||||
runBlocking {
|
||||
spaceId = session.spaceService().createSpace(roomName, topic, null, true)
|
||||
// wait a bit to let the summary update it self :/
|
||||
delay(400)
|
||||
}
|
||||
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
assertEquals(roomName, syncedSpace?.asRoom()?.roomSummary()?.name, "Room name should be set")
|
||||
assertEquals(topic, syncedSpace?.asRoom()?.roomSummary()?.topic, "Room topic should be set")
|
||||
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
|
||||
|
||||
assertNotNull(syncedSpace, "Space should be found by Id")
|
||||
val creationEvent = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE)
|
||||
val createContent = creationEvent?.content.toModel<RoomCreateContent>()
|
||||
assertEquals(RoomType.SPACE, createContent?.type, "Room type should be space")
|
||||
|
||||
var powerLevelsContent: PowerLevelsContent? = null
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val toModel = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)?.content.toModel<PowerLevelsContent>()
|
||||
powerLevelsContent = toModel
|
||||
toModel != null
|
||||
}
|
||||
}
|
||||
assertEquals(100, powerLevelsContent?.eventsDefault, "Space-rooms should be created with a power level for events_default of 100")
|
||||
|
||||
val guestAccess = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS)?.content
|
||||
?.toModel<RoomGuestAccessContent>()?.guestAccess
|
||||
|
||||
assertEquals(GuestAccess.CanJoin, guestAccess, "Public space room should be peekable by guest")
|
||||
|
||||
val historyVisibility = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)?.content
|
||||
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||
|
||||
assertEquals(RoomHistoryVisibility.WORLD_READABLE, historyVisibility, "Public space room should be world readable")
|
||||
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJoinSimplePublicSpace() {
|
||||
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
|
||||
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
|
||||
|
||||
val roomName = "My Space"
|
||||
val topic = "A public space for test"
|
||||
val spaceId: String
|
||||
runBlocking {
|
||||
spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true)
|
||||
// wait a bit to let the summary update it self :/
|
||||
delay(400)
|
||||
}
|
||||
|
||||
// Try to join from bob, it's a public space no need to invite
|
||||
|
||||
val joinResult: JoinSpaceResult
|
||||
runBlocking {
|
||||
joinResult = bobSession.spaceService().joinSpace(spaceId)
|
||||
}
|
||||
|
||||
assertEquals(JoinSpaceResult.Success, joinResult)
|
||||
|
||||
val spaceBobPov = bobSession.spaceService().getSpace(spaceId)
|
||||
assertEquals(roomName, spaceBobPov?.asRoom()?.roomSummary()?.name, "Room name should be set")
|
||||
assertEquals(topic, spaceBobPov?.asRoom()?.roomSummary()?.topic, "Room topic should be set")
|
||||
|
||||
commonTestHelper.signOutAndClose(aliceSession)
|
||||
commonTestHelper.signOutAndClose(bobSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimplePublicSpaceWithChildren() {
|
||||
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
|
||||
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
|
||||
|
||||
val roomName = "My Space"
|
||||
val topic = "A public space for test"
|
||||
|
||||
val spaceId: String = runBlocking { aliceSession.spaceService().createSpace(roomName, topic, null, true) }
|
||||
val syncedSpace = aliceSession.spaceService().getSpace(spaceId)
|
||||
|
||||
// create a room
|
||||
val firstChild: String = runBlocking {
|
||||
aliceSession.createRoom(CreateRoomParams().apply {
|
||||
this.name = "FirstRoom"
|
||||
this.topic = "Description of first room"
|
||||
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
|
||||
})
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true)
|
||||
}
|
||||
|
||||
val secondChild: String = runBlocking {
|
||||
aliceSession.createRoom(CreateRoomParams().apply {
|
||||
this.name = "SecondRoom"
|
||||
this.topic = "Description of second room"
|
||||
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
|
||||
})
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false)
|
||||
}
|
||||
|
||||
// Try to join from bob, it's a public space no need to invite
|
||||
|
||||
val joinResult = runBlocking {
|
||||
bobSession.spaceService().joinSpace(spaceId)
|
||||
}
|
||||
|
||||
assertEquals(JoinSpaceResult.Success, joinResult)
|
||||
|
||||
val spaceBobPov = bobSession.spaceService().getSpace(spaceId)
|
||||
assertEquals(roomName, spaceBobPov?.asRoom()?.roomSummary()?.name, "Room name should be set")
|
||||
assertEquals(topic, spaceBobPov?.asRoom()?.roomSummary()?.topic, "Room topic should be set")
|
||||
|
||||
// check if bob has joined automatically the first room
|
||||
|
||||
val bobMembershipFirstRoom = bobSession.getRoom(firstChild)?.roomSummary()?.membership
|
||||
assertEquals(Membership.JOIN, bobMembershipFirstRoom, "Bob should have joined this room")
|
||||
RoomSummaryQueryParams.Builder()
|
||||
|
||||
val spaceSummaryBobPov = bobSession.spaceService().getSpaceSummaries(roomSummaryQueryParams {
|
||||
this.roomId = QueryStringValue.Equals(spaceId)
|
||||
this.memberships = listOf(Membership.JOIN)
|
||||
}).firstOrNull()
|
||||
|
||||
assertEquals(2, spaceSummaryBobPov?.spaceChildren?.size ?: -1, "Unexpected number of children")
|
||||
|
||||
commonTestHelper.signOutAndClose(aliceSession)
|
||||
commonTestHelper.signOutAndClose(bobSession)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* 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("John", 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 summary update it self :/
|
||||
delay(400)
|
||||
}
|
||||
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
|
||||
val roomId = runBlocking {
|
||||
session.createRoom(CreateRoomParams().apply { name = "General" })
|
||||
}
|
||||
|
||||
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 {
|
||||
runBlocking {
|
||||
session.createRoom(CreateRoomParams().apply { name = it.first })
|
||||
} 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 = runBlocking {
|
||||
session.createRoom(CreateRoomParams().apply { name = "O1" })
|
||||
}
|
||||
val orphan2 = runBlocking {
|
||||
session.createRoom(CreateRoomParams().apply { name = "O2" })
|
||||
}
|
||||
|
||||
val allRooms = session.getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
|
||||
|
||||
assertEquals(9, allRooms.size, "Unexpected number of rooms")
|
||||
|
||||
val orphans = session.getFlattenRoomSummaryChildrenOf(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.getFlattenRoomSummaryChildrenOf(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 = runBlocking {
|
||||
session.createRoom(CreateRoomParams().apply { name = "A3" })
|
||||
}
|
||||
runBlocking {
|
||||
spaceA!!.addChildren(a3, viaServers, null, false)
|
||||
delay(400)
|
||||
// here we do not set the parent!!
|
||||
}
|
||||
|
||||
val orphansUpdate = session.getFlattenRoomSummaryChildrenOf(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.getFlattenRoomSummaryChildrenOf(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.getFlattenRoomSummaryChildrenOfLive(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
|
||||
runBlocking {
|
||||
bRoom!!.leave(null)
|
||||
}
|
||||
// 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 ->
|
||||
runBlocking {
|
||||
session.createRoom(CreateRoomParams().apply { name = entry.first })
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRootSpaces() {
|
||||
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)
|
||||
))
|
||||
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
|
||||
// add C as subspace of B
|
||||
runBlocking {
|
||||
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
||||
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||
}
|
||||
|
||||
Thread.sleep(2000)
|
||||
// + A
|
||||
// a1, a2
|
||||
// + B
|
||||
// b1, b2, b3
|
||||
// + C
|
||||
// + c1, c2
|
||||
|
||||
val rootSpaces = session.spaceService().getRootSpaceSummaries()
|
||||
|
||||
assertEquals(2, rootSpaces.size, "Unexpected number of root spaces ${rootSpaces.map { it.name }}")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 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.api.query
|
||||
|
||||
sealed class ActiveSpaceFilter {
|
||||
object None : ActiveSpaceFilter()
|
||||
data class ActiveSpace(val currentSpaceId: String?) : ActiveSpaceFilter()
|
||||
data class ExcludeSpace(val spaceId: String) : ActiveSpaceFilter()
|
||||
}
|
|
@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.search.SearchService
|
|||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||
import org.matrix.android.sdk.api.session.space.SpaceService
|
||||
import org.matrix.android.sdk.api.session.sync.FilterService
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||
|
@ -227,6 +228,11 @@ interface Session :
|
|||
*/
|
||||
fun thirdPartyService(): ThirdPartyService
|
||||
|
||||
/**
|
||||
* Returns the space service associated with the session
|
||||
*/
|
||||
fun spaceService(): SpaceService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
|
|
@ -52,6 +52,12 @@ object EventType {
|
|||
const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
|
||||
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
|
||||
|
||||
// const val STATE_SPACE_CHILD = "m.space.child"
|
||||
const val STATE_SPACE_CHILD = "org.matrix.msc1772.space.child"
|
||||
|
||||
// const val STATE_SPACE_PARENT = "m.space.parent"
|
||||
const val STATE_SPACE_PARENT = "org.matrix.msc1772.space.parent"
|
||||
|
||||
/**
|
||||
* Note that this Event has been deprecated, see
|
||||
* - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||
|
@ -74,6 +80,7 @@ object EventType {
|
|||
const val CALL_NEGOTIATE = "m.call.negotiate"
|
||||
const val CALL_REJECT = "m.call.reject"
|
||||
const val CALL_HANGUP = "m.call.hangup"
|
||||
|
||||
// This type is not processed by the client, just sent to the server
|
||||
const val CALL_REPLACES = "m.call.replaces"
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ 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.Optional
|
||||
|
||||
/**
|
||||
|
@ -91,4 +92,9 @@ interface Room :
|
|||
beforeLimit: Int,
|
||||
afterLimit: Int,
|
||||
includeProfile: Boolean): SearchResult
|
||||
|
||||
/**
|
||||
* Use this room as a Space, if the type is correct.
|
||||
*/
|
||||
fun asSpace(): Space?
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -177,13 +178,15 @@ interface RoomService {
|
|||
* TODO Doc
|
||||
*/
|
||||
fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config = defaultPagedListConfig): LiveData<PagedList<RoomSummary>>
|
||||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): LiveData<PagedList<RoomSummary>>
|
||||
|
||||
/**
|
||||
* TODO Doc
|
||||
*/
|
||||
fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config = defaultPagedListConfig): UpdatableFilterLivePageResult
|
||||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
|
||||
|
||||
/**
|
||||
* TODO Doc
|
||||
|
@ -197,4 +200,12 @@ interface RoomService {
|
|||
.setEnablePlaceholders(false)
|
||||
.setPrefetchDistance(10)
|
||||
.build()
|
||||
|
||||
fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List<Membership> = Membership.activeMemberships()) : List<RoomSummary>
|
||||
|
||||
/**
|
||||
* Returns all the children of this space, as LiveData
|
||||
*/
|
||||
fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?,
|
||||
memberships: List<Membership> = Membership.activeMemberships()): LiveData<List<RoomSummary>>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* 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.
|
||||
|
@ -12,14 +12,12 @@
|
|||
* 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.grouplist
|
||||
package org.matrix.android.sdk.api.session.room
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
|
||||
sealed class GroupListAction : VectorViewModelAction {
|
||||
data class SelectGroup(val groupSummary: GroupSummary) : GroupListAction()
|
||||
enum class RoomSortOrder {
|
||||
NAME,
|
||||
ACTIVITY,
|
||||
NONE
|
||||
}
|
|
@ -16,15 +16,35 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room
|
||||
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
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 {
|
||||
includeType = listOf(RoomType.SPACE)
|
||||
excludeType = null
|
||||
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]
|
||||
|
@ -35,7 +55,11 @@ data class RoomSummaryQueryParams(
|
|||
val canonicalAlias: QueryStringValue,
|
||||
val memberships: List<Membership>,
|
||||
val roomCategoryFilter: RoomCategoryFilter?,
|
||||
val roomTagQueryFilter: RoomTagQueryFilter?
|
||||
val roomTagQueryFilter: RoomTagQueryFilter?,
|
||||
val excludeType: List<String?>?,
|
||||
val includeType: List<String?>?,
|
||||
val activeSpaceId: ActiveSpaceFilter?,
|
||||
var activeGroupId: String? = null
|
||||
) {
|
||||
|
||||
class Builder {
|
||||
|
@ -46,6 +70,10 @@ data class RoomSummaryQueryParams(
|
|||
var memberships: List<Membership> = Membership.all()
|
||||
var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
|
||||
var roomTagQueryFilter: RoomTagQueryFilter? = null
|
||||
var excludeType: List<String?>? = listOf(RoomType.SPACE)
|
||||
var includeType: List<String?>? = null
|
||||
var activeSpaceId: ActiveSpaceFilter = ActiveSpaceFilter.None
|
||||
var activeGroupId: String? = null
|
||||
|
||||
fun build() = RoomSummaryQueryParams(
|
||||
roomId = roomId,
|
||||
|
@ -53,7 +81,11 @@ data class RoomSummaryQueryParams(
|
|||
canonicalAlias = canonicalAlias,
|
||||
memberships = memberships,
|
||||
roomCategoryFilter = roomCategoryFilter,
|
||||
roomTagQueryFilter = roomTagQueryFilter
|
||||
roomTagQueryFilter = roomTagQueryFilter,
|
||||
excludeType = excludeType,
|
||||
includeType = includeType,
|
||||
activeSpaceId = activeSpaceId,
|
||||
activeGroupId = activeGroupId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.paging.PagedList
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
||||
interface UpdatableFilterLivePageResult {
|
||||
interface UpdatableLivePageResult {
|
||||
val livePagedList: LiveData<PagedList<RoomSummary>>
|
||||
|
||||
fun updateQuery(queryParams: RoomSummaryQueryParams)
|
||||
fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
package org.matrix.android.sdk.api.session.room.alias
|
||||
|
||||
sealed class RoomAliasError : Throwable() {
|
||||
object AliasEmpty : RoomAliasError()
|
||||
object AliasIsBlank : RoomAliasError()
|
||||
object AliasNotAvailable : RoomAliasError()
|
||||
object AliasInvalid : RoomAliasError()
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
|
|||
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
|
||||
|
||||
sealed class CreateRoomFailure : Failure.FeatureFailure() {
|
||||
object CreatedWithTimeout : CreateRoomFailure()
|
||||
data class CreatedWithTimeout(val roomID: String) : CreateRoomFailure()
|
||||
data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure()
|
||||
data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure()
|
||||
}
|
||||
|
|
|
@ -28,43 +28,43 @@ data class PowerLevelsContent(
|
|||
/**
|
||||
* The level required to ban a user. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@Json(name = "ban") val ban: Int = Role.Moderator.value,
|
||||
@Json(name = "ban") val ban: Int? = null,
|
||||
/**
|
||||
* The level required to kick a user. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@Json(name = "kick") val kick: Int = Role.Moderator.value,
|
||||
@Json(name = "kick") val kick: Int? = null,
|
||||
/**
|
||||
* The level required to invite a user. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@Json(name = "invite") val invite: Int = Role.Moderator.value,
|
||||
@Json(name = "invite") val invite: Int? = null,
|
||||
/**
|
||||
* The level required to redact an event. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@Json(name = "redact") val redact: Int = Role.Moderator.value,
|
||||
@Json(name = "redact") val redact: Int? = null,
|
||||
/**
|
||||
* The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified.
|
||||
*/
|
||||
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
|
||||
@Json(name = "events_default") val eventsDefault: Int? = null,
|
||||
/**
|
||||
* The level required to send specific event types. This is a mapping from event type to power level required.
|
||||
*/
|
||||
@Json(name = "events") val events: Map<String, Int> = emptyMap(),
|
||||
@Json(name = "events") val events: Map<String, Int>? = null,
|
||||
/**
|
||||
* The default power level for every user in the room, unless their user_id is mentioned in the users key. Defaults to 0 if unspecified.
|
||||
*/
|
||||
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
|
||||
@Json(name = "users_default") val usersDefault: Int? = null,
|
||||
/**
|
||||
* The power levels for specific users. This is a mapping from user_id to power level for that user.
|
||||
*/
|
||||
@Json(name = "users") val users: Map<String, Int> = emptyMap(),
|
||||
@Json(name = "users") val users: Map<String, Int>? = null,
|
||||
/**
|
||||
* The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
|
||||
@Json(name = "state_default") val stateDefault: Int? = null,
|
||||
/**
|
||||
* The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key.
|
||||
*/
|
||||
@Json(name = "notifications") val notifications: Map<String, Any> = emptyMap()
|
||||
@Json(name = "notifications") val notifications: Map<String, Any>? = null
|
||||
) {
|
||||
/**
|
||||
* Return a copy of this content with a new power level for the specified user
|
||||
|
@ -74,7 +74,7 @@ data class PowerLevelsContent(
|
|||
*/
|
||||
fun setUserPowerLevel(userId: String, powerLevel: Int?): PowerLevelsContent {
|
||||
return copy(
|
||||
users = users.toMutableMap().apply {
|
||||
users = users.orEmpty().toMutableMap().apply {
|
||||
if (powerLevel == null || powerLevel == usersDefault) {
|
||||
remove(userId)
|
||||
} else {
|
||||
|
@ -91,7 +91,7 @@ data class PowerLevelsContent(
|
|||
* @return the level, default to Moderator if the key is not found
|
||||
*/
|
||||
fun notificationLevel(key: String): Int {
|
||||
return when (val value = notifications[key]) {
|
||||
return when (val value = notifications.orEmpty()[key]) {
|
||||
// the first implementation was a string value
|
||||
is String -> value.toInt()
|
||||
is Double -> value.toInt()
|
||||
|
@ -107,3 +107,12 @@ data class PowerLevelsContent(
|
|||
const val NOTIFICATIONS_ROOM_KEY = "room"
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default value, defined in the Matrix specification
|
||||
fun PowerLevelsContent.banOrDefault() = ban ?: Role.Moderator.value
|
||||
fun PowerLevelsContent.kickOrDefault() = kick ?: Role.Moderator.value
|
||||
fun PowerLevelsContent.inviteOrDefault() = invite ?: Role.Moderator.value
|
||||
fun PowerLevelsContent.redactOrDefault() = redact ?: Role.Moderator.value
|
||||
fun PowerLevelsContent.eventsDefaultOrDefault() = eventsDefault ?: Role.Default.value
|
||||
fun PowerLevelsContent.usersDefaultOrDefault() = usersDefault ?: Role.Default.value
|
||||
fun PowerLevelsContent.stateDefaultOrDefault() = stateDefault ?: Role.Moderator.value
|
||||
|
|
|
@ -40,7 +40,7 @@ data class RoomGuestAccessContent(
|
|||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class GuestAccess {
|
||||
@Json(name = "can_join") CanJoin,
|
||||
@Json(name = "forbidden") Forbidden
|
||||
enum class GuestAccess(val value: String) {
|
||||
@Json(name = "can_join") CanJoin("can_join"),
|
||||
@Json(name = "forbidden") Forbidden("forbidden")
|
||||
}
|
||||
|
|
|
@ -16,35 +16,31 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RoomHistoryVisibility {
|
||||
/**
|
||||
* All events while this is the m.room.history_visibility value may be shared by any
|
||||
* participating homeserver with anyone, regardless of whether they have ever joined the room.
|
||||
*/
|
||||
@Json(name = "world_readable") WORLD_READABLE,
|
||||
WORLD_READABLE,
|
||||
|
||||
/**
|
||||
* Previous events are always accessible to newly joined members. All events in the
|
||||
* room are accessible, even those sent when the member was not a part of the room.
|
||||
*/
|
||||
@Json(name = "shared") SHARED,
|
||||
SHARED,
|
||||
|
||||
/**
|
||||
* Events are accessible to newly joined members from the point they were invited onwards.
|
||||
* Events stop being accessible when the member's state changes to something other than invite or join.
|
||||
*/
|
||||
@Json(name = "invited") INVITED,
|
||||
INVITED,
|
||||
|
||||
/**
|
||||
* Events are accessible to newly joined members from the point they joined the room onwards.
|
||||
* Events stop being accessible when the member's state changes to something other than join.
|
||||
*/
|
||||
@Json(name = "joined") JOINED
|
||||
JOINED
|
||||
}
|
||||
|
|
|
@ -24,9 +24,10 @@ import com.squareup.moshi.JsonClass
|
|||
* Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RoomJoinRules {
|
||||
@Json(name = "public") PUBLIC,
|
||||
@Json(name = "invite") INVITE,
|
||||
@Json(name = "knock") KNOCK,
|
||||
@Json(name = "private") PRIVATE
|
||||
enum class RoomJoinRules(val value: String) {
|
||||
@Json(name = "public") PUBLIC("public"),
|
||||
@Json(name = "invite") INVITE("invite"),
|
||||
@Json(name = "knock") KNOCK("knock"),
|
||||
@Json(name = "private") PRIVATE("private"),
|
||||
@Json(name = "restricted") RESTRICTED("restricted")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomJoinRulesAllowEntry(
|
||||
/**
|
||||
* space: The room ID of the space to check the membership of.
|
||||
*/
|
||||
@Json(name = "space") val spaceID: String,
|
||||
/**
|
||||
* via: A list of servers which may be used to peek for membership of the space.
|
||||
*/
|
||||
@Json(name = "via") val via: List<String>
|
||||
)
|
|
@ -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.
|
||||
|
@ -26,13 +27,18 @@ import timber.log.Timber
|
|||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomJoinRulesContent(
|
||||
@Json(name = "join_rule") val _joinRules: String? = null
|
||||
@Json(name = "join_rule") val _joinRules: String? = null,
|
||||
/**
|
||||
* If the allow key is an empty list (or not a list at all), then the room reverts to standard public join rules
|
||||
*/
|
||||
@Json(name = "allow") val allowList: List<RoomJoinRulesAllowEntry>? = null
|
||||
) {
|
||||
val joinRules: RoomJoinRules? = when (_joinRules) {
|
||||
"public" -> RoomJoinRules.PUBLIC
|
||||
"invite" -> RoomJoinRules.INVITE
|
||||
"knock" -> RoomJoinRules.KNOCK
|
||||
"private" -> RoomJoinRules.PRIVATE
|
||||
"restricted" -> RoomJoinRules.RESTRICTED
|
||||
else -> {
|
||||
Timber.w("Invalid value for RoomJoinRules: `$_joinRules`")
|
||||
null
|
||||
|
|
|
@ -55,7 +55,11 @@ data class RoomSummary constructor(
|
|||
val inviterId: String? = null,
|
||||
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
|
||||
val hasFailedSending: Boolean = false
|
||||
val hasFailedSending: Boolean = false,
|
||||
val roomType: String? = null,
|
||||
val spaceParents: List<SpaceParentInfo>? = null,
|
||||
val spaceChildren: List<SpaceChildInfo>? = null,
|
||||
val flattenParentIds: List<String> = emptyList()
|
||||
) {
|
||||
|
||||
val isVersioned: Boolean
|
||||
|
|
|
@ -47,7 +47,7 @@ data class RoomThirdPartyInviteContent(
|
|||
/**
|
||||
* Keys with which the token may be signed.
|
||||
*/
|
||||
@Json(name = "public_keys") val publicKeys: List<PublicKeys>? = emptyList()
|
||||
@Json(name = "public_keys") val publicKeys: List<PublicKeys>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.room.model
|
||||
|
||||
object RoomType {
|
||||
|
||||
const val SPACE = "org.matrix.msc1772.space" // "m.space"
|
||||
// const val MESSAGING = "org.matrix.msc1840.messaging"
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.room.model
|
||||
|
||||
data class SpaceChildInfo(
|
||||
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>,
|
||||
val parentRoomId: String?
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* 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.
|
||||
|
@ -14,14 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.grouplist
|
||||
package org.matrix.android.sdk.api.session.room.model
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
|
||||
data class GroupListViewState(
|
||||
val asyncGroups: Async<List<GroupSummary>> = Uninitialized,
|
||||
val selectedGroup: GroupSummary? = null
|
||||
) : MvRxState
|
||||
data class SpaceParentInfo(
|
||||
val parentId: String?,
|
||||
val roomSummary: RoomSummary?,
|
||||
val canonical: Boolean?,
|
||||
val viaServers: List<String>
|
||||
)
|
|
@ -18,13 +18,15 @@ package org.matrix.android.sdk.api.session.room.model.create
|
|||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
|
||||
// TODO Give a way to include other initial states
|
||||
class CreateRoomParams {
|
||||
open class CreateRoomParams {
|
||||
/**
|
||||
* A public visibility indicates that the room will be shown in the published room list.
|
||||
* A private visibility will hide the room from the published room list.
|
||||
|
@ -68,6 +70,11 @@ class CreateRoomParams {
|
|||
*/
|
||||
val invite3pids = mutableListOf<ThreePid>()
|
||||
|
||||
/**
|
||||
* Initial Guest Access
|
||||
*/
|
||||
var guestAccess: GuestAccess? = null
|
||||
|
||||
/**
|
||||
* If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
||||
* the encryption will be enabled on the created room
|
||||
|
@ -111,6 +118,17 @@ class CreateRoomParams {
|
|||
}
|
||||
}
|
||||
|
||||
var roomType: String? = null // RoomType.MESSAGING
|
||||
set(value) {
|
||||
field = value
|
||||
if (value != null) {
|
||||
creationContent[CREATION_CONTENT_KEY_ROOM_TYPE] = value
|
||||
} else {
|
||||
// This is the default value, we remove the field
|
||||
creationContent.remove(CREATION_CONTENT_KEY_ROOM_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The power level content to override in the default power level event
|
||||
*/
|
||||
|
@ -136,7 +154,12 @@ class CreateRoomParams {
|
|||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
}
|
||||
|
||||
var roomVersion: String? = null
|
||||
|
||||
var joinRuleRestricted: List<RoomJoinRulesAllowEntry>? = null
|
||||
|
||||
companion object {
|
||||
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
|
||||
private const val CREATION_CONTENT_KEY_ROOM_TYPE = "org.matrix.msc1772.type"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,5 +26,7 @@ import com.squareup.moshi.JsonClass
|
|||
data class RoomCreateContent(
|
||||
@Json(name = "creator") val creator: String? = null,
|
||||
@Json(name = "room_version") val roomVersion: String? = null,
|
||||
@Json(name = "predecessor") val predecessor: Predecessor? = null
|
||||
@Json(name = "predecessor") val predecessor: Predecessor? = null,
|
||||
// Defines the room type, see #RoomType (user extensible)
|
||||
@Json(name = "org.matrix.msc1772.type") val type: String? = null
|
||||
)
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room.peeking
|
||||
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
sealed class PeekResult {
|
||||
data class Success(
|
||||
val roomId: String,
|
||||
|
@ -24,7 +26,9 @@ sealed class PeekResult {
|
|||
val topic: String?,
|
||||
val avatarUrl: String?,
|
||||
val numJoinedMembers: Int?,
|
||||
val viaServers: List<String>
|
||||
val roomType: String?,
|
||||
val viaServers: List<String>,
|
||||
val someMembers: List<MatrixItem.UserItem>?
|
||||
) : PeekResult()
|
||||
|
||||
data class PeekingNotAllowed(
|
||||
|
@ -34,4 +38,6 @@ sealed class PeekResult {
|
|||
) : PeekResult()
|
||||
|
||||
object UnknownAlias : PeekResult()
|
||||
|
||||
fun isSuccess() = this is Success
|
||||
}
|
||||
|
|
|
@ -18,6 +18,13 @@
|
|||
package org.matrix.android.sdk.api.session.room.powerlevels
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.banOrDefault
|
||||
import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault
|
||||
import org.matrix.android.sdk.api.session.room.model.inviteOrDefault
|
||||
import org.matrix.android.sdk.api.session.room.model.kickOrDefault
|
||||
import org.matrix.android.sdk.api.session.room.model.redactOrDefault
|
||||
import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
|
||||
import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
|
||||
|
||||
/**
|
||||
* This class is an helper around PowerLevelsContent.
|
||||
|
@ -31,9 +38,9 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
* @return the power level
|
||||
*/
|
||||
fun getUserPowerLevelValue(userId: String): Int {
|
||||
return powerLevelsContent.users.getOrElse(userId) {
|
||||
powerLevelsContent.usersDefault
|
||||
}
|
||||
return powerLevelsContent.users
|
||||
?.get(userId)
|
||||
?: powerLevelsContent.usersDefaultOrDefault()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +52,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
fun getUserRole(userId: String): Role {
|
||||
val value = getUserPowerLevelValue(userId)
|
||||
// I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web
|
||||
return Role.fromValue(value, powerLevelsContent.eventsDefault)
|
||||
return Role.fromValue(value, powerLevelsContent.eventsDefaultOrDefault())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,11 +66,11 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean {
|
||||
return if (userId.isNotEmpty()) {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minimumPowerLevel = powerLevelsContent.events[eventType]
|
||||
val minimumPowerLevel = powerLevelsContent.events?.get(eventType)
|
||||
?: if (isState) {
|
||||
powerLevelsContent.stateDefault
|
||||
powerLevelsContent.stateDefaultOrDefault()
|
||||
} else {
|
||||
powerLevelsContent.eventsDefault
|
||||
powerLevelsContent.eventsDefaultOrDefault()
|
||||
}
|
||||
powerLevel >= minimumPowerLevel
|
||||
} else false
|
||||
|
@ -76,7 +83,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
*/
|
||||
fun isUserAbleToInvite(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
return powerLevel >= powerLevelsContent.invite
|
||||
return powerLevel >= powerLevelsContent.inviteOrDefault()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,7 +93,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
*/
|
||||
fun isUserAbleToBan(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
return powerLevel >= powerLevelsContent.ban
|
||||
return powerLevel >= powerLevelsContent.banOrDefault()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -96,7 +103,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
*/
|
||||
fun isUserAbleToKick(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
return powerLevel >= powerLevelsContent.kick
|
||||
return powerLevel >= powerLevelsContent.kickOrDefault()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,6 +113,6 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
*/
|
||||
fun isUserAbleToRedact(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
return powerLevel >= powerLevelsContent.redact
|
||||
return powerLevel >= powerLevelsContent.redactOrDefault()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,6 @@ data class RoomAggregateNotificationCount(
|
|||
val notificationCount: Int,
|
||||
val highlightCount: Int
|
||||
) {
|
||||
val totalCount = notificationCount + highlightCount
|
||||
val totalCount = notificationCount
|
||||
val isHighlight = highlightCount > 0
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomType
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
|
||||
class CreateSpaceParams : CreateRoomParams() {
|
||||
|
||||
init {
|
||||
// Space-rooms are distinguished from regular messaging rooms by the m.room.type of m.space
|
||||
roomType = RoomType.SPACE
|
||||
|
||||
// Space-rooms should be created with a power level for events_default of 100,
|
||||
// to prevent the rooms accidentally/maliciously clogging up with messages from random members of the space.
|
||||
powerLevelContentOverride = PowerLevelsContent(
|
||||
eventsDefault = 100
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.api.session.space
|
||||
|
||||
sealed class JoinSpaceResult {
|
||||
object Success : JoinSpaceResult()
|
||||
data class Fail(val error: Throwable) : JoinSpaceResult()
|
||||
|
||||
/** Success fully joined the space, but failed to join all or some of it's rooms */
|
||||
data class PartialSuccess(val failedRooms: Map<String, Throwable>) : JoinSpaceResult()
|
||||
|
||||
fun isSuccess() = this is Success || this is PartialSuccess
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.Room
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
||||
interface Space {
|
||||
|
||||
fun asRoom(): Room
|
||||
|
||||
val spaceId: String
|
||||
|
||||
suspend fun leave(reason: String? = null)
|
||||
|
||||
/**
|
||||
* A current snapshot of [RoomSummary] associated with the space
|
||||
*/
|
||||
fun spaceSummary(): RoomSummary?
|
||||
|
||||
suspend fun addChildren(roomId: String,
|
||||
viaServers: List<String>,
|
||||
order: String?,
|
||||
autoJoin: Boolean = false,
|
||||
suggested: Boolean? = false)
|
||||
|
||||
suspend fun removeChildren(roomId: String)
|
||||
|
||||
@Throws
|
||||
suspend fun setChildrenOrder(roomId: String, order: String?)
|
||||
|
||||
@Throws
|
||||
suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
|
||||
|
||||
// fun getChildren() : List<IRoomSummary>
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
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.internal.session.space.peeking.SpacePeekResult
|
||||
|
||||
typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
|
||||
|
||||
interface SpaceService {
|
||||
|
||||
/**
|
||||
* Create a space asynchronously
|
||||
* @return the spaceId of the created space
|
||||
*/
|
||||
suspend fun createSpace(params: CreateSpaceParams): String
|
||||
|
||||
/**
|
||||
* Just a shortcut for space creation for ease of use
|
||||
*/
|
||||
suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String
|
||||
|
||||
/**
|
||||
* Get a space from a roomId
|
||||
* @param spaceId the roomId to look for.
|
||||
* @return a space with spaceId or null if room type is not space
|
||||
*/
|
||||
fun getSpace(spaceId: String): Space?
|
||||
|
||||
/**
|
||||
* Try to resolve (peek) rooms and subspace in this space.
|
||||
* Use this call get preview of children of this space, particularly useful to get a
|
||||
* preview of rooms that you did not join yet.
|
||||
*/
|
||||
suspend fun peekSpace(spaceId: String): SpacePeekResult
|
||||
|
||||
/**
|
||||
* Get's information of a space by querying the server
|
||||
*/
|
||||
suspend fun querySpaceChildren(spaceId: String,
|
||||
suggestedOnly: Boolean? = null,
|
||||
autoJoinedOnly: Boolean? = null): Pair<RoomSummary, List<SpaceChildInfo>>
|
||||
|
||||
/**
|
||||
* 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<RoomSummary>>
|
||||
|
||||
fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary>
|
||||
|
||||
suspend fun joinSpace(spaceIdOrAlias: String,
|
||||
reason: String? = null,
|
||||
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>)
|
||||
|
||||
fun getRootSpaceSummaries(): List<RoomSummary>
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* "content": {
|
||||
* "via": ["example.com"],
|
||||
* "order": "abcd",
|
||||
* "default": true
|
||||
* }
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SpaceChildContent(
|
||||
/**
|
||||
* Key which gives a list of candidate servers that can be used to join the room
|
||||
* Children where via is not present are ignored.
|
||||
*/
|
||||
@Json(name = "via") val via: List<String>? = null,
|
||||
/**
|
||||
* The order key is a string which is used to provide a default ordering of siblings in the room list.
|
||||
* (Rooms are sorted based on a lexicographic ordering of order values; rooms with no order come last.
|
||||
* orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~),
|
||||
* or consist of more than 50 characters, are forbidden and should be ignored if received.)
|
||||
*/
|
||||
@Json(name = "order") val order: String? = null,
|
||||
/**
|
||||
* The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should
|
||||
* be automatically joined by members of that space.
|
||||
* (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.)
|
||||
*/
|
||||
@Json(name = "auto_join") val autoJoin: Boolean? = false,
|
||||
|
||||
/**
|
||||
* If `suggested` is set to `true`, that indicates that the child should be advertised to
|
||||
* members of the space by the client. This could be done by showing them eagerly
|
||||
* in the room list. This is should be ignored if `auto_join` is set to `true`.
|
||||
*/
|
||||
@Json(name = "suggested") val suggested: Boolean? = false
|
||||
) {
|
||||
/**
|
||||
* Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~),
|
||||
* or consist of more than 50 characters, are forbidden and should be ignored if received.)
|
||||
*/
|
||||
fun validOrder(): String? {
|
||||
return order
|
||||
?.takeIf { it.length <= 50 }
|
||||
?.takeIf { ORDER_VALID_CHAR_REGEX.matches(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ORDER_VALID_CHAR_REGEX = "[ -~]+".toRegex()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Rooms can claim parents via the m.space.parent state event.
|
||||
* {
|
||||
* "type": "m.space.parent",
|
||||
* "state_key": "!space:example.com",
|
||||
* "content": {
|
||||
* "via": ["example.com"],
|
||||
* "canonical": true,
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SpaceParentContent(
|
||||
/**
|
||||
* Key which gives a list of candidate servers that can be used to join the parent.
|
||||
* Parents where via is not present are ignored.
|
||||
*/
|
||||
@Json(name = "via") val via: List<String>? = null,
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
@Json(name = "canonical") val canonical: Boolean? = false
|
||||
)
|
|
@ -20,6 +20,7 @@ import org.matrix.android.sdk.BuildConfig
|
|||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
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.SpaceChildInfo
|
||||
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.user.model.User
|
||||
|
@ -157,3 +158,5 @@ fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAl
|
|||
fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||
|
||||
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
|
||||
|
||||
fun SpaceChildInfo.toMatrixItem() = MatrixItem.RoomItem(childRoomId, name, avatarUrl)
|
||||
|
|
|
@ -19,7 +19,10 @@ 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.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
|
@ -31,13 +34,16 @@ 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.SpaceChildSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 9L
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 10L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
|
@ -52,6 +58,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
if (oldVersion <= 6) migrateTo7(realm)
|
||||
if (oldVersion <= 7) migrateTo8(realm)
|
||||
if (oldVersion <= 8) migrateTo9(realm)
|
||||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -174,7 +181,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE)
|
||||
|
||||
?.transform { obj ->
|
||||
|
||||
val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
|
||||
it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE
|
||||
}
|
||||
|
@ -194,4 +200,44 @@ 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")!!)
|
||||
?.addRealmListField(SpaceChildSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
|
||||
|
||||
realm.schema.create("SpaceParentSummaryEntity")
|
||||
?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java)
|
||||
?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java)
|
||||
?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true)
|
||||
?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
|
||||
?.addRealmListField(SpaceParentSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
|
||||
|
||||
val creationContentAdapter = MoshiProvider.providesMoshi().adapter(RoomCreateContent::class.java)
|
||||
realm.schema.get("RoomSummaryEntity")
|
||||
?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
|
||||
?.addField(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, String::class.java)
|
||||
?.addField(RoomSummaryEntityFields.GROUP_IDS, String::class.java)
|
||||
?.transform { obj ->
|
||||
|
||||
val creationEvent = realm.where("CurrentStateEventEntity")
|
||||
.equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
|
||||
.equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_CREATE)
|
||||
.findFirst()
|
||||
|
||||
val roomType = creationEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
|
||||
?.getString(EventEntityFields.CONTENT)?.let {
|
||||
creationContentAdapter.fromJson(it)?.type
|
||||
}
|
||||
|
||||
obj.setString(RoomSummaryEntityFields.ROOM_TYPE, roomType)
|
||||
}
|
||||
?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
|
||||
?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,32 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
|
|||
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
|
||||
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
|
||||
inviterId = roomSummaryEntity.inviterId,
|
||||
hasFailedSending = roomSummaryEntity.hasFailedSending
|
||||
hasFailedSending = roomSummaryEntity.hasFailedSending,
|
||||
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()
|
||||
)
|
||||
},
|
||||
spaceChildren = 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(),
|
||||
parentRoomId = roomSummaryEntity.roomId
|
||||
)
|
||||
},
|
||||
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
|||
set(value) {
|
||||
membersLoadStatusStr = value.name
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
|
@ -27,7 +27,10 @@ import org.matrix.android.sdk.api.session.room.model.VersioningState
|
|||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||
|
||||
internal open class RoomSummaryEntity(
|
||||
@PrimaryKey var roomId: String = ""
|
||||
@PrimaryKey var roomId: String = "",
|
||||
var roomType: String? = null,
|
||||
var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
|
||||
var children: RealmList<SpaceChildSummaryEntity> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
var displayName: String? = ""
|
||||
|
@ -204,6 +207,16 @@ internal open class RoomSummaryEntity(
|
|||
if (value != field) field = value
|
||||
}
|
||||
|
||||
var flattenParentIds: String? = null
|
||||
set(value) {
|
||||
if (value != field) field = value
|
||||
}
|
||||
|
||||
var groupIds: String? = null
|
||||
set(value) {
|
||||
if (value != field) field = value
|
||||
}
|
||||
|
||||
@Index
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
|
||||
|
@ -244,6 +257,5 @@ internal open class RoomSummaryEntity(
|
|||
roomEncryptionTrustLevelStr = value?.name
|
||||
}
|
||||
}
|
||||
|
||||
companion object
|
||||
}
|
||||
|
|
|
@ -61,6 +61,8 @@ import io.realm.annotations.RealmModule
|
|||
CurrentStateEventEntity::class,
|
||||
UserAccountDataEntity::class,
|
||||
ScalarTokenEntity::class,
|
||||
WellknownIntegrationManagerConfigEntity::class
|
||||
WellknownIntegrationManagerConfigEntity::class,
|
||||
SpaceChildSummaryEntity::class,
|
||||
SpaceParentSummaryEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 SpaceChildSummaryEntity(
|
||||
// var isSpace: Boolean = false,
|
||||
|
||||
var order: String? = null,
|
||||
|
||||
var autoJoin: Boolean? = 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
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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()
|
||||
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.query
|
||||
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.Sort
|
||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
|
||||
internal fun RealmQuery<RoomSummaryEntity>.process(sortOrder: RoomSortOrder): RealmQuery<RoomSummaryEntity> {
|
||||
when (sortOrder) {
|
||||
RoomSortOrder.NAME -> {
|
||||
sort(RoomSummaryEntityFields.DISPLAY_NAME, Sort.ASCENDING)
|
||||
}
|
||||
RoomSortOrder.ACTIVITY -> {
|
||||
sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
|
||||
}
|
||||
RoomSortOrder.NONE -> {
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
|
@ -16,10 +16,10 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.query
|
||||
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import io.realm.Case
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmQuery
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import timber.log.Timber
|
||||
|
||||
fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.search.SearchService
|
|||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||
import org.matrix.android.sdk.api.session.signout.SignOutService
|
||||
import org.matrix.android.sdk.api.session.space.SpaceService
|
||||
import org.matrix.android.sdk.api.session.sync.FilterService
|
||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
|
||||
|
@ -120,6 +121,7 @@ internal class DefaultSession @Inject constructor(
|
|||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||
private val callSignalingService: Lazy<CallSignalingService>,
|
||||
private val spaceService: Lazy<SpaceService>,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
|
||||
) : Session,
|
||||
|
@ -265,6 +267,8 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get()
|
||||
|
||||
override fun spaceService(): SpaceService = spaceService.get()
|
||||
|
||||
override fun getOkHttpClient(): OkHttpClient {
|
||||
return unauthenticatedWithCertificateOkHttpClient.get()
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
|
|||
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
|
||||
import org.matrix.android.sdk.internal.session.search.SearchModule
|
||||
import org.matrix.android.sdk.internal.session.signout.SignOutModule
|
||||
import org.matrix.android.sdk.internal.session.space.SpaceModule
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncModule
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTask
|
||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
||||
|
@ -91,7 +92,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
|||
FederationModule::class,
|
||||
CallModule::class,
|
||||
SearchModule::class,
|
||||
ThirdPartyModule::class
|
||||
ThirdPartyModule::class,
|
||||
SpaceModule::class
|
||||
]
|
||||
)
|
||||
@SessionScope
|
||||
|
|
|
@ -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,13 @@ 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.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.util.awaitCallback
|
||||
import java.security.InvalidParameterException
|
||||
import javax.inject.Inject
|
||||
|
@ -148,4 +151,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,11 @@ import com.zhuinden.monarchy.Monarchy
|
|||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||
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.UpdatableLivePageResult
|
||||
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
|
||||
|
@ -67,7 +69,7 @@ internal class DefaultRoomService @Inject constructor(
|
|||
) : RoomService {
|
||||
|
||||
override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
|
||||
return createRoomTask.execute(createRoomParams)
|
||||
return createRoomTask.executeRetry(createRoomParams, 3)
|
||||
}
|
||||
|
||||
override fun getRoom(roomId: String): Room? {
|
||||
|
@ -90,14 +92,14 @@ internal class DefaultRoomService @Inject constructor(
|
|||
return roomSummaryDataSource.getRoomSummariesLive(queryParams)
|
||||
}
|
||||
|
||||
override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config)
|
||||
override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder)
|
||||
: LiveData<PagedList<RoomSummary>> {
|
||||
return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig)
|
||||
return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||
}
|
||||
|
||||
override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config)
|
||||
: UpdatableFilterLivePageResult {
|
||||
return roomSummaryDataSource.getFilteredPagedRoomSummariesLive(queryParams, pagedListConfig)
|
||||
override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder)
|
||||
: UpdatableLivePageResult {
|
||||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||
}
|
||||
|
||||
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||
|
@ -163,4 +165,18 @@ internal class DefaultRoomService @Inject constructor(
|
|||
override suspend fun peekRoom(roomIdOrAlias: String): PeekResult {
|
||||
return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias))
|
||||
}
|
||||
|
||||
override fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List<Membership>): List<RoomSummary> {
|
||||
if (spaceId == null) {
|
||||
return roomSummaryDataSource.getFlattenOrphanRooms()
|
||||
}
|
||||
return roomSummaryDataSource.getAllRoomSummaryChildOf(spaceId, memberships)
|
||||
}
|
||||
|
||||
override fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?, memberships: List<Membership>): LiveData<List<RoomSummary>> {
|
||||
if (spaceId == null) {
|
||||
return roomSummaryDataSource.getFlattenOrphanRoomsLive()
|
||||
}
|
||||
return roomSummaryDataSource.getAllRoomSummaryChildOfLive(spaceId, memberships)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.commonmark.renderer.html.HtmlRenderer
|
|||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
import org.matrix.android.sdk.api.session.space.SpaceService
|
||||
import org.matrix.android.sdk.internal.session.DefaultFileService
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
|
||||
|
@ -89,6 +90,7 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
|
|||
import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
|
||||
import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
|
||||
import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
|
||||
import org.matrix.android.sdk.internal.session.space.DefaultSpaceService
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
|
@ -135,6 +137,9 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindRoomService(service: DefaultRoomService): RoomService
|
||||
|
||||
@Binds
|
||||
abstract fun bindSpaceService(service: DefaultSpaceService): SpaceService
|
||||
|
||||
@Binds
|
||||
abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import org.matrix.android.sdk.api.session.space.Space
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SpaceGetter {
|
||||
fun get(spaceId: String): Space?
|
||||
}
|
||||
|
||||
internal class DefaultSpaceGetter @Inject constructor(
|
||||
private val roomGetter: RoomGetter
|
||||
) : SpaceGetter {
|
||||
|
||||
override fun get(spaceId: String): Space? {
|
||||
return roomGetter.getRoom(spaceId)?.asSpace()
|
||||
}
|
||||
}
|
|
@ -36,7 +36,11 @@ internal class RoomAliasAvailabilityChecker @Inject constructor(
|
|||
@Throws(RoomAliasError::class)
|
||||
suspend fun check(aliasLocalPart: String?) {
|
||||
if (aliasLocalPart.isNullOrEmpty()) {
|
||||
throw RoomAliasError.AliasEmpty
|
||||
// don't check empty or not provided alias
|
||||
return
|
||||
}
|
||||
if (aliasLocalPart.isBlank()) {
|
||||
throw RoomAliasError.AliasIsBlank
|
||||
}
|
||||
// Check alias availability
|
||||
val fullAlias = aliasLocalPart.toFullLocalAlias(userId)
|
||||
|
|
|
@ -111,5 +111,12 @@ internal data class CreateRoomBody(
|
|||
* The power level content to override in the default power level event
|
||||
*/
|
||||
@Json(name = "power_level_content_override")
|
||||
val powerLevelContentOverride: PowerLevelsContent?
|
||||
val powerLevelContentOverride: PowerLevelsContent?,
|
||||
|
||||
/**
|
||||
* The room version to set for the room. If not provided, the homeserver is to use its configured default.
|
||||
* If provided, the homeserver will return a 400 error with the errcode M_UNSUPPORTED_ROOM_VERSION if it does not support the room version.
|
||||
*/
|
||||
@Json(name = "room_version")
|
||||
val roomVersion: String?
|
||||
)
|
||||
|
|
|
@ -20,8 +20,13 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
|
|||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
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.identity.IdentityServiceError
|
||||
import org.matrix.android.sdk.api.session.identity.toMedium
|
||||
import org.matrix.android.sdk.api.session.room.model.GuestAccess
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
|
@ -71,10 +76,17 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
if (params.joinRuleRestricted != null) {
|
||||
params.roomVersion = "org.matrix.msc3083"
|
||||
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
|
||||
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
|
||||
}
|
||||
val initialStates = listOfNotNull(
|
||||
buildEncryptionWithAlgorithmEvent(params),
|
||||
buildHistoryVisibilityEvent(params),
|
||||
buildAvatarEvent(params)
|
||||
buildAvatarEvent(params),
|
||||
buildGuestAccess(params),
|
||||
buildJoinRulesRestricted(params)
|
||||
)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
@ -89,7 +101,9 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
initialStates = initialStates,
|
||||
preset = params.preset,
|
||||
isDirect = params.isDirect,
|
||||
powerLevelContentOverride = params.powerLevelContentOverride
|
||||
powerLevelContentOverride = params.powerLevelContentOverride,
|
||||
roomVersion = params.roomVersion
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -123,6 +137,31 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun buildGuestAccess(params: CreateRoomParams): Event? {
|
||||
return params.guestAccess
|
||||
?.let {
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
stateKey = "",
|
||||
content = mapOf("guest_access" to it.value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? {
|
||||
return params.joinRuleRestricted
|
||||
?.let { allowList ->
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_JOIN_RULES,
|
||||
stateKey = "",
|
||||
content = RoomJoinRulesContent(
|
||||
_joinRules = RoomJoinRules.RESTRICTED.value,
|
||||
allowList = allowList
|
||||
).toContent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the crypto algorithm to the room creation parameters.
|
||||
*/
|
||||
|
|
|
@ -102,7 +102,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
|
||||
}
|
||||
} catch (exception: TimeoutCancellationException) {
|
||||
throw CreateRoomFailure.CreatedWithTimeout
|
||||
throw CreateRoomFailure.CreatedWithTimeout(roomId)
|
||||
}
|
||||
|
||||
Realm.getInstance(realmConfiguration).executeTransactionAsync {
|
||||
|
|
|
@ -23,11 +23,14 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter
|
||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
||||
|
@ -100,7 +103,9 @@ internal class DefaultPeekRoomTask @Inject constructor(
|
|||
name = publicRepoResult.name,
|
||||
topic = publicRepoResult.topic,
|
||||
numJoinedMembers = publicRepoResult.numJoinedMembers,
|
||||
viaServers = serverList
|
||||
viaServers = serverList,
|
||||
roomType = null, // would be nice to get that from directory...
|
||||
someMembers = null
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -125,11 +130,25 @@ internal class DefaultPeekRoomTask @Inject constructor(
|
|||
?.let { it.content?.toModel<RoomCanonicalAliasContent>()?.canonicalAlias }
|
||||
|
||||
// not sure if it's the right way to do that :/
|
||||
val memberCount = stateEvents
|
||||
val membersEvent = stateEvents
|
||||
.filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true }
|
||||
|
||||
val memberCount = membersEvent
|
||||
.distinctBy { it.stateKey }
|
||||
.count()
|
||||
|
||||
val someMembers = membersEvent.mapNotNull { ev ->
|
||||
ev.content?.toModel<RoomMemberContent>()?.let {
|
||||
MatrixItem.UserItem(ev.stateKey ?: "", it.displayName, it.avatarUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val roomType = stateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE }
|
||||
?.content
|
||||
?.toModel<RoomCreateContent>()
|
||||
?.type
|
||||
|
||||
return PeekResult.Success(
|
||||
roomId = roomId,
|
||||
alias = alias,
|
||||
|
@ -137,7 +156,9 @@ internal class DefaultPeekRoomTask @Inject constructor(
|
|||
name = name,
|
||||
topic = topic,
|
||||
numJoinedMembers = memberCount,
|
||||
viaServers = serverList
|
||||
roomType = roomType,
|
||||
viaServers = serverList,
|
||||
someMembers = someMembers
|
||||
)
|
||||
} catch (failure: Throwable) {
|
||||
// Would be M_FORBIDDEN if cannot peek :/
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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.room.relationship
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* Relationship between rooms and spaces
|
||||
* The intention is that rooms and spaces form a hierarchy, which clients can use to structure the user's room list into a tree view.
|
||||
* The parent/child relationship can be expressed in one of two ways:
|
||||
* - The admins of a space can advertise rooms and subspaces for their space by setting m.space.child state events.
|
||||
* The state_key is the ID of a child room or space, and the content should contain a via key which gives
|
||||
* a list of candidate servers that can be used to join the room. present: true key is included to distinguish from a deleted state event.
|
||||
*
|
||||
* - Separately, rooms can claim parents via the m.room.parent state event.
|
||||
*/
|
||||
internal class RoomChildRelationInfo(
|
||||
private val realm: Realm,
|
||||
private val roomId: String
|
||||
) {
|
||||
|
||||
data class SpaceChildInfo(
|
||||
val roomId: String,
|
||||
val order: String?,
|
||||
val autoJoin: Boolean,
|
||||
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()
|
||||
// .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.v("## Space child desc state event $scc")
|
||||
// Children where via is not present are ignored.
|
||||
scc.via?.let { via ->
|
||||
SpaceChildInfo(
|
||||
roomId = it.stateKey,
|
||||
order = scc.validOrder(),
|
||||
autoJoin = scc.autoJoin ?: false,
|
||||
viaServers = via
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.util.JsonDict
|
|||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.session.content.FileUploader
|
||||
import java.lang.UnsupportedOperationException
|
||||
|
||||
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
|
@ -73,7 +74,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||
eventType = eventType,
|
||||
body = body.toSafeJson(eventType)
|
||||
)
|
||||
sendStateTask.execute(params)
|
||||
sendStateTask.executeRetry(params, 3)
|
||||
}
|
||||
|
||||
private fun JsonDict.toSafeJson(eventType: String): JsonDict {
|
||||
|
@ -127,6 +128,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||
|
||||
override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) {
|
||||
if (joinRules != null) {
|
||||
if (joinRules == RoomJoinRules.RESTRICTED) throw UnsupportedOperationException("No yet supported")
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_JOIN_RULES,
|
||||
body = mapOf("join_rule" to joinRules),
|
||||
|
|
|
@ -21,22 +21,21 @@ import com.squareup.moshi.JsonClass
|
|||
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.PowerLevelsContent
|
||||
import org.matrix.android.sdk.api.session.room.powerlevels.Role
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SerializablePowerLevelsContent(
|
||||
@Json(name = "ban") val ban: Int = Role.Moderator.value,
|
||||
@Json(name = "kick") val kick: Int = Role.Moderator.value,
|
||||
@Json(name = "invite") val invite: Int = Role.Moderator.value,
|
||||
@Json(name = "redact") val redact: Int = Role.Moderator.value,
|
||||
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
|
||||
@Json(name = "events") val events: Map<String, Int> = emptyMap(),
|
||||
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
|
||||
@Json(name = "users") val users: Map<String, Int> = emptyMap(),
|
||||
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
|
||||
@Json(name = "ban") val ban: Int?,
|
||||
@Json(name = "kick") val kick: Int?,
|
||||
@Json(name = "invite") val invite: Int?,
|
||||
@Json(name = "redact") val redact: Int?,
|
||||
@Json(name = "events_default") val eventsDefault: Int?,
|
||||
@Json(name = "events") val events: Map<String, Int>?,
|
||||
@Json(name = "users_default") val usersDefault: Int?,
|
||||
@Json(name = "users") val users: Map<String, Int>?,
|
||||
@Json(name = "state_default") val stateDefault: Int?,
|
||||
// `Int` is the diff here (instead of `Any`)
|
||||
@Json(name = "notifications") val notifications: Map<String, Int> = emptyMap()
|
||||
@Json(name = "notifications") val notifications: Map<String, Int>?
|
||||
)
|
||||
|
||||
internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
|
||||
|
@ -52,7 +51,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
|
|||
usersDefault = content.usersDefault,
|
||||
users = content.users,
|
||||
stateDefault = content.stateDefault,
|
||||
notifications = content.notifications.mapValues { content.notificationLevel(it.key) }
|
||||
notifications = content.notifications?.mapValues { content.notificationLevel(it.key) }
|
||||
)
|
||||
}
|
||||
?.toContent()
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright (c) 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 java.util.LinkedList
|
||||
|
||||
data class GraphNode(
|
||||
val name: String
|
||||
)
|
||||
|
||||
data class GraphEdge(
|
||||
val source: GraphNode,
|
||||
val destination: GraphNode
|
||||
)
|
||||
|
||||
class Graph {
|
||||
|
||||
private val adjacencyList: HashMap<GraphNode, ArrayList<GraphEdge>> = HashMap()
|
||||
|
||||
fun getOrCreateNode(name: String): GraphNode {
|
||||
return adjacencyList.entries.firstOrNull { it.key.name == name }?.key
|
||||
?: GraphNode(name).also {
|
||||
adjacencyList[it] = ArrayList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addEdge(sourceName: String, destinationName: String) {
|
||||
val source = getOrCreateNode(sourceName)
|
||||
val destination = getOrCreateNode(destinationName)
|
||||
adjacencyList.getOrPut(source) { ArrayList() }.add(
|
||||
GraphEdge(source, destination)
|
||||
)
|
||||
}
|
||||
|
||||
fun addEdge(source: GraphNode, destination: GraphNode) {
|
||||
adjacencyList.getOrPut(source) { ArrayList() }.add(
|
||||
GraphEdge(source, destination)
|
||||
)
|
||||
}
|
||||
|
||||
fun edgesOf(node: GraphNode): List<GraphEdge> {
|
||||
return adjacencyList[node]?.toList() ?: emptyList()
|
||||
}
|
||||
|
||||
fun withoutEdges(edgesToPrune: List<GraphEdge>): Graph {
|
||||
val output = Graph()
|
||||
this.adjacencyList.forEach { (vertex, edges) ->
|
||||
output.getOrCreateNode(vertex.name)
|
||||
edges.forEach {
|
||||
if (!edgesToPrune.contains(it)) {
|
||||
// add it
|
||||
output.addEdge(it.source, it.destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* Depending on the chosen starting point the background edge might change
|
||||
*/
|
||||
fun findBackwardEdges(startFrom: GraphNode? = null): List<GraphEdge> {
|
||||
val backwardEdges = mutableSetOf<GraphEdge>()
|
||||
val visited = mutableMapOf<GraphNode, Int>()
|
||||
val notVisited = -1
|
||||
val inPath = 0
|
||||
val completed = 1
|
||||
adjacencyList.keys.forEach {
|
||||
visited[it] = notVisited
|
||||
}
|
||||
val stack = LinkedList<GraphNode>()
|
||||
|
||||
(startFrom ?: adjacencyList.entries.firstOrNull { visited[it.key] == notVisited }?.key)
|
||||
?.let {
|
||||
stack.push(it)
|
||||
visited[it] = inPath
|
||||
}
|
||||
|
||||
while (stack.isNotEmpty()) {
|
||||
// Timber.w("VAL: current stack: ${stack.reversed().joinToString { it.name }}")
|
||||
val vertex = stack.peek() ?: break
|
||||
// peek a path to follow
|
||||
var destination: GraphNode? = null
|
||||
edgesOf(vertex).forEach {
|
||||
when (visited[it.destination]) {
|
||||
notVisited -> {
|
||||
// it's a candidate
|
||||
destination = it.destination
|
||||
}
|
||||
inPath -> {
|
||||
// Cycle!!
|
||||
backwardEdges.add(it)
|
||||
}
|
||||
completed -> {
|
||||
// dead end
|
||||
}
|
||||
}
|
||||
}
|
||||
if (destination == null) {
|
||||
// dead end, pop
|
||||
stack.pop().let {
|
||||
visited[it] = completed
|
||||
}
|
||||
} else {
|
||||
// go down this path
|
||||
stack.push(destination)
|
||||
visited[destination!!] = inPath
|
||||
}
|
||||
|
||||
if (stack.isEmpty()) {
|
||||
// try to get another graph of forest?
|
||||
adjacencyList.entries.firstOrNull { visited[it.key] == notVisited }?.key?.let {
|
||||
stack.push(it)
|
||||
visited[it] = inPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backwardEdges.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Only call that on acyclic graph!
|
||||
*/
|
||||
fun flattenDestination(): Map<GraphNode, Set<GraphNode>> {
|
||||
val result = HashMap<GraphNode, Set<GraphNode>>()
|
||||
adjacencyList.keys.forEach { vertex ->
|
||||
result[vertex] = flattenOf(vertex)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun flattenOf(node: GraphNode): Set<GraphNode> {
|
||||
val result = mutableSetOf<GraphNode>()
|
||||
val edgesOf = edgesOf(node)
|
||||
result.addAll(edgesOf.map { it.destination })
|
||||
edgesOf.forEach {
|
||||
result.addAll(flattenOf(it.destination))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return buildString {
|
||||
adjacencyList.forEach { (node, edges) ->
|
||||
append("${node.name} : [")
|
||||
append(edges.joinToString(" ") { it.destination.name })
|
||||
append("]\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(): LiveData<List<String>> = 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 })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
@ -24,12 +25,20 @@ import com.zhuinden.monarchy.Monarchy
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||
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.UpdatableLivePageResult
|
||||
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.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
|
||||
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
|
||||
|
@ -79,11 +88,60 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
|
|||
|
||||
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ roomSummariesQuery(it, queryParams) },
|
||||
{
|
||||
roomSummariesQuery(it, queryParams)
|
||||
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
|
||||
},
|
||||
{ roomSummaryMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
|
||||
return getRoomSummariesLive(queryParams)
|
||||
}
|
||||
|
||||
fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? {
|
||||
return getRoomSummary(roomIdOrAlias)
|
||||
?.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 getRootSpaceSummaries(): List<RoomSummary> {
|
||||
return getRoomSummaries(spaceSummaryQueryParams {
|
||||
memberships = listOf(Membership.JOIN)
|
||||
})
|
||||
.let { allJoinedSpace ->
|
||||
val allFlattenChildren = arrayListOf<RoomSummary>()
|
||||
allJoinedSpace.forEach {
|
||||
flattenSubSpace(it, emptyList(), allFlattenChildren, listOf(Membership.JOIN), false)
|
||||
}
|
||||
val knownNonOrphan = allFlattenChildren.map { it.roomId }.distinct()
|
||||
// keep only root rooms
|
||||
allJoinedSpace.filter { candidate ->
|
||||
!knownNonOrphan.contains(candidate.roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ breadcrumbsQuery(it, queryParams) },
|
||||
|
@ -105,10 +163,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
|
|||
}
|
||||
|
||||
fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config): LiveData<PagedList<RoomSummary>> {
|
||||
pagedListConfig: PagedList.Config,
|
||||
sortOrder: RoomSortOrder): LiveData<PagedList<RoomSummary>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
roomSummariesQuery(realm, queryParams)
|
||||
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
|
||||
roomSummariesQuery(realm, queryParams).process(sortOrder)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
roomSummaryMapper.map(it)
|
||||
|
@ -119,11 +177,11 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
|
|||
)
|
||||
}
|
||||
|
||||
fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config): UpdatableFilterLivePageResult {
|
||||
fun getUpdatablePagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
|
||||
pagedListConfig: PagedList.Config,
|
||||
sortOrder: RoomSortOrder): UpdatableLivePageResult {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
roomSummariesQuery(realm, queryParams)
|
||||
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
|
||||
roomSummariesQuery(realm, queryParams).process(sortOrder)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
roomSummaryMapper.map(it)
|
||||
|
@ -134,13 +192,12 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
|
|||
LivePagedListBuilder(dataSourceFactory, pagedListConfig)
|
||||
)
|
||||
|
||||
return object : UpdatableFilterLivePageResult {
|
||||
return object : UpdatableLivePageResult {
|
||||
override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
|
||||
|
||||
override fun updateQuery(queryParams: RoomSummaryQueryParams) {
|
||||
override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
|
||||
realmDataSourceFactory.updateQuery {
|
||||
roomSummariesQuery(it, queryParams)
|
||||
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
|
||||
roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +246,160 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
|
|||
query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, sn)
|
||||
}
|
||||
}
|
||||
|
||||
queryParams.excludeType?.forEach {
|
||||
query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it)
|
||||
}
|
||||
queryParams.includeType?.forEach {
|
||||
query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it)
|
||||
}
|
||||
when (queryParams.roomCategoryFilter) {
|
||||
RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
|
||||
RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
|
||||
RoomCategoryFilter.ALL -> Unit // nop
|
||||
}
|
||||
|
||||
// Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
|
||||
when (queryParams.activeSpaceId) {
|
||||
is ActiveSpaceFilter.ActiveSpace -> {
|
||||
// It's annoying but for now realm java does not support querying in primitive list :/
|
||||
// https://github.com/realm/realm-java/issues/5361
|
||||
if (queryParams.activeSpaceId.currentSpaceId == null) {
|
||||
// orphan rooms
|
||||
query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
|
||||
} else {
|
||||
query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceId.currentSpaceId)
|
||||
}
|
||||
}
|
||||
is ActiveSpaceFilter.ExcludeSpace -> {
|
||||
query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceId.spaceId)
|
||||
}
|
||||
else -> {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParams.activeGroupId != null) {
|
||||
query.contains(RoomSummaryEntityFields.GROUP_IDS, queryParams.activeGroupId!!)
|
||||
}
|
||||
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 those?
|
||||
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 { 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.spaceChildren?.any { it.childRoomId == roomSummary.roomId } == true) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun flattenChild(current: RoomSummary, parenting: List<String>, output: MutableList<RoomSummary>, memberShips: List<Membership>) {
|
||||
current.spaceChildren?.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>,
|
||||
includeCurrent: Boolean = true) {
|
||||
if (includeCurrent) {
|
||||
output.add(current)
|
||||
}
|
||||
current.spaceChildren?.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.session.room.summary
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -25,6 +26,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomType
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
|
@ -34,29 +37,40 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain
|
|||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||
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.GroupSummaryEntity
|
||||
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.RoomSummaryEntityFields
|
||||
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.query.process
|
||||
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.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
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
internal class RoomSummaryUpdater @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
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,
|
||||
|
@ -89,6 +103,11 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
|
||||
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
|
||||
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
|
||||
val roomCreateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CREATE, stateKey = "")?.root
|
||||
|
||||
val roomType = ContentMapper.map(roomCreateEvent?.content).toModel<RoomCreateContent>()?.type
|
||||
roomSummaryEntity.roomType = roomType
|
||||
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
|
||||
|
||||
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
|
||||
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||
|
@ -163,4 +182,233 @@ 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) {
|
||||
measureTimeMillis {
|
||||
val lookupMap = realm.where(RoomSummaryEntity::class.java)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
|
||||
// we order by roomID to be consistent when breaking parent/child cycles
|
||||
.sort(RoomSummaryEntityFields.ROOM_ID)
|
||||
.findAll().map {
|
||||
it.flattenParentIds = null
|
||||
it to emptyList<RoomSummaryEntity>().toMutableSet()
|
||||
}
|
||||
.toMap()
|
||||
|
||||
lookupMap.keys.forEach { lookedUp ->
|
||||
if (lookedUp.roomType == RoomType.SPACE) {
|
||||
// get childrens
|
||||
|
||||
lookedUp.children.clearWith { it.deleteFromRealm() }
|
||||
|
||||
RoomChildRelationInfo(realm, lookedUp.roomId).getDirectChildrenDescriptions().forEach { child ->
|
||||
|
||||
lookedUp.children.add(
|
||||
realm.createObject<SpaceChildSummaryEntity>().apply {
|
||||
this.childRoomId = child.roomId
|
||||
this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst()
|
||||
this.order = child.order
|
||||
this.autoJoin = child.autoJoin
|
||||
this.viaServers.addAll(child.viaServers)
|
||||
}
|
||||
)
|
||||
|
||||
RoomSummaryEntity.where(realm, child.roomId)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.findFirst()
|
||||
?.let { childSum ->
|
||||
lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry ->
|
||||
if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) {
|
||||
// add looked up as a parent
|
||||
entry.value.add(childSum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lookedUp.parents.clearWith { it.deleteFromRealm() }
|
||||
// can we check parent relations here??
|
||||
RoomChildRelationInfo(realm, lookedUp.roomId).getParentDescriptions()
|
||||
.map { parentInfo ->
|
||||
|
||||
lookedUp.parents.add(
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
RoomSummaryEntity.where(realm, parentInfo.roomId)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.findFirst()
|
||||
?.let { parentSum ->
|
||||
if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) {
|
||||
// add lookedup as a parent
|
||||
lookupMap[parentSum]?.add(lookedUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simple algorithm to break cycles
|
||||
// Need more work to decide how to break, probably need to be as consistent as possible
|
||||
// and also find best way to root the tree
|
||||
|
||||
val graph = Graph()
|
||||
lookupMap
|
||||
// focus only on joined spaces, as room are just leaf
|
||||
.filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
|
||||
.forEach { (sum, children) ->
|
||||
graph.getOrCreateNode(sum.roomId)
|
||||
children.forEach {
|
||||
graph.addEdge(it.roomId, sum.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
val backEdges = graph.findBackwardEdges()
|
||||
Timber.v("## SPACES: Cycle detected = ${backEdges.isNotEmpty()}")
|
||||
|
||||
// break cycles
|
||||
backEdges.forEach { edge ->
|
||||
lookupMap.entries.find { it.key.roomId == edge.source.name }?.let {
|
||||
it.value.removeAll { it.roomId == edge.destination.name }
|
||||
}
|
||||
}
|
||||
|
||||
val acyclicGraph = graph.withoutEdges(backEdges)
|
||||
// Timber.v("## SPACES: acyclicGraph $acyclicGraph")
|
||||
val flattenSpaceParents = acyclicGraph.flattenDestination().map {
|
||||
it.key.name to it.value.map { it.name }
|
||||
}.toMap()
|
||||
// Timber.v("## SPACES: flattenSpaceParents ${flattenSpaceParents.map { it.key.name to it.value.map { it.name } }.joinToString("\n") {
|
||||
// it.first + ": [" + it.second.joinToString(",") + "]"
|
||||
// }}")
|
||||
|
||||
// Timber.v("## SPACES: lookup map ${lookupMap.map { it.key.name to it.value.map { it.name } }.toMap()}")
|
||||
|
||||
lookupMap.entries
|
||||
.filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
|
||||
.forEach { entry ->
|
||||
val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst()
|
||||
if (parent != null) {
|
||||
// Timber.v("## SPACES: check hierarchy of ${parent.name} id ${parent.roomId}")
|
||||
// Timber.v("## SPACES: flat known parents of ${parent.name} are ${flattenSpaceParents[parent.roomId]}")
|
||||
val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId)
|
||||
// Timber.v("## SPACES: flatten known parents of children of ${parent.name} are ${flattenParentsIds}")
|
||||
entry.value.forEach { child ->
|
||||
RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum ->
|
||||
|
||||
// Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}")
|
||||
// var allParents = childSum.flattenParentIds ?: ""
|
||||
if (childSum.flattenParentIds == null) childSum.flattenParentIds = ""
|
||||
flattenParentsIds.forEach {
|
||||
if (childSum.flattenParentIds?.contains(it) != true) {
|
||||
childSum.flattenParentIds += "|$it"
|
||||
}
|
||||
}
|
||||
// childSum.flattenParentIds = "$allParents|"
|
||||
|
||||
// Timber.v("## SPACES: flatten of ${childSum.name} is ${childSum.flattenParentIds}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we need also to filter DMs...
|
||||
// it's more annoying as based on if the other members belong the space or not
|
||||
RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.findAll()
|
||||
.forEach { dmRoom ->
|
||||
val relatedSpaces = lookupMap.keys
|
||||
.filter { it.roomType == RoomType.SPACE }
|
||||
.filter {
|
||||
dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toList()).isNotEmpty()
|
||||
}
|
||||
.map { it.roomId }
|
||||
.distinct()
|
||||
val flattenRelated = mutableListOf<String>().apply {
|
||||
addAll(relatedSpaces)
|
||||
relatedSpaces.map { flattenSpaceParents[it] }.forEach {
|
||||
if (it != null) addAll(it)
|
||||
}
|
||||
}.distinct()
|
||||
if (flattenRelated.isEmpty()) {
|
||||
dmRoom.flattenParentIds = null
|
||||
} else {
|
||||
dmRoom.flattenParentIds = "|${flattenRelated.joinToString("|")}|"
|
||||
}
|
||||
// Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}")
|
||||
}
|
||||
|
||||
// Maybe a good place to count the number of notifications for spaces?
|
||||
|
||||
realm.where(RoomSummaryEntity::class.java)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
|
||||
.findAll().forEach { space ->
|
||||
// get all children
|
||||
var highlightCount = 0
|
||||
var notificationCount = 0
|
||||
realm.where(RoomSummaryEntity::class.java)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
|
||||
.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
|
||||
.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
|
||||
.findAll().forEach {
|
||||
highlightCount += it.highlightCount
|
||||
notificationCount += it.notificationCount
|
||||
}
|
||||
|
||||
space.highlightCount = highlightCount
|
||||
space.notificationCount = notificationCount
|
||||
}
|
||||
// xxx invites??
|
||||
|
||||
// LEGACY GROUPS
|
||||
// lets mark rooms that belongs to groups
|
||||
val existingGroups = GroupSummaryEntity.where(realm).findAll()
|
||||
|
||||
// For rooms
|
||||
realm.where(RoomSummaryEntity::class.java)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
|
||||
.findAll().forEach { room ->
|
||||
val belongsTo = existingGroups.filter { it.roomIds.contains(room.roomId) }
|
||||
room.groupIds = if (belongsTo.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
"|${belongsTo.joinToString("|")}|"
|
||||
}
|
||||
}
|
||||
|
||||
// For DMS
|
||||
realm.where(RoomSummaryEntity::class.java)
|
||||
.process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
.findAll().forEach { room ->
|
||||
val belongsTo = existingGroups.filter {
|
||||
it.userIds.intersect(room.otherMemberIds).isNotEmpty()
|
||||
}
|
||||
room.groupIds = if (belongsTo.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
"|${belongsTo.joinToString("|")}|"
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
Timber.v("## SPACES: Finish checking room hierarchy in $it ms")
|
||||
}
|
||||
}
|
||||
|
||||
// private fun isValidCanonical() : Boolean {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
|
||||
if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) {
|
||||
handleReachEnd(realm, roomId, direction, currentChunk)
|
||||
} else {
|
||||
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 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.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.model.SpaceChildContent
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||
|
||||
internal class DefaultSpace(
|
||||
private val room: Room,
|
||||
private val spaceSummaryDataSource: RoomSummaryDataSource
|
||||
) : Space {
|
||||
|
||||
override fun asRoom(): Room {
|
||||
return room
|
||||
}
|
||||
|
||||
override val spaceId = room.roomId
|
||||
|
||||
override suspend fun leave(reason: String?) {
|
||||
return room.leave(reason)
|
||||
}
|
||||
|
||||
override fun spaceSummary(): RoomSummary? {
|
||||
return spaceSummaryDataSource.getSpaceSummary(room.roomId)
|
||||
}
|
||||
|
||||
override suspend fun addChildren(roomId: String,
|
||||
viaServers: List<String>,
|
||||
order: String?,
|
||||
autoJoin: Boolean,
|
||||
suggested: Boolean?) {
|
||||
room.sendStateEvent(
|
||||
eventType = EventType.STATE_SPACE_CHILD,
|
||||
stateKey = roomId,
|
||||
body = SpaceChildContent(
|
||||
via = viaServers,
|
||||
autoJoin = autoJoin,
|
||||
order = order,
|
||||
suggested = suggested
|
||||
).toContent()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeChildren(roomId: String) {
|
||||
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
|
||||
.firstOrNull()
|
||||
?.content.toModel<SpaceChildContent>()
|
||||
?: // should we throw here?
|
||||
return
|
||||
|
||||
// edit state event and set via to null
|
||||
room.sendStateEvent(
|
||||
eventType = EventType.STATE_SPACE_CHILD,
|
||||
stateKey = roomId,
|
||||
body = SpaceChildContent(
|
||||
order = existing.order,
|
||||
via = null,
|
||||
autoJoin = existing.autoJoin
|
||||
).toContent()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setChildrenOrder(roomId: String, order: String?) {
|
||||
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
|
||||
.firstOrNull()
|
||||
?.content.toModel<SpaceChildContent>()
|
||||
?: throw IllegalArgumentException("$roomId is not a child of this space")
|
||||
|
||||
// edit state event and set via to null
|
||||
room.sendStateEvent(
|
||||
eventType = EventType.STATE_SPACE_CHILD,
|
||||
stateKey = roomId,
|
||||
body = SpaceChildContent(
|
||||
order = order,
|
||||
via = existing.via,
|
||||
autoJoin = existing.autoJoin
|
||||
).toContent()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) {
|
||||
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
|
||||
.firstOrNull()
|
||||
?.content.toModel<SpaceChildContent>()
|
||||
?: throw IllegalArgumentException("$roomId is not a child of this space")
|
||||
|
||||
// edit state event and set via to null
|
||||
room.sendStateEvent(
|
||||
eventType = EventType.STATE_SPACE_CHILD,
|
||||
stateKey = roomId,
|
||||
body = SpaceChildContent(
|
||||
order = existing.order,
|
||||
via = existing.via,
|
||||
autoJoin = autoJoin
|
||||
).toContent()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* 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 android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
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.GuestAccess
|
||||
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.RoomDirectoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
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.JoinSpaceResult
|
||||
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.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.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.create.CreateRoomTask
|
||||
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 javax.inject.Inject
|
||||
|
||||
internal class DefaultSpaceService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val createRoomTask: CreateRoomTask,
|
||||
private val joinSpaceTask: JoinSpaceTask,
|
||||
private val spaceGetter: SpaceGetter,
|
||||
private val roomGetter: RoomGetter,
|
||||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val peekSpaceTask: PeekSpaceTask,
|
||||
private val resolveSpaceInfoTask: ResolveSpaceInfoTask,
|
||||
private val leaveRoomTask: LeaveRoomTask
|
||||
) : SpaceService {
|
||||
|
||||
override suspend fun createSpace(params: CreateSpaceParams): String {
|
||||
return createRoomTask.executeRetry(params, 3)
|
||||
}
|
||||
|
||||
override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String {
|
||||
return createSpace(CreateSpaceParams().apply {
|
||||
this.name = name
|
||||
this.topic = topic
|
||||
this.avatarUri = avatarUri
|
||||
if (isPublic) {
|
||||
this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
|
||||
invite = 0
|
||||
)
|
||||
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
|
||||
this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
|
||||
this.guestAccess = GuestAccess.CanJoin
|
||||
} else {
|
||||
this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
|
||||
visibility = RoomDirectoryVisibility.PRIVATE
|
||||
enableEncryption()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getSpace(spaceId: String): Space? {
|
||||
return spaceGetter.get(spaceId)
|
||||
}
|
||||
|
||||
override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
|
||||
return roomSummaryDataSource.getSpaceSummariesLive(queryParams)
|
||||
}
|
||||
|
||||
override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> {
|
||||
return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
|
||||
}
|
||||
|
||||
override fun getRootSpaceSummaries(): List<RoomSummary> {
|
||||
return roomSummaryDataSource.getRootSpaceSummaries()
|
||||
}
|
||||
|
||||
override suspend fun peekSpace(spaceId: String): SpacePeekResult {
|
||||
return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
|
||||
}
|
||||
|
||||
override suspend fun querySpaceChildren(spaceId: String,
|
||||
suggestedOnly: Boolean?,
|
||||
autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> {
|
||||
return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response ->
|
||||
val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
|
||||
Pair(
|
||||
first = RoomSummary(
|
||||
roomId = spaceDesc?.roomId ?: spaceId,
|
||||
roomType = spaceDesc?.roomType,
|
||||
name = spaceDesc?.name ?: "",
|
||||
displayName = spaceDesc?.name ?: "",
|
||||
topic = spaceDesc?.topic ?: "",
|
||||
joinedMembersCount = spaceDesc?.numJoinedMembers,
|
||||
avatarUrl = spaceDesc?.avatarUrl ?: "",
|
||||
encryptionEventTs = null,
|
||||
typingUsers = emptyList(),
|
||||
isEncrypted = false,
|
||||
flattenParentIds = emptyList()
|
||||
),
|
||||
second = response.rooms
|
||||
?.filter { it.roomId != spaceId }
|
||||
?.map { childSummary ->
|
||||
val childStateEv = response.events
|
||||
?.firstOrNull { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
|
||||
val childStateEvContent = childStateEv?.content.toModel<SpaceChildContent>()
|
||||
SpaceChildInfo(
|
||||
childRoomId = childSummary.roomId,
|
||||
isKnown = true,
|
||||
roomType = childSummary.roomType,
|
||||
name = childSummary.name,
|
||||
topic = childSummary.topic,
|
||||
avatarUrl = childSummary.avatarUrl,
|
||||
order = childStateEvContent?.order,
|
||||
autoJoin = childStateEvContent?.autoJoin ?: false,
|
||||
viaServers = childStateEvContent?.via ?: emptyList(),
|
||||
activeMemberCount = childSummary.numJoinedMembers,
|
||||
parentRoomId = childStateEv?.roomId
|
||||
)
|
||||
}.orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun joinSpace(spaceIdOrAlias: String,
|
||||
reason: String?,
|
||||
viaServers: List<String>): JoinSpaceResult {
|
||||
return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers))
|
||||
}
|
||||
|
||||
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, missing powerlevel")
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
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.JoinSpaceResult
|
||||
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
|
||||
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.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
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, JoinSpaceResult> {
|
||||
data class Params(
|
||||
val roomIdOrAlias: String,
|
||||
val reason: String?,
|
||||
val viaServers: List<String> = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultJoinSpaceTask @Inject constructor(
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
@SessionDatabase
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val roomSummaryDataSource: RoomSummaryDataSource
|
||||
) : JoinSpaceTask {
|
||||
|
||||
override suspend fun execute(params: JoinSpaceTask.Params): JoinSpaceResult {
|
||||
Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...")
|
||||
try {
|
||||
joinRoomTask.execute(JoinRoomTask.Params(
|
||||
params.roomIdOrAlias,
|
||||
params.reason,
|
||||
params.viaServers
|
||||
))
|
||||
} catch (failure: Throwable) {
|
||||
return JoinSpaceResult.Fail(failure)
|
||||
}
|
||||
Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}")
|
||||
// we want to wait for sync result to check for auto join rooms
|
||||
|
||||
Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...")
|
||||
try {
|
||||
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm ->
|
||||
realm.where(RoomSummaryEntity::class.java)
|
||||
.apply {
|
||||
if (params.roomIdOrAlias.startsWith("!")) {
|
||||
equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias)
|
||||
} else {
|
||||
equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, params.roomIdOrAlias)
|
||||
}
|
||||
}
|
||||
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
|
||||
}
|
||||
} catch (exception: TimeoutCancellationException) {
|
||||
Timber.w("## Space: > Error created with timeout")
|
||||
return JoinSpaceResult.PartialSuccess(emptyMap())
|
||||
}
|
||||
|
||||
val errors = mutableMapOf<String, Throwable>()
|
||||
Timber.v("## Space: > Sync done ...")
|
||||
// after that i should have the children (? do I need to paginate to get state)
|
||||
val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
|
||||
Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}")
|
||||
summary?.spaceChildren?.forEach {
|
||||
// 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 (it.roomType == RoomType.SPACE) {
|
||||
// recursively join auto-joined child of this space?
|
||||
when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
|
||||
JoinSpaceResult.Success -> {
|
||||
// nop
|
||||
}
|
||||
is JoinSpaceResult.Fail -> {
|
||||
errors[it.childRoomId] = subspaceJoinResult.error
|
||||
}
|
||||
is JoinSpaceResult.PartialSuccess -> {
|
||||
errors.putAll(subspaceJoinResult.failedRooms)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Timber.v("## Space: Joining room child ${it.childRoomId}")
|
||||
joinRoomTask.execute(JoinRoomTask.Params(
|
||||
roomIdOrAlias = it.childRoomId,
|
||||
reason = "Auto-join parent space",
|
||||
viaServers = it.viaServers
|
||||
))
|
||||
} catch (failure: Throwable) {
|
||||
errors[it.childRoomId] = failure
|
||||
Timber.e("## Space: Failed to join room child ${it.childRoomId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
JoinSpaceResult.Success
|
||||
} else {
|
||||
JoinSpaceResult.PartialSuccess(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try {
|
||||
// awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
|
||||
// realm.where(RoomEntity::class.java)
|
||||
// .equalTo(RoomEntityFields.ROOM_ID, roomId)
|
||||
// }
|
||||
// } catch (exception: TimeoutCancellationException) {
|
||||
// throw CreateRoomFailure.CreatedWithTimeout
|
||||
// }
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
|
||||
data class Params(
|
||||
val spaceId: String,
|
||||
val maxRoomPerSpace: Int?,
|
||||
val limit: Int,
|
||||
val batchToken: String?,
|
||||
val suggestedOnly: Boolean?,
|
||||
val autoJoinOnly: Boolean?
|
||||
) {
|
||||
companion object {
|
||||
fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) =
|
||||
Params(
|
||||
spaceId = spaceId,
|
||||
maxRoomPerSpace = 10,
|
||||
limit = 20,
|
||||
batchToken = null,
|
||||
suggestedOnly = suggestedOnly,
|
||||
autoJoinOnly = autoJoinOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DefaultResolveSpaceInfoTask @Inject constructor(
|
||||
private val spaceApi: SpaceApi,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver
|
||||
) : ResolveSpaceInfoTask {
|
||||
override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse {
|
||||
val body = SpaceSummaryParams(
|
||||
maxRoomPerSpace = params.maxRoomPerSpace,
|
||||
limit = params.limit,
|
||||
batch = params.batchToken ?: "",
|
||||
autoJoinedOnly = params.autoJoinOnly,
|
||||
suggestedOnly = params.suggestedOnly
|
||||
)
|
||||
return executeRequest(globalErrorReceiver) {
|
||||
spaceApi.getSpaces(params.spaceId, body)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
internal interface SpaceApi {
|
||||
|
||||
/**
|
||||
*
|
||||
* POST /_matrix/client/r0/rooms/{roomID}/spaces
|
||||
* {
|
||||
* "max_rooms_per_space": 5, // The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1.
|
||||
* "auto_join_only": true, // If true, only return m.space.child events with auto_join:true, default: false, which returns all events.
|
||||
* "limit": 100, // The maximum number of rooms/subspaces to return, server can override this, default: 100.
|
||||
* "batch": "opaque_string" // A token to use if this is a subsequent HTTP hit, default: "".
|
||||
* }
|
||||
*
|
||||
* Ref:
|
||||
* - MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md
|
||||
* - https://hackmd.io/fNYh4tjUT5mQfR1uuRzWDA
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces")
|
||||
suspend fun getSpaces(@Path("roomId") spaceId: String,
|
||||
@Body params: SpaceSummaryParams): SpacesResponse
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SpaceChildSummaryResponse(
|
||||
/**
|
||||
* The total number of state events which point to or from this room (inbound/outbound edges).
|
||||
* This includes all m.space.child events in the room, in addition to m.room.parent events which point to this room as a parent.
|
||||
*/
|
||||
@Json(name = "num_refs") val numRefs: Int? = null,
|
||||
|
||||
/**
|
||||
* The room type, which is m.space for subspaces.
|
||||
* It can be omitted if there is no room type in which case it should be interpreted as a normal room.
|
||||
*/
|
||||
@Json(name = "room_type") val roomType: String? = null,
|
||||
|
||||
/**
|
||||
* Aliases of the room. May be empty.
|
||||
*/
|
||||
@Json(name = "aliases")
|
||||
val aliases: List<String>? = null,
|
||||
|
||||
/**
|
||||
* The canonical alias of the room, if any.
|
||||
*/
|
||||
@Json(name = "canonical_alias")
|
||||
val canonicalAlias: String? = null,
|
||||
|
||||
/**
|
||||
* The name of the room, if any.
|
||||
*/
|
||||
@Json(name = "name")
|
||||
val name: String? = null,
|
||||
|
||||
/**
|
||||
* Required. The number of members joined to the room.
|
||||
*/
|
||||
@Json(name = "num_joined_members")
|
||||
val numJoinedMembers: Int = 0,
|
||||
|
||||
/**
|
||||
* Required. The ID of the room.
|
||||
*/
|
||||
@Json(name = "room_id")
|
||||
val roomId: String,
|
||||
|
||||
/**
|
||||
* The topic of the room, if any.
|
||||
*/
|
||||
@Json(name = "topic")
|
||||
val topic: String? = null,
|
||||
|
||||
/**
|
||||
* Required. Whether the room may be viewed by guest users without joining.
|
||||
*/
|
||||
@Json(name = "world_readable")
|
||||
val worldReadable: Boolean = false,
|
||||
|
||||
/**
|
||||
* Required. Whether guest users may join the room and participate in it. If they can,
|
||||
* they will be subject to ordinary power level rules like any other user.
|
||||
*/
|
||||
@Json(name = "guest_can_join")
|
||||
val guestCanJoin: Boolean = false,
|
||||
|
||||
/**
|
||||
* The URL for the room's avatar, if one is set.
|
||||
*/
|
||||
@Json(name = "avatar_url")
|
||||
val avatarUrl: String? = null,
|
||||
|
||||
/**
|
||||
* Undocumented item
|
||||
*/
|
||||
@Json(name = "m.federate")
|
||||
val isFederated: Boolean = false
|
||||
)
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.room.DefaultSpaceGetter
|
||||
import org.matrix.android.sdk.internal.session.room.SpaceGetter
|
||||
import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask
|
||||
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@Module
|
||||
internal abstract class SpaceModule {
|
||||
|
||||
@Module
|
||||
companion object {
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@SessionScope
|
||||
fun providesSpacesAPI(retrofit: Retrofit): SpaceApi {
|
||||
return retrofit.create(SpaceApi::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindResolveSpaceTask(task: DefaultResolveSpaceInfoTask): ResolveSpaceInfoTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSpaceGetter(getter: DefaultSpaceGetter): SpaceGetter
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SpaceSummaryParams(
|
||||
/** The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1 */
|
||||
@Json(name = "max_rooms_per_space") val maxRoomPerSpace: Int?,
|
||||
/** The maximum number of rooms/subspaces to return, server can override this, default: 100 */
|
||||
@Json(name = "limit") val limit: Int?,
|
||||
/** A token to use if this is a subsequent HTTP hit, default: "". */
|
||||
@Json(name = "batch") val batch: String = "",
|
||||
/** whether we should only return children with the "suggested" flag set. */
|
||||
@Json(name = "suggested_only") val suggestedOnly: Boolean?,
|
||||
/** whether we should only return children with the "suggested" flag set. */
|
||||
@Json(name = "auto_join_only") val autoJoinedOnly: Boolean?
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SpacesResponse(
|
||||
/** Its presence indicates that there are more results to return. */
|
||||
@Json(name = "next_batch") val nextBatch: String? = null,
|
||||
/** Rooms information like name/avatar/type ... */
|
||||
@Json(name = "rooms") val rooms: List<SpaceChildSummaryResponse>? = null,
|
||||
/** These are the edges of the graph. The objects in the array are complete (or stripped?) m.room.parent or m.space.child events. */
|
||||
@Json(name = "events") val events: List<Event>? = null
|
||||
)
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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.peeking
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
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.room.model.RoomType
|
||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
|
||||
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface PeekSpaceTask : Task<PeekSpaceTask.Params, SpacePeekResult> {
|
||||
data class Params(
|
||||
val roomIdOrAlias: String,
|
||||
// A depth limit as a simple protection against cycles
|
||||
val maxDepth: Int = 4
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultPeekSpaceTask @Inject constructor(
|
||||
private val peekRoomTask: PeekRoomTask,
|
||||
private val resolveRoomStateTask: ResolveRoomStateTask
|
||||
) : PeekSpaceTask {
|
||||
|
||||
override suspend fun execute(params: PeekSpaceTask.Params): SpacePeekResult {
|
||||
val peekResult = peekRoomTask.execute(PeekRoomTask.Params(params.roomIdOrAlias))
|
||||
val roomResult = peekResult as? PeekResult.Success ?: return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult)
|
||||
|
||||
// check the room type
|
||||
// kind of duplicate cause we already did it in Peek? could we pass on the result??
|
||||
val stateEvents = try {
|
||||
resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomResult.roomId))
|
||||
} catch (failure: Throwable) {
|
||||
return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult)
|
||||
}
|
||||
val isSpace = stateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" }
|
||||
?.content
|
||||
?.toModel<RoomCreateContent>()
|
||||
?.type == RoomType.SPACE
|
||||
|
||||
if (!isSpace) return SpacePeekResult.NotSpaceType(params.roomIdOrAlias)
|
||||
|
||||
val children = peekChildren(stateEvents, 0, params.maxDepth)
|
||||
|
||||
return SpacePeekResult.Success(
|
||||
SpacePeekSummary(
|
||||
params.roomIdOrAlias,
|
||||
peekResult,
|
||||
children
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun peekChildren(stateEvents: List<Event>, depth: Int, maxDepth: Int): List<ISpaceChild> {
|
||||
if (depth >= maxDepth) return emptyList()
|
||||
val childRoomsIds = stateEvents
|
||||
.filter {
|
||||
it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty()
|
||||
// Children where via is not present are ignored.
|
||||
&& it.content?.toModel<SpaceChildContent>()?.via != null
|
||||
}
|
||||
.map { it.stateKey to it.content?.toModel<SpaceChildContent>() }
|
||||
|
||||
Timber.v("## SPACE_PEEK: found ${childRoomsIds.size} present children")
|
||||
|
||||
val spaceChildResults = mutableListOf<ISpaceChild>()
|
||||
childRoomsIds.forEach { entry ->
|
||||
|
||||
Timber.v("## SPACE_PEEK: peeking child $entry")
|
||||
// peek each child
|
||||
val childId = entry.first ?: return@forEach
|
||||
try {
|
||||
val childPeek = peekRoomTask.execute(PeekRoomTask.Params(childId))
|
||||
|
||||
val childStateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(childId))
|
||||
val createContent = childStateEvents
|
||||
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" }
|
||||
?.let { it.content?.toModel<RoomCreateContent>() }
|
||||
|
||||
if (!childPeek.isSuccess() || createContent == null) {
|
||||
Timber.v("## SPACE_PEEK: cannot peek child $entry")
|
||||
// can't peek :/
|
||||
spaceChildResults.add(
|
||||
SpaceChildPeekResult(
|
||||
childId, childPeek, entry.second?.autoJoin, entry.second?.order
|
||||
)
|
||||
)
|
||||
// continue to next child
|
||||
return@forEach
|
||||
}
|
||||
val type = createContent.type
|
||||
if (type == RoomType.SPACE) {
|
||||
Timber.v("## SPACE_PEEK: subspace child $entry")
|
||||
spaceChildResults.add(
|
||||
SpaceSubChildPeekResult(
|
||||
childId,
|
||||
childPeek,
|
||||
entry.second?.autoJoin,
|
||||
entry.second?.order,
|
||||
peekChildren(childStateEvents, depth + 1, maxDepth)
|
||||
)
|
||||
)
|
||||
} else
|
||||
/** if (type == RoomType.MESSAGING || type == null)*/
|
||||
{
|
||||
Timber.v("## SPACE_PEEK: room child $entry")
|
||||
spaceChildResults.add(
|
||||
SpaceChildPeekResult(
|
||||
childId, childPeek, entry.second?.autoJoin, entry.second?.order
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// let's check child info
|
||||
} catch (failure: Throwable) {
|
||||
// can this happen?
|
||||
Timber.e(failure, "## Failed to resolve space child")
|
||||
}
|
||||
}
|
||||
return spaceChildResults
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.peeking
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
|
||||
|
||||
// TODO Move to api package
|
||||
data class SpacePeekSummary(
|
||||
val idOrAlias: String,
|
||||
val roomPeekResult: PeekResult.Success,
|
||||
val children: List<ISpaceChild>
|
||||
)
|
||||
|
||||
interface ISpaceChild {
|
||||
val id: String
|
||||
val roomPeekResult: PeekResult
|
||||
val default: Boolean?
|
||||
val order: String?
|
||||
}
|
||||
|
||||
data class SpaceChildPeekResult(
|
||||
override val id: String,
|
||||
override val roomPeekResult: PeekResult,
|
||||
override val default: Boolean? = null,
|
||||
override val order: String? = null
|
||||
) : ISpaceChild
|
||||
|
||||
data class SpaceSubChildPeekResult(
|
||||
override val id: String,
|
||||
override val roomPeekResult: PeekResult,
|
||||
override val default: Boolean?,
|
||||
override val order: String?,
|
||||
val children: List<ISpaceChild>
|
||||
) : ISpaceChild
|
||||
|
||||
sealed class SpacePeekResult {
|
||||
abstract class SpacePeekError : SpacePeekResult()
|
||||
data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError()
|
||||
data class NotSpaceType(val spaceId: String) : SpacePeekError()
|
||||
|
||||
data class Success(val summary: SpacePeekSummary): SpacePeekResult()
|
||||
}
|
|
@ -143,7 +143,7 @@ internal class ReadReceiptHandler @Inject constructor(
|
|||
@Suppress("UNCHECKED_CAST")
|
||||
val content = dataFromFile
|
||||
.events
|
||||
.firstOrNull { it.type == EventType.RECEIPT }
|
||||
?.firstOrNull { it.type == EventType.RECEIPT }
|
||||
?.content as? ReadReceiptContent
|
||||
|
||||
if (content == null) {
|
||||
|
|
|
@ -95,8 +95,14 @@ 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)
|
||||
}
|
||||
|
||||
fun postSyncSpaceHierarchyHandle(realm: Realm) {
|
||||
roomSummaryUpdater.validateSpaceRelationship(realm)
|
||||
}
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleRoomSync(realm: Realm,
|
||||
|
@ -212,6 +218,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
// Timber.v("## Space state event: $eventEntity")
|
||||
eventId = event.eventId
|
||||
root = eventEntity
|
||||
}
|
||||
|
@ -455,7 +462,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
for (event in accountData.events) {
|
||||
accountData.events?.forEach { event ->
|
||||
val eventType = event.getClearType()
|
||||
if (eventType == EventType.TAG) {
|
||||
val content = event.getClearContent().toModel<RoomTagContent>()
|
||||
|
|
|
@ -132,6 +132,11 @@ internal class SyncResponseHandler @Inject constructor(
|
|||
|
||||
Timber.v("On sync completed")
|
||||
cryptoSyncHandler.onSyncCompleted(syncResponse)
|
||||
|
||||
// post sync stuffs
|
||||
monarchy.writeAsync {
|
||||
roomSyncHandler.postSyncSpaceHierarchyHandle(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,5 +25,5 @@ internal data class RoomSyncAccountData(
|
|||
/**
|
||||
* List of account data events (array of Event).
|
||||
*/
|
||||
@Json(name = "events") val events: List<Event> = emptyList()
|
||||
@Json(name = "events") val events: List<Event>? = null
|
||||
)
|
||||
|
|
|
@ -26,5 +26,5 @@ internal data class RoomSyncEphemeral(
|
|||
/**
|
||||
* List of ephemeral events (array of Event).
|
||||
*/
|
||||
@Json(name = "events") val events: List<Event> = emptyList()
|
||||
@Json(name = "events") val events: List<Event>? = null
|
||||
)
|
||||
|
|
|
@ -27,5 +27,5 @@ internal data class RoomSyncState(
|
|||
/**
|
||||
* List of state events (array of Event). The resulting state corresponds to the *start* of the timeline.
|
||||
*/
|
||||
@Json(name = "events") val events: List<Event> = emptyList()
|
||||
@Json(name = "events") val events: List<Event>? = null
|
||||
)
|
||||
|
|
|
@ -27,7 +27,7 @@ internal data class RoomSyncTimeline(
|
|||
/**
|
||||
* List of events (array of Event).
|
||||
*/
|
||||
@Json(name = "events") val events: List<Event> = emptyList(),
|
||||
@Json(name = "events") val events: List<Event>? = null,
|
||||
|
||||
/**
|
||||
* Boolean which tells whether there are more events on the server
|
||||
|
|
|
@ -37,7 +37,8 @@ internal data class ConfigurableTask<PARAMS, RESULT>(
|
|||
val id: UUID,
|
||||
val callbackThread: TaskThread,
|
||||
val executionThread: TaskThread,
|
||||
val callback: MatrixCallback<RESULT>
|
||||
val callback: MatrixCallback<RESULT>,
|
||||
val maxRetryCount: Int = 0
|
||||
|
||||
) : Task<PARAMS, RESULT> by task {
|
||||
|
||||
|
@ -57,7 +58,8 @@ internal data class ConfigurableTask<PARAMS, RESULT>(
|
|||
id = id,
|
||||
callbackThread = callbackThread,
|
||||
executionThread = executionThread,
|
||||
callback = callback
|
||||
callback = callback,
|
||||
maxRetryCount = retryCount
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,29 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.task
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.shouldBeRetried
|
||||
import timber.log.Timber
|
||||
|
||||
internal interface Task<PARAMS, RESULT> {
|
||||
|
||||
suspend fun execute(params: PARAMS): RESULT
|
||||
|
||||
suspend fun executeRetry(params: PARAMS, remainingRetry: Int) : RESULT {
|
||||
return try {
|
||||
execute(params)
|
||||
} catch (failure: Throwable) {
|
||||
if (failure.shouldBeRetried() && remainingRetry > 0) {
|
||||
Timber.d(failure, "## TASK: Retriable error")
|
||||
if (failure is Failure.ServerError) {
|
||||
val waitTime = failure.error.retryAfterMillis ?: 0L
|
||||
Timber.d(failure, "## TASK: Quota wait time $waitTime")
|
||||
delay(waitTime + 100)
|
||||
}
|
||||
return executeRetry(params, remainingRetry - 1)
|
||||
}
|
||||
throw failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,9 +40,9 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
|
|||
.launch(task.callbackThread.toDispatcher()) {
|
||||
val resultOrFailure = runCatching {
|
||||
withContext(task.executionThread.toDispatcher()) {
|
||||
Timber.v("Enqueue task $task")
|
||||
Timber.v("Execute task $task on ${Thread.currentThread().name}")
|
||||
task.execute(task.params)
|
||||
Timber.v("## TASK: Enqueue task $task")
|
||||
Timber.v("## TASK: Execute task $task on ${Thread.currentThread().name}")
|
||||
task.executeRetry(task.params, task.maxRetryCount)
|
||||
}
|
||||
}
|
||||
resultOrFailure
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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.util
|
||||
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.MatrixTest
|
||||
import org.matrix.android.sdk.internal.session.room.summary.Graph
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class GraphUtilsTest : MatrixTest {
|
||||
|
||||
@Test
|
||||
fun testCreateGraph() {
|
||||
val graph = Graph()
|
||||
|
||||
graph.addEdge("E", "C")
|
||||
graph.addEdge("B", "A")
|
||||
graph.addEdge("C", "A")
|
||||
graph.addEdge("D", "C")
|
||||
graph.addEdge("E", "D")
|
||||
|
||||
graph.getOrCreateNode("F")
|
||||
|
||||
System.out.println(graph.toString())
|
||||
|
||||
val backEdges = graph.findBackwardEdges(graph.getOrCreateNode("E"))
|
||||
|
||||
assertTrue(backEdges.isEmpty(), "There should not be any cycle in this graphs")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCycleGraph() {
|
||||
val graph = Graph()
|
||||
|
||||
graph.addEdge("E", "C")
|
||||
graph.addEdge("B", "A")
|
||||
graph.addEdge("C", "A")
|
||||
graph.addEdge("D", "C")
|
||||
graph.addEdge("E", "D")
|
||||
|
||||
graph.getOrCreateNode("F")
|
||||
|
||||
// adding loops
|
||||
graph.addEdge("C", "E")
|
||||
graph.addEdge("B", "B")
|
||||
|
||||
System.out.println(graph.toString())
|
||||
|
||||
val backEdges = graph.findBackwardEdges(graph.getOrCreateNode("E"))
|
||||
System.out.println(backEdges.joinToString(" | ") { "${it.source.name} -> ${it.destination.name}" })
|
||||
|
||||
assertTrue(backEdges.size == 2, "There should be 2 backward edges not ${backEdges.size}")
|
||||
|
||||
val edge1 = backEdges.find { it.source.name == "C" }
|
||||
assertNotNull(edge1, "There should be a back edge from C")
|
||||
assertEquals("E", edge1.destination.name, "There should be a back edge C -> E")
|
||||
|
||||
val edge2 = backEdges.find { it.source.name == "B" }
|
||||
assertNotNull(edge2, "There should be a back edge from B")
|
||||
assertEquals("B", edge2.destination.name, "There should be a back edge C -> C")
|
||||
|
||||
// clean the graph
|
||||
val acyclicGraph = graph.withoutEdges(backEdges)
|
||||
System.out.println(acyclicGraph.toString())
|
||||
|
||||
assertTrue(acyclicGraph.findBackwardEdges(acyclicGraph.getOrCreateNode("E")).isEmpty(), "There should be no backward edges")
|
||||
|
||||
val flatten = acyclicGraph.flattenDestination()
|
||||
|
||||
assertTrue(flatten[acyclicGraph.getOrCreateNode("A")]!!.isEmpty())
|
||||
|
||||
val flattenParentsB = flatten[acyclicGraph.getOrCreateNode("B")]
|
||||
assertTrue(flattenParentsB!!.size == 1)
|
||||
assertTrue(flattenParentsB.contains(acyclicGraph.getOrCreateNode("A")))
|
||||
|
||||
val flattenParentsE = flatten[acyclicGraph.getOrCreateNode("E")]
|
||||
assertTrue(flattenParentsE!!.size == 3)
|
||||
assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("A")))
|
||||
assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("C")))
|
||||
assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("D")))
|
||||
|
||||
// System.out.println(
|
||||
// buildString {
|
||||
// flatten.entries.forEach {
|
||||
// append("${it.key.name}: [")
|
||||
// append(it.value.joinToString(",") { it.name })
|
||||
// append("]\n")
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
}
|
||||
}
|
|
@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
|
|||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===94
|
||||
enum class===99
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
|
|
@ -333,6 +333,7 @@ dependencies {
|
|||
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||
|
||||
// Log
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"message": "William Shakespeare (bapt. 26 April 1564 – 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.",
|
||||
"roomName": "Matrix HQ",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"spaceName": "Runner's world",
|
||||
"roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topic, with a https://www.example.org url and a phone number: 0102030405 which should not be clickable."
|
||||
},
|
||||
{
|
||||
|
@ -14,6 +15,7 @@
|
|||
"message": "Hello!",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"spaceName": "Matrix Org",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
|
@ -22,6 +24,7 @@
|
|||
"message": "How are you?",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"spaceName": "Rennes",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
|
@ -30,6 +33,7 @@
|
|||
"message": "Great weather today!",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"spaceName": "Est London",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
|
@ -38,6 +42,7 @@
|
|||
"message": "Let's do a picnic",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"spaceName": "Element HQ",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
},
|
||||
{
|
||||
|
@ -46,6 +51,7 @@
|
|||
"message": "Yes, great idea",
|
||||
"roomName": "Room name very loooooooong with some details",
|
||||
"roomAlias": "#matrix:matrix.org",
|
||||
"spaceName": "My Company",
|
||||
"roomTopic": "Room topic very loooooooong with some details"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -291,6 +291,10 @@
|
|||
</activity>
|
||||
|
||||
<activity android:name=".features.devtools.RoomDevToolActivity" />
|
||||
<activity android:name=".features.spaces.SpacePreviewActivity" />
|
||||
<activity android:name=".features.spaces.SpaceExploreActivity" />
|
||||
<activity android:name=".features.spaces.SpaceCreationActivity" />
|
||||
<activity android:name=".features.spaces.manage.SpaceManageActivity" />
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
|
|
|
@ -19,20 +19,103 @@ package im.vector.app
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import arrow.core.Option
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.utils.BehaviorDataSource
|
||||
import im.vector.app.features.ui.UiStateRepository
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed class RoomGroupingMethod {
|
||||
data class ByLegacyGroup(val groupSummary: GroupSummary?) : RoomGroupingMethod()
|
||||
data class BySpace(val spaceSummary: RoomSummary?) : RoomGroupingMethod()
|
||||
}
|
||||
|
||||
fun RoomGroupingMethod.space() = (this as? RoomGroupingMethod.BySpace)?.spaceSummary
|
||||
fun RoomGroupingMethod.group() = (this as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary
|
||||
|
||||
/**
|
||||
* This class handles the global app state.
|
||||
* It requires to be added to ProcessLifecycleOwner.get().lifecycle
|
||||
*/
|
||||
// TODO Keep this class for now, will maybe be used fro Space
|
||||
@Singleton
|
||||
class AppStateHandler @Inject constructor() : LifecycleObserver {
|
||||
class AppStateHandler @Inject constructor(
|
||||
sessionDataSource: ActiveSessionDataSource,
|
||||
private val uiStateRepository: UiStateRepository,
|
||||
private val activeSessionHolder: ActiveSessionHolder
|
||||
) : LifecycleObserver {
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomGroupingMethod>>(Option.empty())
|
||||
|
||||
val selectedRoomGroupingObservable = selectedSpaceDataSource.observe()
|
||||
|
||||
fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? = selectedSpaceDataSource.currentValue?.orNull()
|
||||
|
||||
fun setCurrentSpace(spaceId: String?, session: Session? = null) {
|
||||
val uSession = session ?: activeSessionHolder.getSafeActiveSession()
|
||||
if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace
|
||||
&& spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return
|
||||
val spaceSum = spaceId?.let { uSession?.getRoomSummary(spaceId) }
|
||||
selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.BySpace(spaceSum)))
|
||||
if (spaceId != null) {
|
||||
GlobalScope.launch {
|
||||
tryOrNull {
|
||||
uSession?.getRoom(spaceId)?.loadRoomMembersIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentGroup(groupId: String?, session: Session? = null) {
|
||||
val uSession = session ?: activeSessionHolder.getSafeActiveSession()
|
||||
if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.ByLegacyGroup
|
||||
&& groupId == selectedSpaceDataSource.currentValue?.orNull()?.group()?.groupId) return
|
||||
val activeGroup = groupId?.let { uSession?.getGroupSummary(groupId) }
|
||||
selectedSpaceDataSource.post(Option.just(RoomGroupingMethod.ByLegacyGroup(activeGroup)))
|
||||
if (groupId != null) {
|
||||
GlobalScope.launch {
|
||||
tryOrNull {
|
||||
uSession?.getGroup(groupId)?.fetchGroupData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
sessionDataSource.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe {
|
||||
// sessionDataSource could already return a session while acitveSession holder still returns null
|
||||
it.orNull()?.let { session ->
|
||||
if (uiStateRepository.isGroupingMethodSpace(session.sessionId)) {
|
||||
setCurrentSpace(uiStateRepository.getSelectedSpace(session.sessionId), session)
|
||||
} else {
|
||||
setCurrentGroup(uiStateRepository.getSelectedGroup(session.sessionId), session)
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
compositeDisposable.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun safeActiveSpaceId(): String? {
|
||||
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.spaceSummary?.roomId
|
||||
}
|
||||
|
||||
fun safeActiveGroupId(): String? {
|
||||
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
}
|
||||
|
@ -40,5 +123,16 @@ class AppStateHandler @Inject constructor() : LifecycleObserver {
|
|||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
compositeDisposable.clear()
|
||||
val session = activeSessionHolder.getSafeActiveSession() ?: return
|
||||
when (val currentMethod = selectedSpaceDataSource.currentValue?.orNull() ?: RoomGroupingMethod.BySpace(null)) {
|
||||
is RoomGroupingMethod.BySpace -> {
|
||||
uiStateRepository.storeGroupingMethod(true, session.sessionId)
|
||||
uiStateRepository.storeSelectedSpace(currentMethod.spaceSummary?.roomId, session.sessionId)
|
||||
}
|
||||
is RoomGroupingMethod.ByLegacyGroup -> {
|
||||
uiStateRepository.storeGroupingMethod(false, session.sessionId)
|
||||
uiStateRepository.storeSelectedGroup(currentMethod.groupSummary?.groupId, session.sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ import im.vector.app.features.devtools.RoomDevToolSendFormFragment
|
|||
import im.vector.app.features.devtools.RoomDevToolStateEventListFragment
|
||||
import im.vector.app.features.discovery.DiscoverySettingsFragment
|
||||
import im.vector.app.features.discovery.change.SetIdentityServerFragment
|
||||
import im.vector.app.features.grouplist.GroupListFragment
|
||||
import im.vector.app.features.home.HomeDetailFragment
|
||||
import im.vector.app.features.home.HomeDrawerFragment
|
||||
import im.vector.app.features.home.LoadingFragment
|
||||
|
@ -72,6 +71,8 @@ import im.vector.app.features.login.LoginSplashFragment
|
|||
import im.vector.app.features.login.LoginWaitForEmailFragment
|
||||
import im.vector.app.features.login.LoginWebFragment
|
||||
import im.vector.app.features.login.terms.LoginTermsFragment
|
||||
import im.vector.app.features.matrixto.MatrixToRoomSpaceFragment
|
||||
import im.vector.app.features.matrixto.MatrixToUserFragment
|
||||
import im.vector.app.features.pin.PinFragment
|
||||
import im.vector.app.features.qrcode.QrCodeScannerFragment
|
||||
import im.vector.app.features.reactions.EmojiChooserFragment
|
||||
|
@ -117,6 +118,14 @@ import im.vector.app.features.settings.push.PushRulesFragment
|
|||
import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
|
||||
import im.vector.app.features.share.IncomingShareFragment
|
||||
import im.vector.app.features.signout.soft.SoftLogoutFragment
|
||||
import im.vector.app.features.spaces.SpaceListFragment
|
||||
import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment
|
||||
import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment
|
||||
import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
|
||||
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
|
||||
import im.vector.app.features.spaces.explore.SpaceDirectoryFragment
|
||||
import im.vector.app.features.spaces.manage.SpaceAddRoomFragment
|
||||
import im.vector.app.features.spaces.preview.SpacePreviewFragment
|
||||
import im.vector.app.features.terms.ReviewTermsFragment
|
||||
import im.vector.app.features.usercode.ShowUserCodeFragment
|
||||
import im.vector.app.features.userdirectory.UserListFragment
|
||||
|
@ -142,8 +151,8 @@ interface FragmentModule {
|
|||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(GroupListFragment::class)
|
||||
fun bindGroupListFragment(fragment: GroupListFragment): Fragment
|
||||
@FragmentKey(SpaceListFragment::class)
|
||||
fun bindSpaceListFragment(fragment: SpaceListFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
@ -624,4 +633,49 @@ interface FragmentModule {
|
|||
@IntoMap
|
||||
@FragmentKey(RoomDevToolSendFormFragment::class)
|
||||
fun bindRoomDevToolSendFormFragment(fragment: RoomDevToolSendFormFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SpacePreviewFragment::class)
|
||||
fun bindSpacePreviewFragment(fragment: SpacePreviewFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(ChooseSpaceTypeFragment::class)
|
||||
fun bindChooseSpaceTypeFragment(fragment: ChooseSpaceTypeFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(CreateSpaceDetailsFragment::class)
|
||||
fun bindCreateSpaceDetailsFragment(fragment: CreateSpaceDetailsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(CreateSpaceDefaultRoomsFragment::class)
|
||||
fun bindCreateSpaceDefaultRoomsFragment(fragment: CreateSpaceDefaultRoomsFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(MatrixToUserFragment::class)
|
||||
fun bindMatrixToUserFragment(fragment: MatrixToUserFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(MatrixToRoomSpaceFragment::class)
|
||||
fun bindMatrixToRoomSpaceFragment(fragment: MatrixToRoomSpaceFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SpaceDirectoryFragment::class)
|
||||
fun bindSpaceDirectoryFragment(fragment: SpaceDirectoryFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(ChoosePrivateSpaceTypeFragment::class)
|
||||
fun bindChoosePrivateSpaceTypeFragment(fragment: ChoosePrivateSpaceTypeFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(SpaceAddRoomFragment::class)
|
||||
fun bindSpaceAddRoomFragment(fragment: SpaceAddRoomFragment): Fragment
|
||||
}
|
||||
|
|
|
@ -77,6 +77,12 @@ import im.vector.app.features.settings.VectorSettingsActivity
|
|||
import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet
|
||||
import im.vector.app.features.share.IncomingShareActivity
|
||||
import im.vector.app.features.signout.soft.SoftLogoutActivity
|
||||
import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet
|
||||
import im.vector.app.features.spaces.ShareSpaceBottomSheet
|
||||
import im.vector.app.features.spaces.SpaceCreationActivity
|
||||
import im.vector.app.features.spaces.SpaceExploreActivity
|
||||
import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet
|
||||
import im.vector.app.features.spaces.manage.SpaceManageActivity
|
||||
import im.vector.app.features.terms.ReviewTermsActivity
|
||||
import im.vector.app.features.ui.UiStateRepository
|
||||
import im.vector.app.features.usercode.UserCodeActivity
|
||||
|
@ -151,6 +157,9 @@ interface ScreenComponent {
|
|||
fun inject(activity: CallTransferActivity)
|
||||
fun inject(activity: ReAuthActivity)
|
||||
fun inject(activity: RoomDevToolActivity)
|
||||
fun inject(activity: SpaceCreationActivity)
|
||||
fun inject(activity: SpaceExploreActivity)
|
||||
fun inject(activity: SpaceManageActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -173,6 +182,9 @@ interface ScreenComponent {
|
|||
fun inject(bottomSheet: CallControlsBottomSheet)
|
||||
fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
|
||||
fun inject(bottomSheet: MatrixToBottomSheet)
|
||||
fun inject(bottomSheet: ShareSpaceBottomSheet)
|
||||
fun inject(bottomSheet: SpaceSettingsMenuBottomSheet)
|
||||
fun inject(bottomSheet: InviteRoomSpaceChooserBottomSheet)
|
||||
|
||||
/* ==========================================================================================
|
||||
* Others
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.res.Resources
|
|||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.AppStateHandler
|
||||
import im.vector.app.EmojiCompatFontProvider
|
||||
import im.vector.app.EmojiCompatWrapper
|
||||
import im.vector.app.VectorApplication
|
||||
|
@ -34,8 +35,8 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
|||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
import im.vector.app.features.grouplist.SelectedGroupDataSource
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.CurrentSpaceSuggestedRoomListDataSource
|
||||
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
|
@ -113,7 +114,9 @@ interface VectorComponent {
|
|||
|
||||
fun errorFormatter(): ErrorFormatter
|
||||
|
||||
fun selectedGroupStore(): SelectedGroupDataSource
|
||||
fun appStateHandler(): AppStateHandler
|
||||
|
||||
fun currentSpaceSuggestedRoomListDataSource(): CurrentSpaceSuggestedRoomListDataSource
|
||||
|
||||
fun roomDetailPendingActionStore(): RoomDetailPendingActionStore
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel
|
|||
import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheetSharedActionViewModel
|
||||
import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel
|
||||
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel
|
||||
import im.vector.app.features.spaces.SpacePreviewSharedActionViewModel
|
||||
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
|
||||
|
||||
@Module
|
||||
|
@ -142,4 +143,9 @@ interface ViewModelModule {
|
|||
@IntoMap
|
||||
@ViewModelKey(DiscoverySharedViewModel::class)
|
||||
fun bindDiscoverySharedViewModel(viewModel: DiscoverySharedViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(SpacePreviewSharedActionViewModel::class)
|
||||
fun bindSpacePreviewSharedActionViewModel(viewModel: SpacePreviewSharedActionViewModel): ViewModel
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package im.vector.app.core.epoxy.bottomsheet
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
|
||||
/**
|
||||
* A action for bottom sheet.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_radio)
|
||||
abstract class BottomSheetRadioActionItem : VectorEpoxyModel<BottomSheetRadioActionItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var title: CharSequence? = null
|
||||
|
||||
@StringRes
|
||||
@EpoxyAttribute
|
||||
var titleRes: Int? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var selected = false
|
||||
|
||||
@EpoxyAttribute
|
||||
var description: CharSequence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var listener: View.OnClickListener
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.view.setOnClickListener {
|
||||
listener.onClick(it)
|
||||
}
|
||||
|
||||
if (titleRes != null) {
|
||||
holder.titleText.setText(titleRes!!)
|
||||
} else {
|
||||
holder.titleText.text = title
|
||||
}
|
||||
holder.descriptionText.setTextOrHide(description)
|
||||
|
||||
if (selected) {
|
||||
holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on))
|
||||
holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked)
|
||||
} else {
|
||||
holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off))
|
||||
holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val titleText by bind<TextView>(R.id.actionTitle)
|
||||
val descriptionText by bind<TextView>(R.id.actionDescription)
|
||||
val radioImage by bind<ImageView>(R.id.radioIcon)
|
||||
}
|
||||
}
|
|
@ -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.core.platform.livedata
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Generic Bottom sheet with actions
|
||||
*/
|
||||
abstract class BottomSheetGeneric<STATE : BottomSheetGenericState, ACTION : BottomSheetGenericAction> :
|
||||
abstract class BottomSheetGeneric<STATE : BottomSheetGenericState, ACTION : BottomSheetGenericRadioAction> :
|
||||
VectorBaseBottomSheetDialogFragment<BottomSheetGenericListBinding>(),
|
||||
BottomSheetGenericController.Listener<ACTION> {
|
||||
|
||||
|
|
|
@ -17,12 +17,11 @@ package im.vector.app.core.ui.bottomsheet
|
|||
|
||||
import android.view.View
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.core.epoxy.dividerItem
|
||||
|
||||
/**
|
||||
* Epoxy controller for generic bottom sheet actions
|
||||
*/
|
||||
abstract class BottomSheetGenericController<State : BottomSheetGenericState, Action : BottomSheetGenericAction>
|
||||
abstract class BottomSheetGenericController<State : BottomSheetGenericState, Action : BottomSheetGenericRadioAction>
|
||||
: TypedEpoxyController<State>() {
|
||||
|
||||
var listener: Listener<Action>? = null
|
||||
|
@ -43,16 +42,14 @@ abstract class BottomSheetGenericController<State : BottomSheetGenericState, Act
|
|||
subTitle(getSubTitle())
|
||||
}
|
||||
|
||||
dividerItem {
|
||||
id("title_separator")
|
||||
}
|
||||
// dividerItem {
|
||||
// id("title_separator")
|
||||
// }
|
||||
}
|
||||
// Actions
|
||||
val actions = getActions(state)
|
||||
val showIcons = actions.any { it.iconResId > 0 }
|
||||
actions.forEach { action ->
|
||||
action.toBottomSheetItem()
|
||||
.showIcon(showIcons)
|
||||
action.toRadioBottomSheetItem()
|
||||
.listener(View.OnClickListener { listener?.didSelectAction(action) })
|
||||
.addTo(this)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue