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_JOIN_RULES = "m.room.join_rules"
const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access" const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels" 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_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 * Note that this Event has been deprecated, see
* - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events * - 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_NEGOTIATE = "m.call.negotiate"
const val CALL_REJECT = "m.call.reject" const val CALL_REJECT = "m.call.reject"
const val CALL_HANGUP = "m.call.hangup" const val CALL_HANGUP = "m.call.hangup"
// This type is not processed by the client, just sent to the server // This type is not processed by the client, just sent to the server
const val CALL_REPLACES = "m.call.replaces" const val CALL_REPLACES = "m.call.replaces"

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.api.session.room.alias package org.matrix.android.sdk.api.session.room.alias
sealed class RoomAliasError : Throwable() { sealed class RoomAliasError : Throwable() {
object AliasEmpty : RoomAliasError() object AliasIsBlank : RoomAliasError()
object AliasNotAvailable : RoomAliasError() object AliasNotAvailable : RoomAliasError()
object AliasInvalid : 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( data class SpaceChildInfo(
val roomSummary: IRoomSummary?, val roomSummary: IRoomSummary?,
val present: Boolean,
val order: String?, val order: String?,
val autoJoin: Boolean, val autoJoin: Boolean,
val viaServers: List<String> 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 android.net.Uri
import org.matrix.android.sdk.api.session.identity.ThreePid 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.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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 * 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. * Mark as a direct message room.
@ -149,6 +149,6 @@ open class CreateRoomParams {
companion object { companion object {
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate" 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 * @return the power level
*/ */
fun getUserPowerLevelValue(userId: String): Int { fun getUserPowerLevelValue(userId: String): Int {
return powerLevelsContent.users.getOrElse(userId) { return powerLevelsContent.users?.get(userId)
powerLevelsContent.usersDefault ?: powerLevelsContent.usersDefault
}
} }
/** /**

View file

@ -16,12 +16,20 @@
package org.matrix.android.sdk.api.session.space 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.RoomType
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
class CreateSpaceParams : CreateRoomParams() { class CreateSpaceParams : CreateRoomParams() {
init { init {
// Space-rooms are distinguished from regular messaging rooms by the m.room.type of m.space
roomType = RoomType.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 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> // fun getChildren() : List<IRoomSummary>
} }

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.api.session.space package org.matrix.android.sdk.api.session.space
import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -31,6 +32,11 @@ interface SpaceService {
*/ */
suspend fun createSpace(params: CreateSpaceParams): String 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 * Get a space from a roomId
* @param roomId the roomId to look for. * @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 * Use this call get preview of children of this space, particularly useful to get a
* preview of rooms that you did not join yet. * 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 * 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. * 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 { sealed class JoinSpaceResult {
object Success: JoinSpaceResult() object Success : JoinSpaceResult()
data class Fail(val error: Throwable?): JoinSpaceResult() data class Fail(val error: Throwable) : JoinSpaceResult()
/** Success fully joined the space, but failed to join all or some of it's rooms */ /** 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() data class PartialSuccess(val failedRooms: Map<String, Throwable>) : JoinSpaceResult()
@ -74,8 +81,7 @@ interface SpaceService {
suspend fun joinSpace(spaceIdOrAlias: String, suspend fun joinSpace(spaceIdOrAlias: String,
reason: String? = null, reason: String? = null,
viaServers: List<String> = emptyList(), viaServers: List<String> = emptyList()): JoinSpaceResult
autoJoinChild: List<ChildAutoJoinInfo>) : JoinSpaceResult
suspend fun rejectInvite(spaceId: String, reason: String?) suspend fun rejectInvite(spaceId: String, reason: String?)
} }

View file

@ -31,13 +31,9 @@ import com.squareup.moshi.JsonClass
data class SpaceChildContent( data class SpaceChildContent(
/** /**
* Key which gives a list of candidate servers that can be used to join the room * 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, @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. * 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. * (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, @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. * The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should
* This means that when a user joins the parent space, they will automatically be joined to those default children. * 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") val spaceChildInfoSchema = realm.schema.create("SpaceChildInfoEntity")
?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java) ?.addField(SpaceChildInfoEntityFields.ORDER, String::class.java)
?.addField(SpaceChildInfoEntityFields.PRESENT, Boolean::class.java)
?.setNullable(SpaceChildInfoEntityFields.PRESENT, true)
?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java) ?.addRealmListField(SpaceChildInfoEntityFields.VIA_SERVERS.`$`, String::class.java)
?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!) ?.addRealmObjectField(SpaceChildInfoEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)

View file

@ -31,7 +31,6 @@ internal class SpaceSummaryMapper @Inject constructor(private val roomSummaryMap
SpaceChildInfo( SpaceChildInfo(
roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) }, roomSummary = it.roomSummaryEntity?.let { rs -> roomSummaryMapper.map(rs) },
autoJoin = it.autoJoin ?: false, autoJoin = it.autoJoin ?: false,
present = it.present ?: false,
viaServers = it.viaServers.map { it }, viaServers = it.viaServers.map { it },
order = it.order 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,8 +24,6 @@ import io.realm.RealmObject
*/ */
internal open class SpaceChildInfoEntity( internal open class SpaceChildInfoEntity(
var viaServers: RealmList<String> = RealmList(), 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 // Use for alphabetic ordering of this child
var order: String? = null, var order: String? = null,
// If true, this child should be join when parent is joined // 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) @Throws(RoomAliasError::class)
suspend fun check(aliasLocalPart: String?) { suspend fun check(aliasLocalPart: String?) {
if (aliasLocalPart.isNullOrEmpty()) { if (aliasLocalPart.isNullOrEmpty()) {
throw RoomAliasError.AliasEmpty // don't check empty or not provided alias
return
}
if (aliasLocalPart.isBlank()) {
throw RoomAliasError.AliasIsBlank
} }
// Check alias availability // Check alias availability
val fullAlias = aliasLocalPart.toFullLocalAlias(userId) 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.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.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.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody 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 * The power level content to override in the default power level event
*/ */
@Json(name = "power_level_content_override") @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( data class SpaceChildInfo(
val roomId: String, val roomId: String,
val present: Boolean,
val order: String?, val order: String?,
val autoJoin: Boolean, val autoJoin: Boolean,
val viaServers: List<String> val viaServers: List<String>
) )
/**
* Gets the ordered list of valid child description.
*/
fun getDirectChildrenDescriptions(): List<SpaceChildInfo> { fun getDirectChildrenDescriptions(): List<SpaceChildInfo> {
return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD) return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
.findAll() .findAll()
// .filter { ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.present == true }
.mapNotNull { .mapNotNull {
// ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()
ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc -> ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc ->
Timber.d("## Space child desc state event $scc") Timber.d("## Space child desc state event $scc")
SpaceChildInfo( // Children where via is not present are ignored.
roomId = it.stateKey, scc.via?.let { via ->
present = scc.present ?: false, SpaceChildInfo(
order = scc.order, roomId = it.stateKey,
autoJoin = scc.default ?: false, order = scc.validOrder(),
viaServers = scc.via ?: emptyList() autoJoin = scc.autoJoin ?: false,
) viaServers = via
)
}
} }
} }
.sortedBy { it.order }
} }
} }

View file

@ -50,7 +50,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
eventsDefault = content.eventsDefault, eventsDefault = content.eventsDefault,
events = content.events, events = content.events,
usersDefault = content.usersDefault, usersDefault = content.usersDefault,
users = content.users, users = content.users ?: emptyMap(),
stateDefault = content.stateDefault, stateDefault = content.stateDefault,
notifications = content.notifications.mapValues { content.notificationLevel(it.key) } 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 val roomType = ContentMapper.map(roomCreateEvent?.content).toModel<RoomCreateContent>()?.type
roomSummaryEntity.roomType = roomType 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 // 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) 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 { realm.createObject<SpaceChildInfoEntity>().apply {
this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId) this.roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, it.roomId)
this.order = it.order this.order = it.order
this.present = it.present
this.autoJoin = it.autoJoin this.autoJoin = it.autoJoin
}.also { }.also {
Timber.v("## Space: Updating summary for room $roomId with children $it") 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 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.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.Room 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.Space
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
@ -28,11 +30,34 @@ class DefaultSpace(private val room: Room) : Space {
return room return room
} }
override suspend fun addRoom(roomId: String) { override suspend fun addChildren(roomId: String, viaServers: List<String>, order: String?, autoJoin: Boolean) {
asRoom().sendStateEvent( asRoom().sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD, eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId, 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 package org.matrix.android.sdk.internal.session.space
import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.EventType 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.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.Space import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.space.SpaceService
@ -66,6 +68,15 @@ internal class DefaultSpaceService @Inject constructor(
return createRoomTask.execute(params) 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? { override fun getSpace(spaceId: String): Space? {
return roomGetter.getRoom(spaceId) return roomGetter.getRoom(spaceId)
?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE } ?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE }
@ -120,8 +131,7 @@ internal class DefaultSpaceService @Inject constructor(
isEncrypted = false isEncrypted = false
), ),
order = childStateEv?.order, order = childStateEv?.order,
present = childStateEv?.present ?: false, autoJoin = childStateEv?.autoJoin ?: false,
autoJoin = childStateEv?.default ?: false,
viaServers = childStateEv?.via ?: emptyList() viaServers = childStateEv?.via ?: emptyList()
) )
} ?: emptyList() } ?: emptyList()
@ -131,30 +141,8 @@ internal class DefaultSpaceService @Inject constructor(
override suspend fun joinSpace(spaceIdOrAlias: String, override suspend fun joinSpace(spaceIdOrAlias: String,
reason: String?, reason: String?,
viaServers: List<String>, viaServers: List<String>): SpaceService.JoinSpaceResult {
autoJoinChild: List<SpaceService.ChildAutoJoinInfo>): SpaceService.JoinSpaceResult { return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers))
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)
}
} }
override suspend fun rejectInvite(spaceId: String, reason: String?) { 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 io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException 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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
@ -32,7 +32,7 @@ import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, Unit> { internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, SpaceService.JoinSpaceResult> {
data class Params( data class Params(
val roomIdOrAlias: String, val roomIdOrAlias: String,
val reason: String?, val reason: String?,
@ -48,13 +48,17 @@ internal class DefaultJoinSpaceTask @Inject constructor(
private val spaceSummaryDataSource: SpaceSummaryDataSource private val spaceSummaryDataSource: SpaceSummaryDataSource
) : JoinSpaceTask { ) : 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} ...") Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...")
joinRoomTask.execute(JoinRoomTask.Params( try {
params.roomIdOrAlias, joinRoomTask.execute(JoinRoomTask.Params(
params.reason, params.roomIdOrAlias,
params.viaServers params.reason,
)) params.viaServers
))
} catch (failure: Throwable) {
return SpaceService.JoinSpaceResult.Fail(failure)
}
Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}") Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}")
// we want to wait for sync result to check for auto join rooms // 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) { } catch (exception: TimeoutCancellationException) {
Timber.w("## Space: > Error created with timeout") 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 ...") 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) val summary = spaceSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}") Timber.v("## Space: Found space summary Name:[${summary?.roomSummary?.name}] children: ${summary?.children?.size}")
summary?.children?.forEach { summary?.children?.forEach {
val childRoomSummary = it.roomSummary ?: return@forEach val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] present: ${it.present} autoJoin:${it.autoJoin}") Timber.v("## Space: Processing child :[${childRoomSummary.roomId}] autoJoin:${it.autoJoin}")
if (it.present && it.autoJoin) { if (it.autoJoin) {
// I should try to join as well // I should try to join as well
if (childRoomSummary.roomType == RoomType.SPACE) { if (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 { } else {
try { try {
Timber.v("## Space: Joining room child ${childRoomSummary.roomId}") Timber.v("## Space: Joining room child ${childRoomSummary.roomId}")
@ -95,12 +112,18 @@ internal class DefaultJoinSpaceTask @Inject constructor(
viaServers = it.viaServers viaServers = it.viaServers
)) ))
} catch (failure: Throwable) { } catch (failure: Throwable) {
// todo keep track for partial success errors[it.roomSummary.roomId] = failure
Timber.e("## Space: Failed to join room child ${childRoomSummary.roomId}") 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View file

@ -75,8 +75,8 @@ internal class DefaultPeekSpaceTask @Inject constructor(
val childRoomsIds = stateEvents val childRoomsIds = stateEvents
.filter { .filter {
it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty() it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty()
// Children where present is not present or is not set to true are ignored. // Children where via is not present are ignored.
&& it.content?.toModel<SpaceChildContent>()?.present == true && it.content?.toModel<SpaceChildContent>()?.via != null
} }
.map { it.stateKey to it.content?.toModel<SpaceChildContent>() } .map { it.stateKey to it.content?.toModel<SpaceChildContent>() }
@ -101,7 +101,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
// can't peek :/ // can't peek :/
spaceChildResults.add( spaceChildResults.add(
SpaceChildPeekResult( SpaceChildPeekResult(
childId, childPeek, entry.second?.default, entry.second?.order childId, childPeek, entry.second?.autoJoin, entry.second?.order
) )
) )
// continue to next child // continue to next child
@ -114,7 +114,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
SpaceSubChildPeekResult( SpaceSubChildPeekResult(
childId, childId,
childPeek, childPeek,
entry.second?.default, entry.second?.autoJoin,
entry.second?.order, entry.second?.order,
peekChildren(childStateEvents, depth + 1, maxDepth) peekChildren(childStateEvents, depth + 1, maxDepth)
) )
@ -125,7 +125,7 @@ internal class DefaultPeekSpaceTask @Inject constructor(
Timber.v("## SPACE_PEEK: room child $entry") Timber.v("## SPACE_PEEK: room child $entry")
spaceChildResults.add( spaceChildResults.add(
SpaceChildPeekResult( 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), CONFETTI("/confetti", "<message>", R.string.command_confetti),
SNOW("/snow", "<message>", R.string.command_snow), SNOW("/snow", "<message>", R.string.command_snow),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space), 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 val length
get() = command.length + 1 get() = command.length + 1

View file

@ -318,6 +318,18 @@ object CommandParser {
rawCommand 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 -> { else -> {
// Unknown command // Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand) ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -59,4 +59,6 @@ sealed class ParsedCommand {
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand() class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand()
class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand() class CreateSpace(val name: String, val invitees: List<String>) : ParsedCommand()
class AddToSpace(val spaceId: 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) invitedUserIds.addAll(slashCommandResult.invitees)
} }
val spaceId = session.spaceService().createSpace(params) 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) { } catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
} }
@ -842,7 +848,37 @@ class RoomDetailViewModel @AssistedInject constructor(
is ParsedCommand.AddToSpace -> { is ParsedCommand.AddToSpace -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { 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) { } catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure)) _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.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown 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.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session 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.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
@ -182,6 +184,15 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
return@withState 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 { setState {
copy(asyncCreateRoomRequest = Loading()) copy(asyncCreateRoomRequest = Loading())
} }

View file

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

View file

@ -100,7 +100,7 @@ class SpacePreviewViewModel @AssistedInject constructor(
// trigger modal loading // trigger modal loading
_viewEvents.post(SpacePreviewViewEvents.StartJoining) _viewEvents.post(SpacePreviewViewEvents.StartJoining)
viewModelScope.launch(Dispatchers.IO) { 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) { when (joinResult) {
SpaceService.JoinSpaceResult.Success, SpaceService.JoinSpaceResult.Success,
is SpaceService.JoinSpaceResult.PartialSuccess -> { is SpaceService.JoinSpaceResult.PartialSuccess -> {

View file

@ -3248,6 +3248,9 @@
<string name="dev_tools_event_content_hint">Event content</string> <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 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_sending">Sending</string>
<string name="event_status_a11y_sent">Sent</string> <string name="event_status_a11y_sent">Sent</string>