Private (me and teamates) space support

This commit is contained in:
Valere 2021-04-06 15:26:43 +02:00
parent bfbd37074e
commit 79bde6ee91
24 changed files with 461 additions and 65 deletions

View File

@ -40,7 +40,7 @@ data class RoomGuestAccessContent(
} }
@JsonClass(generateAdapter = false) @JsonClass(generateAdapter = false)
enum class GuestAccess { enum class GuestAccess(val value: String) {
@Json(name = "can_join") CanJoin, @Json(name = "can_join") CanJoin("can_join"),
@Json(name = "forbidden") Forbidden @Json(name = "forbidden") Forbidden("forbidden")
} }

View File

@ -24,9 +24,10 @@ import com.squareup.moshi.JsonClass
* Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules
*/ */
@JsonClass(generateAdapter = false) @JsonClass(generateAdapter = false)
enum class RoomJoinRules { enum class RoomJoinRules(val value: String) {
@Json(name = "public") PUBLIC, @Json(name = "public") PUBLIC("public"),
@Json(name = "invite") INVITE, @Json(name = "invite") INVITE("invite"),
@Json(name = "knock") KNOCK, @Json(name = "knock") KNOCK("knock"),
@Json(name = "private") PRIVATE @Json(name = "private") PRIVATE("private"),
@Json(name = "restricted") RESTRICTED("restricted")
} }

View File

@ -0,0 +1,33 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RoomJoinRulesAllowEntry(
/**
* space: The room ID of the space to check the membership of.
*/
@Json(name = "space") val spaceID: String,
/**
* via: A list of servers which may be used to peek for membership of the space.
*/
@Json(name = "via") val via: List<String>
)

View File

@ -1,5 +1,6 @@
/* /*
* Copyright 2020 The Matrix.org Foundation C.I.C. * Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2021 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * 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.
@ -26,16 +27,22 @@ import timber.log.Timber
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomJoinRulesContent( data class RoomJoinRulesContent(
@Json(name = "join_rule") val _joinRules: String? = null @Json(name = "join_rule") val _joinRules: String? = null,
/**
* If the allow key is an empty list (or not a list at all), then the room reverts to standard public join rules
*/
@Json(name = "allow") val allowList:List<RoomJoinRulesAllowEntry>? = null
) { ) {
val joinRules: RoomJoinRules? = when (_joinRules) { val joinRules: RoomJoinRules? = when (_joinRules) {
"public" -> RoomJoinRules.PUBLIC "public" -> RoomJoinRules.PUBLIC
"invite" -> RoomJoinRules.INVITE "invite" -> RoomJoinRules.INVITE
"knock" -> RoomJoinRules.KNOCK "knock" -> RoomJoinRules.KNOCK
"private" -> RoomJoinRules.PRIVATE "private" -> RoomJoinRules.PRIVATE
"restricted" -> RoomJoinRules.RESTRICTED
else -> { else -> {
Timber.w("Invalid value for RoomJoinRules: `$_joinRules`") Timber.w("Invalid value for RoomJoinRules: `$_joinRules`")
null null
} }
} }
} }

View File

@ -22,6 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
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.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
// TODO Give a way to include other initial states // TODO Give a way to include other initial states
@ -153,8 +155,14 @@ open class CreateRoomParams {
algorithm = MXCRYPTO_ALGORITHM_MEGOLM algorithm = MXCRYPTO_ALGORITHM_MEGOLM
} }
var roomVersion: String? = null
var joinRuleRestricted: List<RoomJoinRulesAllowEntry>? = null
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 = "org.matrix.msc1772.type" private const val CREATION_CONTENT_KEY_ROOM_TYPE = "org.matrix.msc1772.type"
} }
} }

View File

@ -111,5 +111,13 @@ 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: PowerLevelsContent?,
/**
* The room version to set for the room. If not provided, the homeserver is to use its configured default.
* If provided, the homeserver will return a 400 error with the errcode M_UNSUPPORTED_ROOM_VERSION if it does not support the room version.
*/
@Json(name = "room_version")
val roomVersion: String?
) )

View File

@ -23,7 +23,11 @@ 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.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.toMedium import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.DeviceListManager
@ -73,11 +77,17 @@ internal class CreateRoomBodyBuilder @Inject constructor(
} }
} }
if (params.joinRuleRestricted != null) {
params.roomVersion = "org.matrix.msc3083"
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
}
val initialStates = listOfNotNull( val initialStates = listOfNotNull(
buildEncryptionWithAlgorithmEvent(params), buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params), buildHistoryVisibilityEvent(params),
buildAvatarEvent(params), buildAvatarEvent(params),
buildGuestAccess(params) buildGuestAccess(params),
buildJoinRulesRestricted(params)
) )
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
@ -92,7 +102,9 @@ internal class CreateRoomBodyBuilder @Inject constructor(
initialStates = initialStates, initialStates = initialStates,
preset = params.preset, preset = params.preset,
isDirect = params.isDirect, isDirect = params.isDirect,
powerLevelContentOverride = params.powerLevelContentOverride powerLevelContentOverride = params.powerLevelContentOverride,
roomVersion = params.roomVersion
) )
} }
@ -132,7 +144,21 @@ internal class CreateRoomBodyBuilder @Inject constructor(
Event( Event(
type = EventType.STATE_ROOM_GUEST_ACCESS, type = EventType.STATE_ROOM_GUEST_ACCESS,
stateKey = "", stateKey = "",
content = RoomGuestAccessContent(it.name).toContent() content = RoomGuestAccessContent(it.value).toContent()
)
}
}
private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? {
return params.joinRuleRestricted
?.let { allowList ->
Event(
type = EventType.STATE_ROOM_JOIN_RULES,
stateKey = "",
content = RoomJoinRulesContent(
_joinRules = RoomJoinRules.RESTRICTED.value,
allowList = allowList
).toContent()
) )
} }
} }

View File

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.GuestAccess
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.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
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.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
@ -67,14 +68,18 @@ internal class DefaultSpaceService @Inject constructor(
return createSpace(CreateSpaceParams().apply { return createSpace(CreateSpaceParams().apply {
this.name = name this.name = name
this.topic = topic this.topic = topic
this.preset = if (isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT
this.avatarUri = avatarUri this.avatarUri = avatarUri
if (isPublic) { if (isPublic) {
this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
invite = 0 invite = 0
) )
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
this.guestAccess = GuestAccess.CanJoin this.guestAccess = GuestAccess.CanJoin
} else {
this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
} }
}) })
} }

View File

@ -120,6 +120,7 @@ import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment
import im.vector.app.features.share.IncomingShareFragment import im.vector.app.features.share.IncomingShareFragment
import im.vector.app.features.signout.soft.SoftLogoutFragment import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.spaces.SpaceListFragment
import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment
import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment
import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
@ -672,4 +673,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(SpaceDirectoryFragment::class) @FragmentKey(SpaceDirectoryFragment::class)
fun bindSpaceDirectoryFragment(fragment: SpaceDirectoryFragment): Fragment fun bindSpaceDirectoryFragment(fragment: SpaceDirectoryFragment): Fragment
@Binds
@IntoMap
@FragmentKey(ChoosePrivateSpaceTypeFragment::class)
fun bindChoosePrivateSpaceTypeFragment(fragment: ChoosePrivateSpaceTypeFragment): Fragment
} }

View File

@ -128,14 +128,36 @@ class RoomSettingsController @Inject constructor(
private fun RoomSettingsViewState.getJoinRuleWording(): String { private fun RoomSettingsViewState.getJoinRuleWording(): String {
val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules
val guestAccess = newRoomJoinRules.newGuestAccess ?: currentGuestAccess val guestAccess = newRoomJoinRules.newGuestAccess ?: currentGuestAccess
return stringProvider.getString(if (joinRule == RoomJoinRules.INVITE) { val resId = when (joinRule) {
R.string.room_settings_room_access_entry_only_invited RoomJoinRules.INVITE -> {
} else { R.string.room_settings_room_access_entry_only_invited to null
}
RoomJoinRules.PRIVATE -> {
R.string.room_settings_room_access_entry_unknown to joinRule.value
}
RoomJoinRules.PUBLIC -> {
if (guestAccess == GuestAccess.CanJoin) { if (guestAccess == GuestAccess.CanJoin) {
R.string.room_settings_room_access_entry_anyone_with_link_including_guest R.string.room_settings_room_access_entry_anyone_with_link_including_guest to null
} else { } else {
R.string.room_settings_room_access_entry_anyone_with_link_apart_guest R.string.room_settings_room_access_entry_anyone_with_link_apart_guest to null
} }
}) }
RoomJoinRules.KNOCK -> {
R.string.room_settings_room_access_entry_knock to null
}
RoomJoinRules.RESTRICTED -> {
R.string.room_settings_room_access_entry_restricted to null
}
}
return if (resId.second == null) stringProvider.getString(resId.first) else stringProvider.getString(resId.first, resId.second)
// return stringProvider.getString(if (joinRule == RoomJoinRules.INVITE) {
// R.string.room_settings_room_access_entry_only_invited
// } else {
// if (guestAccess == GuestAccess.CanJoin) {
// R.string.room_settings_room_access_entry_anyone_with_link_including_guest
// } else {
// R.string.room_settings_room_access_entry_anyone_with_link_apart_guest
// }
// })
} }
} }

View File

@ -51,6 +51,13 @@ class RoomJoinRuleController @Inject constructor(
title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_including_guest), title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_including_guest),
iconResId = 0, iconResId = 0,
isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC && state.currentGuestAccess == GuestAccess.CanJoin isSelected = state.currentRoomJoinRule == RoomJoinRules.PUBLIC && state.currentGuestAccess == GuestAccess.CanJoin
),
RoomJoinRuleAction(
roomJoinRule = RoomJoinRules.RESTRICTED,
roomGuestAccess = null,
title = stringProvider.getString(R.string.room_settings_room_access_entry_restricted),
iconResId = 0,
isSelected = state.currentRoomJoinRule == RoomJoinRules.RESTRICTED
) )
) )
} }

View File

@ -28,6 +28,7 @@ import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment
import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment
import im.vector.app.features.spaces.create.CreateSpaceAction import im.vector.app.features.spaces.create.CreateSpaceAction
import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
@ -35,6 +36,7 @@ import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
import im.vector.app.features.spaces.create.CreateSpaceEvents import im.vector.app.features.spaces.create.CreateSpaceEvents
import im.vector.app.features.spaces.create.CreateSpaceState import im.vector.app.features.spaces.create.CreateSpaceState
import im.vector.app.features.spaces.create.CreateSpaceViewModel import im.vector.app.features.spaces.create.CreateSpaceViewModel
import im.vector.app.features.spaces.create.SpaceType
import javax.inject.Inject import javax.inject.Inject
class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Factory { class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Factory {
@ -85,6 +87,9 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
CreateSpaceEvents.NavigateToAddRooms -> { CreateSpaceEvents.NavigateToAddRooms -> {
navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java)
} }
CreateSpaceEvents.NavigateToChoosePrivateType -> {
navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java)
}
is CreateSpaceEvents.ShowModalError -> { is CreateSpaceEvents.ShowModalError -> {
hideWaitingView() hideWaitingView()
AlertDialog.Builder(this) AlertDialog.Builder(this)
@ -124,8 +129,12 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
private fun renderState(state: CreateSpaceState) { private fun renderState(state: CreateSpaceState) {
val titleRes = when (state.step) { val titleRes = when (state.step) {
CreateSpaceState.Step.ChooseType -> R.string.activity_create_space_title CreateSpaceState.Step.ChooseType -> R.string.activity_create_space_title
CreateSpaceState.Step.SetDetails -> R.string.your_public_space CreateSpaceState.Step.SetDetails,
CreateSpaceState.Step.AddRooms -> R.string.your_public_space CreateSpaceState.Step.AddRooms -> {
if (state.spaceType == SpaceType.Public) R.string.your_public_space
else R.string.your_private_space
}
CreateSpaceState.Step.ChoosePrivateType -> R.string.your_private_space
} }
supportActionBar?.let { supportActionBar?.let {
it.title = getString(titleRes) it.title = getString(titleRes)

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.create
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import im.vector.app.R
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.databinding.FragmentSpaceCreateChoosePrivateModelBinding
import javax.inject.Inject
class ChoosePrivateSpaceTypeFragment @Inject constructor(
private val stringProvider: StringProvider
) : VectorBaseFragment<FragmentSpaceCreateChoosePrivateModelBinding>(), OnBackPressed {
private val sharedViewModel: CreateSpaceViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentSpaceCreateChoosePrivateModelBinding.inflate(layoutInflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.justMeButton.setOnClickListener(DebouncedClickListener({
vectorBaseActivity.notImplemented("Organize room as space is not yet implemented")
// sharedViewModel.handle(CreateSpaceAction.SetSpaceTopology(SpaceTopology.JustMe))
}))
views.teammatesButton.setOnClickListener(DebouncedClickListener({
sharedViewModel.handle(CreateSpaceAction.SetSpaceTopology(SpaceTopology.MeAndTeammates))
}))
sharedViewModel.subscribe { state ->
views.accessInfoHelpText.text = stringProvider.getString(R.string.create_spaces_make_sure_access, state.name ?: "")
}
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
sharedViewModel.handle(CreateSpaceAction.OnBackPressed)
return true
}
}

View File

@ -41,8 +41,7 @@ class ChooseSpaceTypeFragment @Inject constructor() : VectorBaseFragment<Fragmen
})) }))
views.privateButton.setOnClickListener(DebouncedClickListener({ views.privateButton.setOnClickListener(DebouncedClickListener({
vectorBaseActivity.notImplemented("Creating private space") sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private))
// sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private))
})) }))
} }
} }

View File

@ -28,4 +28,6 @@ sealed class CreateSpaceAction : VectorViewModelAction {
object NextFromDetails : CreateSpaceAction() object NextFromDetails : CreateSpaceAction()
object NextFromDefaultRooms : CreateSpaceAction() object NextFromDefaultRooms : CreateSpaceAction()
data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction() data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction()
data class SetSpaceTopology(val topology: SpaceTopology) : CreateSpaceAction()
} }

View File

@ -22,6 +22,7 @@ sealed class CreateSpaceEvents : VectorViewEvents {
object NavigateToDetails : CreateSpaceEvents() object NavigateToDetails : CreateSpaceEvents()
object NavigateToChooseType : CreateSpaceEvents() object NavigateToChooseType : CreateSpaceEvents()
object NavigateToAddRooms : CreateSpaceEvents() object NavigateToAddRooms : CreateSpaceEvents()
object NavigateToChoosePrivateType : CreateSpaceEvents()
object Dismiss : CreateSpaceEvents() object Dismiss : CreateSpaceEvents()
data class FinishSuccess(val spaceId: String, val defaultRoomId: String?) : CreateSpaceEvents() data class FinishSuccess(val spaceId: String, val defaultRoomId: String?) : CreateSpaceEvents()
data class ShowModalError(val errorMessage: String) : CreateSpaceEvents() data class ShowModalError(val errorMessage: String) : CreateSpaceEvents()

View File

@ -27,6 +27,7 @@ data class CreateSpaceState(
val topic: String = "", val topic: String = "",
val step: Step = Step.ChooseType, val step: Step = Step.ChooseType,
val spaceType: SpaceType? = null, val spaceType: SpaceType? = null,
val spaceTopology: SpaceTopology? = null,
val nameInlineError: String? = null, val nameInlineError: String? = null,
val defaultRooms: Map<Int, String?>? = null, val defaultRooms: Map<Int, String?>? = null,
val creationResult: Async<String> = Uninitialized val creationResult: Async<String> = Uninitialized
@ -35,6 +36,8 @@ data class CreateSpaceState(
enum class Step { enum class Step {
ChooseType, ChooseType,
SetDetails, SetDetails,
AddRooms AddRooms,
ChoosePrivateType
} }
} }

View File

@ -115,9 +115,34 @@ class CreateSpaceViewModel @AssistedInject constructor(
is CreateSpaceAction.SetAvatar -> { is CreateSpaceAction.SetAvatar -> {
setState { copy(avatarUri = action.uri) } setState { copy(avatarUri = action.uri) }
} }
is CreateSpaceAction.SetSpaceTopology -> {
handleSetTopology(action)
}
}.exhaustive }.exhaustive
} }
private fun handleSetTopology(action: CreateSpaceAction.SetSpaceTopology) {
when (action.topology) {
SpaceTopology.JustMe -> {
setState {
copy(
spaceTopology = SpaceTopology.JustMe
)
}
// XXX finish and open the add rooms directly
}
SpaceTopology.MeAndTeammates -> {
setState {
copy(
spaceTopology = SpaceTopology.MeAndTeammates,
step = CreateSpaceState.Step.AddRooms
)
}
_viewEvents.post(CreateSpaceEvents.NavigateToAddRooms)
}
}
}
private fun handleBackNavigation() = withState { state -> private fun handleBackNavigation() = withState { state ->
when (state.step) { when (state.step) {
CreateSpaceState.Step.ChooseType -> { CreateSpaceState.Step.ChooseType -> {
@ -134,6 +159,24 @@ class CreateSpaceViewModel @AssistedInject constructor(
_viewEvents.post(CreateSpaceEvents.NavigateToChooseType) _viewEvents.post(CreateSpaceEvents.NavigateToChooseType)
} }
CreateSpaceState.Step.AddRooms -> { CreateSpaceState.Step.AddRooms -> {
if (state.spaceType == SpaceType.Private && state.spaceTopology == SpaceTopology.MeAndTeammates) {
setState {
copy(
spaceTopology = null,
step = CreateSpaceState.Step.ChoosePrivateType
)
}
_viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType)
} else {
setState {
copy(
step = CreateSpaceState.Step.SetDetails
)
}
_viewEvents.post(CreateSpaceEvents.NavigateToDetails)
}
}
CreateSpaceState.Step.ChoosePrivateType -> {
setState { setState {
copy( copy(
step = CreateSpaceState.Step.SetDetails step = CreateSpaceState.Step.SetDetails
@ -151,6 +194,14 @@ class CreateSpaceViewModel @AssistedInject constructor(
nameInlineError = stringProvider.getString(R.string.create_space_error_empty_field_space_name) nameInlineError = stringProvider.getString(R.string.create_space_error_empty_field_space_name)
) )
} }
} else {
if (state.spaceType == SpaceType.Private) {
setState {
copy(
step = CreateSpaceState.Step.ChoosePrivateType
)
}
_viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType)
} else { } else {
setState { setState {
copy( copy(
@ -160,6 +211,7 @@ class CreateSpaceViewModel @AssistedInject constructor(
_viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms)
} }
} }
}
private fun handleNextFromDefaultRooms() = withState { state -> private fun handleNextFromDefaultRooms() = withState { state ->
val spaceName = state.name ?: return@withState val spaceName = state.name ?: return@withState

View File

@ -20,6 +20,7 @@ import android.net.Uri
import im.vector.app.core.platform.ViewModelTask import im.vector.app.core.platform.ViewModelTask
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
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
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
@ -59,18 +60,32 @@ class CreateSpaceViewModelTask @Inject constructor(
val childErrors = mutableMapOf<String, Throwable>() val childErrors = mutableMapOf<String, Throwable>()
val childIds = mutableListOf<String>() val childIds = mutableListOf<String>()
if (params.isPublic) {
params.defaultRooms params.defaultRooms
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.forEach { roomName -> .forEach { roomName ->
try { try {
val roomId = try { val roomId = try {
awaitCallback<String> { if (params.isPublic) {
awaitCallback {
session.createRoom(CreateRoomParams().apply { session.createRoom(CreateRoomParams().apply {
this.name = roomName this.name = roomName
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
}, it) }, it)
} }
} else {
awaitCallback { callback ->
session.createRoom(CreateRoomParams().apply {
this.name = roomName
this.joinRuleRestricted = listOf(
RoomJoinRulesAllowEntry(
spaceID = spaceID,
via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList()
)
)
}, callback)
}
}
} catch (timeout: CreateRoomFailure.CreatedWithTimeout) { } catch (timeout: CreateRoomFailure.CreatedWithTimeout) {
// we ignore that? // we ignore that?
timeout.roomID timeout.roomID
@ -90,7 +105,6 @@ class CreateSpaceViewModelTask @Inject constructor(
childErrors[roomName] = failure childErrors[roomName] = failure
} }
} }
}
return if (childErrors.isEmpty()) { return if (childErrors.isEmpty()) {
CreateSpaceTaskResult.Success(spaceID, childIds) CreateSpaceTaskResult.Success(spaceID, childIds)

View File

@ -39,13 +39,27 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
genericFooterItem { genericFooterItem {
id("info_help_header") id("info_help_header")
style(ItemStyle.TITLE) style(ItemStyle.TITLE)
text(stringProvider.getString(R.string.create_spaces_room_public_header, data?.name)) text(
if (data?.spaceType == SpaceType.Public) {
stringProvider.getString(R.string.create_spaces_room_public_header, data.name)
} else {
stringProvider.getString(R.string.create_spaces_room_private_header)
}
)
textColor(colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)) textColor(colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color))
} }
genericFooterItem { genericFooterItem {
id("info_help") id("info_help")
text(stringProvider.getString(R.string.create_spaces_room_public_header_desc)) text(
stringProvider.getString(
if (data?.spaceType == SpaceType.Public) {
R.string.create_spaces_room_public_header_desc
} else {
R.string.create_spaces_room_private_header_desc
}
)
)
textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)) textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary))
} }

View File

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

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17.5911,20.2922C15.9951,21.3704 14.0711,22 12,22C9.7488,22 7.6713,21.2561 6,20.0007C3.5711,18.1763 2,15.2716 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12C22,15.4518 20.2511,18.4951 17.5911,20.2922ZM12,12.5C13.6569,12.5 15,11.0449 15,9.25C15,7.4551 13.6569,6 12,6C10.3431,6 9,7.4551 9,9.25C9,11.0449 10.3431,12.5 12,12.5ZM12,20C14.162,20 16.1236,19.1424 17.5634,17.7488C16.673,15.5506 14.5176,14 12,14C9.4824,14 7.327,15.5506 6.4366,17.7488C7.8763,19.1424 9.838,20 12,20Z"
android:fillColor="#C1C6CD"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M17.5911,20.2922C15.9951,21.3704 14.0711,22 12,22C9.7488,22 7.6713,21.2561 6,20.0007C3.5711,18.1763 2,15.2716 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12C22,15.4518 20.2511,18.4951 17.5911,20.2922ZM12,12.5C13.6569,12.5 15,11.0449 15,9.25C15,7.4551 13.6569,6 12,6C10.3431,6 9,7.4551 9,9.25C9,11.0449 10.3431,12.5 12,12.5ZM12,20C14.162,20 16.1236,19.1424 17.5634,17.7488C16.673,15.5506 14.5176,14 12,14C9.4824,14 7.327,15.5506 6.4366,17.7488C7.8763,19.1424 9.838,20 12,20Z"
android:fillType="evenOdd"/>
<path
android:pathData="M17.5911,20.2922L16.4715,18.6349L17.5911,20.2922ZM6,20.0007L4.7989,21.5999L4.7989,21.5999L6,20.0007ZM17.5634,17.7488L18.9544,19.1859L19.9234,18.2479L19.4171,16.998L17.5634,17.7488ZM6.4366,17.7488L4.5829,16.998L4.0766,18.2479L5.0456,19.1859L6.4366,17.7488ZM12,24C14.4825,24 16.7945,23.244 18.7107,21.9494L16.4715,18.6349C15.1957,19.4968 13.6596,20 12,20V24ZM4.7989,21.5999C6.8046,23.1065 9.3008,24 12,24V20C10.1967,20 8.538,19.4058 7.2011,18.4016L4.7989,21.5999ZM0,12C0,15.9273 1.8887,19.414 4.7989,21.5999L7.2011,18.4016C5.2535,16.9387 4,14.616 4,12H0ZM12,0C5.3726,0 0,5.3726 0,12H4C4,7.5817 7.5817,4 12,4V0ZM24,12C24,5.3726 18.6274,0 12,0V4C16.4183,4 20,7.5817 20,12H24ZM18.7107,21.9494C21.8977,19.7963 24,16.144 24,12H20C20,14.7596 18.6045,17.1939 16.4715,18.6349L18.7107,21.9494ZM13,9.25C13,10.0941 12.4046,10.5 12,10.5V14.5C14.9091,14.5 17,11.9958 17,9.25H13ZM12,8C12.4046,8 13,8.4059 13,9.25H17C17,6.5043 14.9091,4 12,4V8ZM11,9.25C11,8.4059 11.5954,8 12,8V4C9.0909,4 7,6.5043 7,9.25H11ZM12,10.5C11.5954,10.5 11,10.0941 11,9.25H7C7,11.9958 9.0909,14.5 12,14.5V10.5ZM16.1724,16.3118C15.0906,17.3588 13.6223,18 12,18V22C14.7017,22 17.1567,20.926 18.9544,19.1859L16.1724,16.3118ZM12,16C13.6752,16 15.1146,17.0305 15.7097,18.4996L19.4171,16.998C18.2314,14.0707 15.3599,12 12,12V16ZM8.2903,18.4996C8.8854,17.0305 10.3248,16 12,16V12C8.6401,12 5.7686,14.0707 4.5829,16.998L8.2903,18.4996ZM12,18C10.3777,18 8.9094,17.3588 7.8276,16.3118L5.0456,19.1859C6.8433,20.926 9.2983,22 12,22V18Z"
android:fillColor="#C1C6CD"/>
</group>
</vector>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">
<TextView
android:id="@+id/headerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/create_spaces_who_are_you_working_with"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/accessInfoHelpText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/headerText"
tools:text="@string/create_spaces_make_sure_access" />
<im.vector.app.features.spaces.create.WizardButtonView
android:id="@+id/justMeButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:icon="@drawable/ic_user_round"
app:iconTint="?riotx_text_secondary"
app:layout_constraintBottom_toTopOf="@id/teammatesButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:subTitle="@string/create_spaces_organise_rooms"
app:title="@string/create_spaces_just_me" />
<im.vector.app.features.spaces.create.WizardButtonView
android:id="@+id/teammatesButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:icon="@drawable/ic_member_small"
app:iconTint="?riotx_text_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:subTitle="@string/create_spaces_private_teammates"
app:title="@string/create_spaces_me_and_teammates" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -1451,6 +1451,9 @@
<string name="room_settings_room_access_entry_only_invited">Only people who have been invited</string> <string name="room_settings_room_access_entry_only_invited">Only people who have been invited</string>
<string name="room_settings_room_access_entry_anyone_with_link_apart_guest">Anyone who knows the rooms link, apart from guests</string> <string name="room_settings_room_access_entry_anyone_with_link_apart_guest">Anyone who knows the rooms link, apart from guests</string>
<string name="room_settings_room_access_entry_anyone_with_link_including_guest">Anyone who knows the rooms link, including guests</string> <string name="room_settings_room_access_entry_anyone_with_link_including_guest">Anyone who knows the rooms link, including guests</string>
<string name="room_settings_room_access_entry_restricted">Restricted to members of a parent space</string>
<string name="room_settings_room_access_entry_knock">Anyone can knock on the room, members can then accept or reject</string>
<string name="room_settings_room_access_entry_unknown">Unknown access setting (%s)</string>
<!-- Room settings: banned users --> <!-- Room settings: banned users -->
<string name="room_settings_banned_users_title">Banned users</string> <string name="room_settings_banned_users_title">Banned users</string>
@ -3264,10 +3267,17 @@
<string name="add_space">Add Space</string> <string name="add_space">Add Space</string>
<string name="your_public_space">Your public space</string> <string name="your_public_space">Your public space</string>
<string name="your_private_space">Your private space</string>
<string name="create_spaces_type_header">Spaces are a new way to group rooms and people</string> <string name="create_spaces_type_header">Spaces are a new way to group rooms and people</string>
<string name="create_spaces_choose_type_label">What type of space do you want to create?</string> <string name="create_spaces_choose_type_label">What type of space do you want to create?</string>
<string name="create_spaces_you_can_change_later">You can change this later</string> <string name="create_spaces_you_can_change_later">You can change this later</string>
<string name="create_spaces_join_info_help">To join an existing space, you need an invite.</string> <string name="create_spaces_join_info_help">To join an existing space, you need an invite.</string>
<string name="create_spaces_who_are_you_working_with">Who are you working with?</string>
<string name="create_spaces_make_sure_access">Make sure the right people have access to %s. You can change this later.</string>
<string name="create_spaces_just_me">Just me</string>
<string name="create_spaces_organise_rooms">A private space to organise your rooms</string>
<string name="create_spaces_me_and_teammates">Me and teammates</string>
<string name="create_spaces_private_teammates">A private space for you &amp; your teammates</string>
<string name="space_type_public">Public</string> <string name="space_type_public">Public</string>
<string name="space_type_public_desc">Open to anyone, best for communities</string> <string name="space_type_public_desc">Open to anyone, best for communities</string>
<string name="space_type_private">Private</string> <string name="space_type_private">Private</string>
@ -3278,6 +3288,8 @@
<string name="create_space_error_empty_field_space_name">Give it a name to continue.</string> <string name="create_space_error_empty_field_space_name">Give it a name to continue.</string>
<string name="create_spaces_room_public_header">What are some discussions you want to have in %s?</string> <string name="create_spaces_room_public_header">What are some discussions you want to have in %s?</string>
<string name="create_spaces_room_public_header_desc">Well create rooms for them. You can add more later too.</string> <string name="create_spaces_room_public_header_desc">Well create rooms for them. You can add more later too.</string>
<string name="create_spaces_room_private_header">What things are you working on?</string>
<string name="create_spaces_room_private_header_desc">Lets create a room for each of them. You can add more later too, including already existing ones.</string>
<string name="create_spaces_default_public_room_name">General</string> <string name="create_spaces_default_public_room_name">General</string>
<string name="create_spaces_default_public_random_room_name">Random</string> <string name="create_spaces_default_public_random_room_name">Random</string>
<string name="create_spaces_loading_message">Creating Space…</string> <string name="create_spaces_loading_message">Creating Space…</string>