Merge pull request #2840 from vector-im/feature/bca/spaces_sdk

Spaces support - beta
This commit is contained in:
Benoit Marty 2021-04-29 10:18:31 +02:00 committed by GitHub
commit 751efb57fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
312 changed files with 14432 additions and 1165 deletions

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -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.

View file

@ -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"

View file

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

View file

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@ -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>>
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,6 @@ data class RoomAggregateNotificationCount(
val notificationCount: Int,
val highlightCount: Int
) {
val totalCount = notificationCount + highlightCount
val totalCount = notificationCount
val isHighlight = highlightCount > 0
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
@ -64,7 +66,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()
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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> {

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.alias.AliasService
import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.api.session.room.members.MembershipService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
import org.matrix.android.sdk.api.session.room.read.ReadService
@ -36,11 +37,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)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
*/

View file

@ -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 {

View file

@ -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 :/

View file

@ -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 ?: ""
)
}
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,67 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.summary
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.Optional
internal class HierarchyLiveDataHelper(
val spaceId: String,
val memberships: List<Membership>,
val roomSummaryDataSource: RoomSummaryDataSource) {
private val sources = HashMap<String, LiveData<Optional<RoomSummary>>>()
private val mediatorLiveData = MediatorLiveData<List<String>>()
fun liveData(): 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 })
}
}
}

View file

@ -1,5 +1,6 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -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)
}
}
}
}
}
}
}

View file

@ -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 {
//
// }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
}
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.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)
}
}
}
}
}

View file

@ -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> {

View file

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