Udpate since msc 1772

This commit is contained in:
Valere 2021-02-11 13:12:02 +01:00
parent a8d7c25244
commit c8916ee83c
33 changed files with 544 additions and 99 deletions

View file

@ -0,0 +1,188 @@
/*
* 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 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.Membership
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
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.SpaceService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.internal.util.awaitCallback
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 summry 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")
commonTestHelper.signOutAndClose(session)
}
@Test
fun testJoinSimplePublicSpace() {
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("alice", 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 summry update it self :/
delay(400)
}
// Try to join from bob, it's a public space no need to invite
val joinResult: SpaceService.JoinSpaceResult
runBlocking {
joinResult = bobSession.spaceService().joinSpace(spaceId)
}
assertEquals(SpaceService.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("alice", SessionTestParams(true))
val roomName = "My Space"
val topic = "A public space for test"
val spaceId: String
val firstChild: String
val secondChild: String
spaceId = runBlocking { aliceSession.spaceService().createSpace(roomName, topic, null, true) }
val syncedSpace = aliceSession.spaceService().getSpace(spaceId)
// create a room
firstChild = runBlocking {
awaitCallback<String> {
aliceSession.createRoom(CreateRoomParams().apply {
this.name = "FirstRoom"
this.topic = "Description of first room"
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
}, it)
}
}
runBlocking {
syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true)
}
secondChild = runBlocking {
awaitCallback {
aliceSession.createRoom(CreateRoomParams().apply {
this.name = "SecondRoom"
this.topic = "Description of second room"
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
}, it)
}
}
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(SpaceService.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?.children?.size ?: -1, "Unexpected number of children")
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
}
}

View file

@ -51,9 +51,13 @@ object EventType {
const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
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 = "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
@ -76,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

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

@ -0,0 +1,67 @@
/*
* 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
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
*/
@JsonClass(generateAdapter = true)
data class PowerLevelsContentOverride(
/**
* The level required to ban a user. Defaults to 50 if unspecified.
*/
@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? = null,
/**
* The level required to invite a user. Defaults to 50 if unspecified.
*/
@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? = 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? = 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>? = 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? = 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>? = 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? = 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>? = null
)

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.session.room.model
data class SpaceChildInfo(
val roomSummary: IRoomSummary?,
val present: Boolean,
val order: String?,
val autoJoin: Boolean,
val viaServers: List<String>

View file

@ -18,7 +18,7 @@ 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.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContentOverride
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.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@ -125,7 +125,7 @@ open class CreateRoomParams {
/**
* The power level content to override in the default power level event
*/
var powerLevelContentOverride: PowerLevelsContent? = null
var powerLevelContentOverride: PowerLevelsContentOverride? = null
/**
* Mark as a direct message room.
@ -149,6 +149,6 @@ open class CreateRoomParams {
companion object {
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
private const val CREATION_CONTENT_KEY_ROOM_TYPE = "org.matrix.msc1772.type"
}
}

View file

@ -31,9 +31,8 @@ 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.usersDefault
}
/**

View file

@ -16,12 +16,20 @@
package org.matrix.android.sdk.api.session.space
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContentOverride
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 = PowerLevelsContentOverride(
eventsDefault = 100
)
}
}

View file

@ -22,7 +22,9 @@ interface Space {
fun asRoom() : Room
suspend fun addRoom(roomId: String)
suspend fun addChildren(roomId: String, viaServers: List<String>, order: String?, autoJoin: Boolean = false)
suspend fun removeRoom(roomId: String)
// fun getChildren() : List<IRoomSummary>
}

View file

@ -16,6 +16,7 @@
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
@ -31,6 +32,11 @@ interface SpaceService {
*/
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 roomId the roomId to look for.
@ -43,12 +49,12 @@ interface SpaceService {
* 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
suspend fun peekSpace(spaceId: String): SpacePeekResult
/**
* Get's information of a space by querying the server
*/
suspend fun querySpaceChildren(spaceId: String) : Pair<RoomSummary, List<SpaceChildInfo>>
suspend fun querySpaceChildren(spaceId: String): Pair<RoomSummary, List<SpaceChildInfo>>
/**
* Get a live list of space summaries. This list is refreshed as soon as the data changes.
@ -64,8 +70,9 @@ interface SpaceService {
)
sealed class JoinSpaceResult {
object Success: JoinSpaceResult()
data class Fail(val error: Throwable?): 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()
@ -74,8 +81,7 @@ interface SpaceService {
suspend fun joinSpace(spaceIdOrAlias: String,
reason: String? = null,
viaServers: List<String> = emptyList(),
autoJoinChild: List<ChildAutoJoinInfo>) : JoinSpaceResult
viaServers: List<String> = emptyList()): JoinSpaceResult
suspend fun rejectInvite(spaceId: String, reason: String?)
}

View file

@ -31,13 +31,9 @@ import com.squareup.moshi.JsonClass
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,
/**
* present: true key is included to distinguish from a deleted state event
* Children where present is not present or is not set to true are ignored.
*/
@Json(name = "present") val present: Boolean? = false,
/**
* 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.
@ -46,8 +42,25 @@ data class SpaceChildContent(
*/
@Json(name = "order") val order: String? = null,
/**
* The default flag on a child listing allows a space admin to list the "default" sub-spaces and rooms in that space.
* This means that when a user joins the parent space, they will automatically be joined to those default children.
* 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 = "default") val default: Boolean? = false
)
@Json(name = "auto_join") val autoJoin: 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? {
order?.let {
if (order.length > 50) return null
if (!ORDER_VALID_CHAR_REGEX.matches(it)) return null
}
return order
}
companion object {
private val ORDER_VALID_CHAR_REGEX = "[ -~]+".toRegex()
}
}

View file

@ -0,0 +1,54 @@
/*
* 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"],
* "present": true,
* "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,
/**
* present: true key is included to distinguish from a deleted state event
* Parent where present is not present (sic) or is not set to true are ignored.
*/
@Json(name = "present") val present: Boolean? = false,
/**
* Canonical determines whether this is the main parent for the space.
* When a user joins a room with a canonical parent, clients may switch to view the room
* 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

@ -209,8 +209,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity")
?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java)
?.addField(SpaceChildInfoEntityFields.PRESENT, Boolean::class.java)
?.setNullable(SpaceChildInfoEntityFields.PRESENT, true)
?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java)
?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)

View file

@ -31,7 +31,6 @@ internal class SpaceSummaryMapper @Inject constructor(private val roomSummaryMap
SpaceChildInfo(
roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) },
autoJoin = it.autoJoin ?: false,
present = it.present ?: false,
viaServers = it.viaServers.map { it },
order = it.order
)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 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.
@ -24,8 +24,6 @@ import io.realm.RealmObject
*/
internal open class SpaceChildInfoEntity(
var viaServers: RealmList<String> = RealmList(),
// it's an active child of the space if and only if present is not null and true
var present: Boolean? = null,
// Use for alphabetic ordering of this child
var order: String? = null,
// If true, this child should be join when parent is joined

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

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.room.create
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContentOverride
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
@ -111,5 +111,5 @@ 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: PowerLevelsContentOverride?
)

View file

@ -41,28 +41,31 @@ internal class RoomRelationshipHelper(private val realm: Realm,
data class SpaceChildInfo(
val roomId: String,
val present: Boolean,
val order: String?,
val autoJoin: Boolean,
val viaServers: List<String>
)
/**
* Gets the ordered list of valid child description.
*/
fun getDirectChildrenDescriptions(): List<SpaceChildInfo> {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
.findAll()
// .filter { ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.present == true }
.mapNotNull {
// ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()
ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc ->
Timber.d("## Space child desc state event $scc")
// Children where via is not present are ignored.
scc.via?.let { via ->
SpaceChildInfo(
roomId = it.stateKey,
present = scc.present ?: false,
order = scc.order,
autoJoin = scc.default ?: false,
viaServers = scc.via ?: emptyList()
order = scc.validOrder(),
autoJoin = scc.autoJoin ?: false,
viaServers = via
)
}
}
}
.sortedBy { it.order }
}
}

View file

@ -50,7 +50,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
eventsDefault = content.eventsDefault,
events = content.events,
usersDefault = content.usersDefault,
users = content.users,
users = content.users ?: emptyMap(),
stateDefault = content.stateDefault,
notifications = content.notifications.mapValues { content.notificationLevel(it.key) }
)

View file

@ -99,6 +99,7 @@ internal class RoomSummaryUpdater @Inject constructor(
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)
@ -176,7 +177,6 @@ internal class RoomSummaryUpdater @Inject constructor(
realm.createObject<SpaceChildInfoEntity>().apply {
this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId)
this.order = it.order
this.present = it.present
this.autoJoin = it.autoJoin
}.also {
Timber.v("## Space: Updating summary for room $roomId with children $it")

View file

@ -16,8 +16,10 @@
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.space.Space
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
@ -28,11 +30,34 @@ class DefaultSpace(private val room: Room) : Space {
return room
}
override suspend fun addRoom(roomId: String) {
override suspend fun addChildren(roomId: String, viaServers: List<String>, order: String?, autoJoin: Boolean) {
asRoom().sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(present = true).toContent()
body = SpaceChildContent(
via = viaServers,
autoJoin = autoJoin,
order = order
).toContent()
)
}
override suspend fun removeRoom(roomId: String) {
val existing = asRoom().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
asRoom().sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
order = existing.order,
via = null,
autoJoin = existing.autoJoin
).toContent()
)
}

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.space
import android.net.Uri
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.EventType
@ -23,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
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.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceService
@ -66,6 +68,15 @@ internal class DefaultSpaceService @Inject constructor(
return createRoomTask.execute(params)
}
override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String {
return createSpace(CreateSpaceParams().apply {
this.name = name
this.topic = topic
this.preset = if (isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
this.avatarUri = avatarUri
})
}
override fun getSpace(spaceId: String): Space? {
return roomGetter.getRoom(spaceId)
?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE }
@ -120,8 +131,7 @@ internal class DefaultSpaceService @Inject constructor(
isEncrypted = false
),
order = childStateEv?.order,
present = childStateEv?.present ?: false,
autoJoin = childStateEv?.default ?: false,
autoJoin = childStateEv?.autoJoin ?: false,
viaServers = childStateEv?.via ?: emptyList()
)
} ?: emptyList()
@ -131,30 +141,8 @@ internal class DefaultSpaceService @Inject constructor(
override suspend fun joinSpace(spaceIdOrAlias: String,
reason: String?,
viaServers: List<String>,
autoJoinChild: List<SpaceService.ChildAutoJoinInfo>): SpaceService.JoinSpaceResult {
try {
joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers))
// TODO partial success
return SpaceService.JoinSpaceResult.Success
// val childJoinFailures = mutableMapOf<String, Throwable>()
// autoJoinChild.forEach { info ->
// // TODO what if the child is it self a subspace with some default children?
// try {
// joinRoomTask.execute(JoinRoomTask.Params(info.roomIdOrAlias, null, info.viaServers))
// } catch (failure: Throwable) {
// // TODO, i could already be a member of this room, handle that as it should not be an error in this context
// childJoinFailures[info.roomIdOrAlias] = failure
// }
// }
// return if (childJoinFailures.isEmpty()) {
// SpaceService.JoinSpaceResult.Success
// } else {
// SpaceService.JoinSpaceResult.PartialSuccess(childJoinFailures)
// }
} catch (throwable: Throwable) {
return SpaceService.JoinSpaceResult.Fail(throwable)
}
viaServers: List<String>): SpaceService.JoinSpaceResult {
return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers))
}
override suspend fun rejectInvite(spaceId: String, reason: String?) {

View file

@ -18,9 +18,9 @@ package org.matrix.android.sdk.internal.session.space
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
@ -32,7 +32,7 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, Unit> {
internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, SpaceService.JoinSpaceResult> {
data class Params(
val roomIdOrAlias: String,
val reason: String?,
@ -48,13 +48,17 @@ internal class DefaultJoinSpaceTask @Inject constructor(
private val spaceSummaryDataSource: SpaceSummaryDataSource
) : JoinSpaceTask {
override suspend fun execute(params: JoinSpaceTask.Params) {
override suspend fun execute(params: JoinSpaceTask.Params): SpaceService.JoinSpaceResult {
Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...")
try {
joinRoomTask.execute(JoinRoomTask.Params(
params.roomIdOrAlias,
params.reason,
params.viaServers
))
} catch (failure: Throwable) {
return SpaceService.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
@ -73,19 +77,32 @@ internal class DefaultJoinSpaceTask @Inject constructor(
}
} catch (exception: TimeoutCancellationException) {
Timber.w("## Space: > Error created with timeout")
throw CreateRoomFailure.CreatedWithTimeout
return SpaceService.JoinSpaceResult.PartialSuccess(emptyMap())
}
val errors = HashMap<String, Throwable>()
Timber.v("## Space: > Sync done ...")
// after that i should have the children (? do i nead to paginate to get state)
// after that i should have the children (? do I need to paginate to get state)
val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}")
summary?.children?.forEach {
val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] present: ${it.present} autoJoin:${it.autoJoin}")
if (it.present && it.autoJoin) {
Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}")
if (it.autoJoin) {
// I should try to join as well
if (childRoomSummary.roomType == RoomType.SPACE) {
// recursively join auto-joined child of this space?
when (val subspaceJoinResult = this.execute(JoinSpaceTask.Params(it.roomSummary.roomId, null, it.viaServers))) {
SpaceService.JoinSpaceResult.Success -> {
// nop
}
is SpaceService.JoinSpaceResult.Fail -> {
errors[it.roomSummary.roomId] = subspaceJoinResult.error
}
is SpaceService.JoinSpaceResult.PartialSuccess -> {
errors.putAll(subspaceJoinResult.failedRooms)
}
}
} else {
try {
Timber.v("## Space: Joining room child ${childRoomSummary.roomId}")
@ -95,12 +112,18 @@ internal class DefaultJoinSpaceTask @Inject constructor(
viaServers = it.viaServers
))
} catch (failure: Throwable) {
// todo keep track for partial success
errors[it.roomSummary.roomId] = failure
Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}")
}
}
}
}
return if (errors.isEmpty()) {
SpaceService.JoinSpaceResult.Success
} else {
SpaceService.JoinSpaceResult.PartialSuccess(errors)
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 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.

View file

@ -75,8 +75,8 @@ internal class DefaultPeekSpaceTask @Inject constructor(
val childRoomsIds = stateEvents
.filter {
it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty()
// Children where present is not present or is not set to true are ignored.
&& it.content?.toModel<SpaceChildContent>()?.present == true
// Children where via is not present are ignored.
&& it.content?.toModel<SpaceChildContent>()?.via != null
}
.map { it.stateKey to it.content?.toModel<SpaceChildContent>() }
@ -101,7 +101,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
// can't peek :/
spaceChildResults.add(
SpaceChildPeekResult(
childId, childPeek, entry.second?.default, entry.second?.order
childId, childPeek, entry.second?.autoJoin, entry.second?.order
)
)
// continue to next child
@ -114,7 +114,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
SpaceSubChildPeekResult(
childId,
childPeek,
entry.second?.default,
entry.second?.autoJoin,
entry.second?.order,
peekChildren(childStateEvents, depth + 1, maxDepth)
)
@ -125,7 +125,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
Timber.v("## SPACE_PEEK: room child $entry")
spaceChildResults.add(
SpaceChildPeekResult(
childId, childPeek, entry.second?.default, entry.second?.order
childId, childPeek, entry.second?.autoJoin, entry.second?.order
)
)
}

View file

@ -48,7 +48,9 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
CONFETTI("/confetti", "<message>", R.string.command_confetti),
SNOW("/snow", "<message>", R.string.command_snow),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space);
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space),
JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space),
LEAVE_ROOM("/leave", "<roomId?>", R.string.command_description_leave_room);
val length
get() = command.length + 1

View file

@ -318,6 +318,18 @@ object CommandParser {
rawCommand
)
}
Command.JOIN_SPACE.command -> {
val spaceIdOrAlias = textMessage.substring(Command.JOIN_SPACE.command.length).trim()
ParsedCommand.JoinSpace(
spaceIdOrAlias
)
}
Command.LEAVE_ROOM.command -> {
val spaceIdOrAlias = textMessage.substring(Command.LEAVE_ROOM.command.length).trim()
ParsedCommand.LeaveRoom(
spaceIdOrAlias
)
}
else -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -59,4 +59,6 @@ sealed class ParsedCommand {
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand()
class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand()
class AddToSpace(val spaceId: String) : ParsedCommand()
class JoinSpace(val spaceIdOrAlias: String) : ParsedCommand()
class LeaveRoom(val roomId: String) : ParsedCommand()
}

View file

@ -831,7 +831,13 @@ class RoomDetailViewModel @AssistedInject constructor(
invitedUserIds.addAll(slashCommandResult.invitees)
}
val spaceId = session.spaceService().createSpace(params)
session.spaceService().getSpace(spaceId)?.addRoom(state.roomId)
session.spaceService().getSpace(spaceId)
?.addChildren(
state.roomId,
listOf(session.sessionParams.homeServerHost ?: ""),
null,
true
)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
@ -842,7 +848,37 @@ class RoomDetailViewModel @AssistedInject constructor(
is ParsedCommand.AddToSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().getSpace(slashCommandResult.spaceId)?.addRoom(room.roomId)
session.spaceService().getSpace(slashCommandResult.spaceId)
?.addChildren(
room.roomId,
listOf(session.sessionParams.homeServerHost ?: ""),
null,
false
)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
}
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.JoinSpace -> {
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().joinSpace(slashCommandResult.spaceIdOrAlias)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
}
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.LeaveRoom -> {
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback {
session.getRoom(slashCommandResult.roomId)?.leave(null, it)
}
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}

View file

@ -26,8 +26,8 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
@ -37,6 +37,8 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
@ -182,6 +184,15 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
return@withState
}
if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public
&& state.roomVisibilityType.aliasLocalPart.isBlank()) {
// we require an alias for public rooms
setState {
copy(asyncCreateRoomRequest = Fail(CreateRoomFailure.AliasError(RoomAliasError.AliasIsBlank)))
}
return@withState
}
setState {
copy(asyncCreateRoomRequest = Loading())
}

View file

@ -26,7 +26,7 @@ class RoomAliasErrorFormatter @Inject constructor(
) {
fun format(roomAliasError: RoomAliasError?): String? {
return when (roomAliasError) {
is RoomAliasError.AliasEmpty -> R.string.create_room_alias_empty
is RoomAliasError.AliasIsBlank -> R.string.create_room_alias_empty
is RoomAliasError.AliasNotAvailable -> R.string.create_room_alias_already_in_use
is RoomAliasError.AliasInvalid -> R.string.create_room_alias_invalid
else -> null

View file

@ -100,7 +100,7 @@ class SpacePreviewViewModel @AssistedInject constructor(
// trigger modal loading
_viewEvents.post(SpacePreviewViewEvents.StartJoining)
viewModelScope.launch(Dispatchers.IO) {
val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia, emptyList())
val joinResult = session.spaceService().joinSpace(initialState.idOrAlias, null, spaceVia)
when (joinResult) {
SpaceService.JoinSpaceResult.Success,
is SpaceService.JoinSpaceResult.PartialSuccess -> {

View file

@ -3248,6 +3248,9 @@
<string name="dev_tools_event_content_hint">Event content</string>
<string name="command_description_create_space">Create a community</string>
<string name="command_description_create_space">Create a Spcae</string>
<string name="command_description_join_space">Join the Space with the given id</string>
<string name="command_description_leave_room">Leave room with given id (or current room if null)</string>
<string name="event_status_a11y_sending">Sending</string>
<string name="event_status_a11y_sent">Sent</string>