From 79bde6ee912be443fa8afb5e76906e9f8e8bdbe3 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 6 Apr 2021 15:26:43 +0200 Subject: [PATCH] Private (me and teamates) space support --- .../room/model/RoomGuestAccessContent.kt | 6 +- .../api/session/room/model/RoomJoinRules.kt | 11 ++-- .../room/model/RoomJoinRulesAllowEntry.kt | 33 ++++++++++ .../room/model/RoomJoinRulesContent.kt | 17 +++-- .../room/model/create/CreateRoomParams.kt | 8 +++ .../session/room/create/CreateRoomBody.kt | 10 ++- .../room/create/CreateRoomBodyBuilder.kt | 32 +++++++++- .../session/space/DefaultSpaceService.kt | 7 +- .../im/vector/app/core/di/FragmentModule.kt | 6 ++ .../settings/RoomSettingsController.kt | 42 +++++++++--- .../joinrule/RoomJoinRuleController.kt | 7 ++ .../features/spaces/SpaceCreationActivity.kt | 13 +++- .../create/ChoosePrivateSpaceTypeFragment.kt | 63 ++++++++++++++++++ .../spaces/create/ChooseSpaceTypeFragment.kt | 3 +- .../spaces/create/CreateSpaceAction.kt | 2 + .../spaces/create/CreateSpaceEvents.kt | 1 + .../spaces/create/CreateSpaceState.kt | 5 +- .../spaces/create/CreateSpaceViewModel.kt | 64 +++++++++++++++++-- .../spaces/create/CreateSpaceViewModelTask.kt | 62 +++++++++++------- .../create/SpaceDefaultRoomEpoxyController.kt | 18 +++++- .../features/spaces/create/SpaceTopology.kt | 22 +++++++ .../src/main/res/drawable/ic_user_round.xml | 18 ++++++ ...ment_space_create_choose_private_model.xml | 64 +++++++++++++++++++ vector/src/main/res/values/strings.xml | 12 ++++ 24 files changed, 461 insertions(+), 65 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/SpaceTopology.kt create mode 100644 vector/src/main/res/drawable/ic_user_round.xml create mode 100644 vector/src/main/res/layout/fragment_space_create_choose_private_model.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt index 0760c6f1b4..020e7ed39e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt @@ -40,7 +40,7 @@ data class RoomGuestAccessContent( } @JsonClass(generateAdapter = false) -enum class GuestAccess { - @Json(name = "can_join") CanJoin, - @Json(name = "forbidden") Forbidden +enum class GuestAccess(val value: String) { + @Json(name = "can_join") CanJoin("can_join"), + @Json(name = "forbidden") Forbidden("forbidden") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt index f3e8d357f3..a86301a276 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt @@ -24,9 +24,10 @@ import com.squareup.moshi.JsonClass * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules */ @JsonClass(generateAdapter = false) -enum class RoomJoinRules { - @Json(name = "public") PUBLIC, - @Json(name = "invite") INVITE, - @Json(name = "knock") KNOCK, - @Json(name = "private") PRIVATE +enum class RoomJoinRules(val value: String) { + @Json(name = "public") PUBLIC("public"), + @Json(name = "invite") INVITE("invite"), + @Json(name = "knock") KNOCK("knock"), + @Json(name = "private") PRIVATE("private"), + @Json(name = "restricted") RESTRICTED("restricted") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt new file mode 100644 index 0000000000..7b87bc34d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt index 8082486b22..145e6039f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt @@ -1,5 +1,6 @@ /* * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,16 +27,22 @@ import timber.log.Timber */ @JsonClass(generateAdapter = true) data class RoomJoinRulesContent( - @Json(name = "join_rule") val _joinRules: String? = null + @Json(name = "join_rule") val _joinRules: String? = null, + /** + * If the allow key is an empty list (or not a list at all), then the room reverts to standard public join rules + */ + @Json(name = "allow") val allowList:List? = null ) { val joinRules: RoomJoinRules? = when (_joinRules) { - "public" -> RoomJoinRules.PUBLIC - "invite" -> RoomJoinRules.INVITE - "knock" -> RoomJoinRules.KNOCK + "public" -> RoomJoinRules.PUBLIC + "invite" -> RoomJoinRules.INVITE + "knock" -> RoomJoinRules.KNOCK "private" -> RoomJoinRules.PRIVATE - else -> { + "restricted" -> RoomJoinRules.RESTRICTED + else -> { Timber.w("Invalid value for RoomJoinRules: `$_joinRules`") null } } } + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index 82c6e187f8..9ac7d412af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -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.RoomDirectoryVisibility 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 // TODO Give a way to include other initial states @@ -153,8 +155,14 @@ open class CreateRoomParams { algorithm = MXCRYPTO_ALGORITHM_MEGOLM } + + var roomVersion: String? = null + + var joinRuleRestricted: List? = null + companion object { private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate" private const val CREATION_CONTENT_KEY_ROOM_TYPE = "org.matrix.msc1772.type" } } + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt index 13d403e2e4..7d80284f70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt @@ -111,5 +111,13 @@ internal data class CreateRoomBody( * The power level content to override in the default power level event */ @Json(name = "power_level_content_override") - val powerLevelContentOverride: PowerLevelsContent? + val powerLevelContentOverride: PowerLevelsContent?, + + /** + * The room version to set for the room. If not provided, the homeserver is to use its configured default. + * If provided, the homeserver will return a 400 error with the errcode M_UNSUPPORTED_ROOM_VERSION if it does not support the room version. + */ + @Json(name = "room_version") + val roomVersion: String? ) + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 6332af932d..49a3ffac5a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -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.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.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.util.MimeTypes 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( buildEncryptionWithAlgorithmEvent(params), buildHistoryVisibilityEvent(params), buildAvatarEvent(params), - buildGuestAccess(params) + buildGuestAccess(params), + buildJoinRulesRestricted(params) ) .takeIf { it.isNotEmpty() } @@ -92,7 +102,9 @@ internal class CreateRoomBodyBuilder @Inject constructor( initialStates = initialStates, preset = params.preset, isDirect = params.isDirect, - powerLevelContentOverride = params.powerLevelContentOverride + powerLevelContentOverride = params.powerLevelContentOverride, + roomVersion = params.roomVersion + ) } @@ -132,7 +144,21 @@ internal class CreateRoomBodyBuilder @Inject constructor( Event( type = EventType.STATE_ROOM_GUEST_ACCESS, 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() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 892cfbbd0e..ba063df375 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -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.Membership import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo @@ -67,14 +68,18 @@ internal class DefaultSpaceService @Inject constructor( 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 if (isPublic) { this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( invite = 0 ) + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE this.guestAccess = GuestAccess.CanJoin + } else { + this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT + visibility = RoomDirectoryVisibility.PRIVATE + enableEncryption() } }) } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index ea7835cc28..8900241b88 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -120,6 +120,7 @@ import im.vector.app.features.settings.threepids.ThreePidsSettingsFragment import im.vector.app.features.share.IncomingShareFragment import im.vector.app.features.signout.soft.SoftLogoutFragment import im.vector.app.features.spaces.SpaceListFragment +import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment @@ -672,4 +673,9 @@ interface FragmentModule { @IntoMap @FragmentKey(SpaceDirectoryFragment::class) fun bindSpaceDirectoryFragment(fragment: SpaceDirectoryFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(ChoosePrivateSpaceTypeFragment::class) + fun bindChoosePrivateSpaceTypeFragment(fragment: ChoosePrivateSpaceTypeFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 6e77ceaa36..755dcc4a25 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -63,13 +63,13 @@ class RoomSettingsController @Inject constructor( id("avatar") enabled(data.actionPermissions.canChangeAvatar) when (val avatarAction = data.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> { + RoomSettingsViewState.AvatarAction.None -> { // Use the current value avatarRenderer(avatarRenderer) // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) } - RoomSettingsViewState.AvatarAction.DeleteAvatar -> + RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null) is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri) @@ -128,14 +128,36 @@ class RoomSettingsController @Inject constructor( private fun RoomSettingsViewState.getJoinRuleWording(): String { val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules val guestAccess = newRoomJoinRules.newGuestAccess ?: currentGuestAccess - 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 + val resId = when (joinRule) { + RoomJoinRules.INVITE -> { + 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) { + R.string.room_settings_room_access_entry_anyone_with_link_including_guest to null + } else { + 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 +// } +// }) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt index ab00396dbe..4e94fa426b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleController.kt @@ -51,6 +51,13 @@ class RoomJoinRuleController @Inject constructor( title = stringProvider.getString(R.string.room_settings_room_access_entry_anyone_with_link_including_guest), iconResId = 0, 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 ) ) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt index 608c9b5d58..47083aa12b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -28,6 +28,7 @@ import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.toMvRxBundle 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.CreateSpaceAction 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.CreateSpaceState import im.vector.app.features.spaces.create.CreateSpaceViewModel +import im.vector.app.features.spaces.create.SpaceType import javax.inject.Inject class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Factory { @@ -85,6 +87,9 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac CreateSpaceEvents.NavigateToAddRooms -> { navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) } + CreateSpaceEvents.NavigateToChoosePrivateType -> { + navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java) + } is CreateSpaceEvents.ShowModalError -> { hideWaitingView() AlertDialog.Builder(this) @@ -124,8 +129,12 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac private fun renderState(state: CreateSpaceState) { val titleRes = when (state.step) { CreateSpaceState.Step.ChooseType -> R.string.activity_create_space_title - CreateSpaceState.Step.SetDetails -> R.string.your_public_space - CreateSpaceState.Step.AddRooms -> R.string.your_public_space + CreateSpaceState.Step.SetDetails, + 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 { it.title = getString(titleRes) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt new file mode 100644 index 0000000000..23a8ce1a57 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChoosePrivateSpaceTypeFragment.kt @@ -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(), 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 + } +} + diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt index 7ff13c06fd..51f1821b38 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt @@ -41,8 +41,7 @@ class ChooseSpaceTypeFragment @Inject constructor() : VectorBaseFragment? = null, val creationResult: Async = Uninitialized @@ -35,6 +36,8 @@ data class CreateSpaceState( enum class Step { ChooseType, SetDetails, - AddRooms + AddRooms, + ChoosePrivateType } + } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index ed16829c06..e61707e6a2 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -115,9 +115,34 @@ class CreateSpaceViewModel @AssistedInject constructor( is CreateSpaceAction.SetAvatar -> { setState { copy(avatarUri = action.uri) } } + is CreateSpaceAction.SetSpaceTopology -> { + handleSetTopology(action) + } }.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 -> when (state.step) { CreateSpaceState.Step.ChooseType -> { @@ -134,6 +159,24 @@ class CreateSpaceViewModel @AssistedInject constructor( _viewEvents.post(CreateSpaceEvents.NavigateToChooseType) } 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 { copy( step = CreateSpaceState.Step.SetDetails @@ -152,12 +195,21 @@ class CreateSpaceViewModel @AssistedInject constructor( ) } } else { - setState { - copy( - step = CreateSpaceState.Step.AddRooms - ) + if (state.spaceType == SpaceType.Private) { + setState { + copy( + step = CreateSpaceState.Step.ChoosePrivateType + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType) + } else { + setState { + copy( + step = CreateSpaceState.Step.AddRooms + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) } - _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) } } @@ -187,7 +239,7 @@ class CreateSpaceViewModel @AssistedInject constructor( } _viewEvents.post(CreateSpaceEvents.FinishSuccess(result.spaceId, result.childIds.firstOrNull())) } - is CreateSpaceTaskResult.PartialSuccess -> { + is CreateSpaceTaskResult.PartialSuccess -> { // XXX what can we do here? setState { copy(creationResult = Success(result.spaceId)) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt index df4b8a8cfe..35bec2b571 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt @@ -20,6 +20,7 @@ import android.net.Uri import im.vector.app.core.platform.ViewModelTask 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.model.RoomJoinRulesAllowEntry 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.internal.util.awaitCallback @@ -59,38 +60,51 @@ class CreateSpaceViewModelTask @Inject constructor( val childErrors = mutableMapOf() val childIds = mutableListOf() - if (params.isPublic) { - params.defaultRooms - .filter { it.isNotBlank() } - .forEach { roomName -> - try { - val roomId = try { - awaitCallback { + params.defaultRooms + .filter { it.isNotBlank() } + .forEach { roomName -> + try { + + val roomId = try { + if (params.isPublic) { + awaitCallback { session.createRoom(CreateRoomParams().apply { this.name = roomName this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT }, it) } - } catch (timeout: CreateRoomFailure.CreatedWithTimeout) { - // we ignore that? - timeout.roomID + } 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) + } } - val via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList() - createdSpace!!.addChildren(roomId, via, null, autoJoin = false, suggested = true) - // set canonical - session.spaceService().setSpaceParent( - roomId, - createdSpace.spaceId, - true, - via - ) - childIds.add(roomId) - } catch (failure: Throwable) { - Timber.d("Space: Failed to create child room in $spaceID") - childErrors[roomName] = failure + } catch (timeout: CreateRoomFailure.CreatedWithTimeout) { + // we ignore that? + timeout.roomID } + val via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList() + createdSpace!!.addChildren(roomId, via, null, autoJoin = false, suggested = true) + // set canonical + session.spaceService().setSpaceParent( + roomId, + createdSpace.spaceId, + true, + via + ) + childIds.add(roomId) + } catch (failure: Throwable) { + Timber.d("Space: Failed to create child room in $spaceID") + childErrors[roomName] = failure } - } + } return if (childErrors.isEmpty()) { CreateSpaceTaskResult.Success(spaceID, childIds) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt index 9a17a8272e..8a87ff3473 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -39,13 +39,27 @@ class SpaceDefaultRoomEpoxyController @Inject constructor( genericFooterItem { id("info_help_header") 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)) } genericFooterItem { 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)) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceTopology.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceTopology.kt new file mode 100644 index 0000000000..800630b52e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceTopology.kt @@ -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 +} diff --git a/vector/src/main/res/drawable/ic_user_round.xml b/vector/src/main/res/drawable/ic_user_round.xml new file mode 100644 index 0000000000..721f3aa81c --- /dev/null +++ b/vector/src/main/res/drawable/ic_user_round.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml b/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml new file mode 100644 index 0000000000..1da9d1951d --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f8e0621a5d..0566fbbacb 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1451,6 +1451,9 @@ Only people who have been invited Anyone who knows the room’s link, apart from guests Anyone who knows the room’s link, including guests + Restricted to members of a parent space + Anyone can knock on the room, members can then accept or reject + Unknown access setting (%s) Banned users @@ -3264,10 +3267,17 @@ Add Space Your public space + Your private space Spaces are a new way to group rooms and people What type of space do you want to create? You can change this later To join an existing space, you need an invite. + Who are you working with? + Make sure the right people have access to %s. You can change this later. + Just me + A private space to organise your rooms + Me and teammates + A private space for you & your teammates Public Open to anyone, best for communities Private @@ -3278,6 +3288,8 @@ Give it a name to continue. What are some discussions you want to have in %s? We’ll create rooms for them. You can add more later too. + What things are you working on? + Let’s create a room for each of them. You can add more later too, including already existing ones. General Random Creating Space…