diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index 10c2db4535..98d5196a15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -40,7 +40,48 @@ data class HomeServerCapabilities( */ val roomVersions: RoomVersionCapabilities? = null ) { + + enum class RoomCapabilitySupport { + SUPPORTED, + SUPPORTED_UNSTABLE, + UNSUPPORTED, + UNKNOWN + } + + fun isFeatureSupported(feature: String): RoomCapabilitySupport { + if (roomVersions?.capabilities == null) return RoomCapabilitySupport.UNKNOWN + val info = roomVersions.capabilities[feature] ?: return RoomCapabilitySupport.UNSUPPORTED + + val preferred = info.preferred ?: info.support.lastOrNull() + val versionCap = roomVersions.supportedVersion.firstOrNull { it.version == preferred } + + return when { + versionCap == null -> { + RoomCapabilitySupport.UNKNOWN + } + versionCap.status == RoomVersionStatus.STABLE -> { + RoomCapabilitySupport.SUPPORTED + } + else -> { + RoomCapabilitySupport.SUPPORTED_UNSTABLE + } + } + } + fun isFeatureSupported(feature: String, byRoomVersion: String): Boolean { + if (roomVersions?.capabilities == null) return false + val info = roomVersions.capabilities[feature] ?: return false + + return info.preferred == byRoomVersion || info.support.contains(byRoomVersion) + } + + fun versionOverrideForFeature(feature: String) : String? { + val cap = roomVersions?.capabilities?.get(feature) + return cap?.preferred ?: cap?.support?.lastOrNull() + } + companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L + const val ROOM_CAP_KNOCK = "knock" + const val ROOM_CAP_RESTRICTED = "restricted" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt index 7798b4cc63..20610ca3d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt @@ -16,9 +16,12 @@ package org.matrix.android.sdk.api.session.homeserver +import com.squareup.moshi.JsonClass + data class RoomVersionCapabilities( val defaultRoomVersion: String, - val supportedVersion: List + val supportedVersion: List, + val capabilities: Map? ) data class RoomVersionInfo( @@ -26,6 +29,12 @@ data class RoomVersionInfo( val status: RoomVersionStatus ) +@JsonClass(generateAdapter = true) +data class RoomCapabilitySupport( + val preferred: String?, + val support: List +) + enum class RoomVersionStatus { STABLE, UNSTABLE 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 c46d7d0fd2..5667906000 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,7 +22,6 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM open class CreateRoomParams { @@ -162,7 +161,7 @@ open class CreateRoomParams { var roomVersion: String? = null - var joinRuleRestricted: List? = null + var featurePreset: RoomFeaturePreset? = null companion object { private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt new file mode 100644 index 0000000000..8614582320 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt @@ -0,0 +1,56 @@ +/* + * 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.api.session.room.model.create + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry +import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent + +interface RoomFeaturePreset { + + fun updateRoomParams(params: CreateRoomParams) + + fun setupInitialStates(): List? +} + +class RestrictedRoomPreset(val homeServerCapabilities: HomeServerCapabilities, val restrictedList: List) : RoomFeaturePreset { + + override fun updateRoomParams(params: CreateRoomParams) { + params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED + params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden + params.roomVersion = homeServerCapabilities.versionOverrideForFeature(HomeServerCapabilities.ROOM_CAP_RESTRICTED) + } + + override fun setupInitialStates(): List? { + return listOf( + Event( + type = EventType.STATE_ROOM_JOIN_RULES, + stateKey = "", + content = RoomJoinRulesContent( + _joinRules = RoomJoinRules.RESTRICTED.value, + allowList = restrictedList + ).toContent() + ) + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 7fa48c3da1..d250185d9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import dagger.Lazy import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider @@ -34,7 +35,7 @@ internal interface SendVerificationMessageTask : Task, private val roomAPI: RoomAPI, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val globalErrorReceiver: GlobalErrorReceiver) : SendVerificationMessageTask { @@ -64,7 +65,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) { try { - return encryptEventTask.execute(EncryptEventTask.Params( + return encryptEventTask.get().execute(EncryptEventTask.Params( params.event.roomId ?: "", params.event, listOf("m.relates_to") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 2575cdef26..0a8c6145c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.homeserver.RoomCapabilitySupport import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities import org.matrix.android.sdk.api.session.homeserver.RoomVersionInfo import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus @@ -45,19 +46,26 @@ internal object HomeServerCapabilitiesMapper { roomVersionsJson ?: return null return tryOrNull { - MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let { + MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let { roomVersions -> RoomVersionCapabilities( - defaultRoomVersion = it.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION, - supportedVersion = it.available.entries.map { entry -> - RoomVersionInfo( - version = entry.key, - status = if (entry.value == "stable") { - RoomVersionStatus.STABLE - } else { - RoomVersionStatus.UNSTABLE - } - ) - } + defaultRoomVersion = roomVersions.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION, + supportedVersion = roomVersions.available?.entries?.map { entry -> + RoomVersionInfo(entry.key, RoomVersionStatus.STABLE + .takeIf { entry.value == "stable" } + ?: RoomVersionStatus.UNSTABLE) + }.orEmpty(), + capabilities = roomVersions.roomCapabilities?.entries?.mapNotNull { entry -> + (entry.value as? Map<*, *>)?.let { + val preferred = it["preferred"] as? String ?: return@mapNotNull null + val support = (it["support"] as? List<*>)?.filterIsInstance() + entry.key to RoomCapabilitySupport(preferred, support.orEmpty()) + } + }?.toMap() ?: mapOf( + HomeServerCapabilities.ROOM_CAP_RESTRICTED to RoomCapabilitySupport( + preferred = null, + support = listOf("org.matrix.msc3083") + ) + ) ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index c32c019625..ceaf5876b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker +import timber.log.Timber import javax.inject.Inject internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper, @@ -38,7 +39,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa } // typings are updated through the sync where room summary entity gets updated no matter what, so it's ok get there val typingUsers = typingUsersTracker.getTypingUsers(roomSummaryEntity.roomId) - + Timber.i("[${roomSummaryEntity.displayName ?: "?"}] roomSummaryEntity.flattenParentIds: <${roomSummaryEntity.flattenParentIds?.take(400)}>") return RoomSummary( roomId = roomSummaryEntity.roomId, displayName = roomSummaryEntity.displayName ?: "", @@ -97,7 +98,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC ) }, - flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() + flattenParentIds = roomSummaryEntity.flattenParentIds?.split('|', ignoreCase = false, limit = 100) ?: emptyList() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt index c4bc09a233..1fe4f9d90a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt @@ -70,7 +70,22 @@ internal data class RoomVersions( * Required. A detailed description of the room versions the server supports. */ @Json(name = "available") - val available: JsonDict + val available: JsonDict? = null, + + /** + * "room_capabilities": { + * "knock" : { + * "preferred": "7", + * "support" : ["7"] + * }, + * "restricted" : { + * "preferred": "9", + * "support" : ["8", "9"] + * } + * } + */ + @Json(name = "room_capabilities") + val roomCapabilities: JsonDict? = null ) // The spec says: If not present, the client should assume that password changes are possible via the API 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 2c04759b22..aa047ca7e6 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 @@ -17,16 +17,10 @@ package org.matrix.android.sdk.internal.session.room.create import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.toMedium -import org.matrix.android.sdk.api.session.room.model.GuestAccess -import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomJoinRules -import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.DeviceListManager @@ -45,7 +39,6 @@ import javax.inject.Inject internal class CreateRoomBodyBuilder @Inject constructor( private val ensureIdentityTokenTask: EnsureIdentityTokenTask, - private val crossSigningService: CrossSigningService, private val deviceListManager: DeviceListManager, private val identityStore: IdentityStore, private val fileUploader: FileUploader, @@ -76,19 +69,20 @@ 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), - buildJoinRulesRestricted(params) - ) - + buildCustomInitialStates(params)) + + + params.featurePreset?.updateRoomParams(params) + + val initialStates = ( + listOfNotNull( + buildEncryptionWithAlgorithmEvent(params), + buildHistoryVisibilityEvent(params), + buildAvatarEvent(params), + buildGuestAccess(params) + ) + + params.featurePreset?.setupInitialStates().orEmpty() + + buildCustomInitialStates(params) + ) .takeIf { it.isNotEmpty() } return CreateRoomBody( @@ -158,19 +152,19 @@ internal class CreateRoomBodyBuilder @Inject constructor( } } - 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() - ) - } - } +// private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? { +// return params.joinRuleRestricted +// ?.let { allowList -> +// Event( +// type = EventType.STATE_ROOM_JOIN_RULES, +// stateKey = "", +// content = RoomJoinRulesContent( +// _joinRules = RoomJoinRules.RESTRICTED.value, +// allowList = allowList +// ).toContent() +// ) +// } +// } /** * Add the crypto algorithm to the room creation parameters. diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e71f355a23..df85e31208 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -224,6 +224,7 @@ + { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_private_title), + subtitle = stringProvider.getString(R.string.room_settings_room_access_private_description), + divider = false, + editable = true, + action = { host.listener?.selectVisibility() } + ) + } + RoomJoinRules.PUBLIC -> { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_public_title), + subtitle = stringProvider.getString(R.string.room_settings_room_access_public_description), + divider = false, + editable = true, + action = { host.listener?.selectVisibility() } + ) + } + RoomJoinRules.RESTRICTED -> { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_restricted_title), + subtitle = stringProvider.getString(R.string.room_create_member_of_space_name_can_join, viewState.parentSpaceSummary?.displayName), + divider = false, + editable = true, + action = { host.listener?.selectVisibility() } + ) + } + } + settingsSectionTitleItem { id("settingsSection") titleResId(R.string.create_room_settings_section) } - formSwitchItem { - id("public") - enabled(enableFormElement) - title(host.stringProvider.getString(R.string.create_room_public_title)) - summary(host.stringProvider.getString(R.string.create_room_public_description)) - switchChecked(viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) - listener { value -> - host.listener?.setIsPublic(value) - } - } - if (viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) { + + if (viewState.roomJoinRules == RoomJoinRules.PUBLIC) { // Room alias for public room formEditTextItem { id("alias") enabled(enableFormElement) - value(viewState.roomVisibilityType.aliasLocalPart) + value(viewState.aliasLocalPart) suffixText(":" + viewState.homeServerName) prefixText("#") hint(host.stringProvider.getString(R.string.room_alias_address_hint)) @@ -137,9 +169,10 @@ class CreateRoomController @Inject constructor( } } } - dividerItem { - id("divider1") - } + +// dividerItem { +// id("divider1") +// } formAdvancedToggleItem { id("showAdvanced") title(host.stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced)) @@ -169,7 +202,7 @@ class CreateRoomController @Inject constructor( fun onAvatarChange() fun onNameChange(newName: String) fun onTopicChange(newTopic: String) - fun setIsPublic(isPublic: Boolean) + fun selectVisibility() fun setAliasLocalPart(aliasLocalPart: String) fun setIsEncrypted(isEncrypted: Boolean) fun toggleShowAdvanced() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index cb71d6cd7b..fdc2bd8562 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -40,9 +40,12 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentCreateRoomBinding import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel +import im.vector.app.features.roomprofile.settings.joinrule.toOption import kotlinx.parcelize.Parcelize - import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject @Parcelize @@ -64,6 +67,8 @@ class CreateRoomFragment @Inject constructor( private val viewModel: CreateRoomViewModel by fragmentViewModel() private val args: CreateRoomArgs by args() + private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCreateRoomBinding { @@ -74,6 +79,7 @@ class CreateRoomFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) vectorBaseActivity.setSupportActionBar(views.createRoomToolbar) sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) + setupRoomJoinRuleSharedActionViewModel() setupWaitingView() setupRecyclerView() views.createRoomClose.debouncedClicks { @@ -87,6 +93,16 @@ class CreateRoomFragment @Inject constructor( } } + private fun setupRoomJoinRuleSharedActionViewModel() { + roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) + roomJoinRuleSharedActionViewModel + .observe() + .subscribe { action -> + viewModel.handle(CreateRoomAction.SetVisibility(action.roomJoinRule)) + } + .disposeOnDestroyView() + } + override fun showFailure(throwable: Throwable) { // Note: RoomAliasError are displayed directly in the form if (throwable !is CreateRoomFailure.AliasError) { @@ -130,9 +146,19 @@ class CreateRoomFragment @Inject constructor( viewModel.handle(CreateRoomAction.SetTopic(newTopic)) } - override fun setIsPublic(isPublic: Boolean) { - viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) + override fun selectVisibility() = withState(viewModel) { state -> + + val allowed = if (state.supportsRestricted) { + listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC, RoomJoinRules.RESTRICTED) + } else { + listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC) + } + RoomJoinRuleBottomSheet.newInstance(state.roomJoinRules, allowed.map { it.toOption(false) }) + .show(childFragmentManager, "RoomJoinRuleBottomSheet") } +// override fun setIsPublic(isPublic: Boolean) { +// viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) +// } override fun setAliasLocalPart(aliasLocalPart: String) { viewModel.handle(CreateRoomAction.SetRoomAliasLocalPart(aliasLocalPart)) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index ff62136267..f40829e4b7 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -32,22 +32,28 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities 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.RoomJoinRules +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.api.session.room.model.create.RestrictedRoomPreset import timber.log.Timber class CreateRoomViewModel @AssistedInject constructor(@Assisted private val initialState: CreateRoomViewState, private val session: Session, - private val rawService: RawService + private val rawService: RawService, + private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { @AssistedFactory @@ -58,6 +64,27 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init init { initHomeServerName() initAdminE2eByDefault() + + val restrictedSupport = session.getHomeServerCapabilities().isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED) + val createRestricted = when (restrictedSupport) { + HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED -> true + HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED_UNSTABLE -> vectorPreferences.labsUseExperimentalRestricted() + else -> false + } + + val defaultJoinRules = if (initialState.parentSpaceId != null && createRestricted) { + RoomJoinRules.RESTRICTED + } else { + RoomJoinRules.INVITE + } + + setState { + copy( + supportsRestricted = createRestricted, + roomJoinRules = defaultJoinRules, + parentSpaceSummary = initialState.parentSpaceId?.let { session.getRoomSummary(it) } + ) + } } private fun initHomeServerName() { @@ -80,7 +107,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init setState { copy( - isEncrypted = roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Private && adminE2EByDefault, + isEncrypted = RoomJoinRules.INVITE == roomJoinRules && adminE2EByDefault, hsAdminHasDisabledE2E = !adminE2EByDefault ) } @@ -102,7 +129,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init is CreateRoomAction.SetAvatar -> setAvatar(action) is CreateRoomAction.SetName -> setName(action) is CreateRoomAction.SetTopic -> setTopic(action) - is CreateRoomAction.SetIsPublic -> setIsPublic(action) + is CreateRoomAction.SetVisibility -> setVisibility(action) is CreateRoomAction.SetRoomAliasLocalPart -> setRoomAliasLocalPart(action) is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) is CreateRoomAction.Create -> doCreateRoom() @@ -149,35 +176,45 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) } - private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState { - if (action.isPublic) { - copy( - roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(""), - // Reset any error in the form about alias - asyncCreateRoomRequest = Uninitialized, - isEncrypted = false - ) - } else { - copy( - roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Private, - isEncrypted = adminE2EByDefault - ) + private fun setVisibility(action: CreateRoomAction.SetVisibility) = setState { + when (action.rule) { + RoomJoinRules.PUBLIC -> { + copy( + roomJoinRules = RoomJoinRules.PUBLIC, + // Reset any error in the form about alias + asyncCreateRoomRequest = Uninitialized, + isEncrypted = false + ) + } + RoomJoinRules.RESTRICTED -> { + copy( + roomJoinRules = RoomJoinRules.RESTRICTED, + // Reset any error in the form about alias + asyncCreateRoomRequest = Uninitialized, + isEncrypted = adminE2EByDefault + ) + } +// RoomJoinRules.INVITE, +// RoomJoinRules.KNOCK, +// RoomJoinRules.PRIVATE, + else -> { + // default to invite + copy( + roomJoinRules = RoomJoinRules.INVITE, + isEncrypted = adminE2EByDefault + ) + } } } private fun setRoomAliasLocalPart(action: CreateRoomAction.SetRoomAliasLocalPart) { - withState { state -> - if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) { - setState { - copy( - roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(action.aliasLocalPart), - // Reset any error in the form about alias - asyncCreateRoomRequest = Uninitialized - ) - } - } + setState { + copy( + aliasLocalPart = action.aliasLocalPart, + // Reset any error in the form about alias + asyncCreateRoomRequest = Uninitialized + ) } - // Else ignore } private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) } @@ -187,8 +224,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init return@withState } - if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public - && state.roomVisibilityType.aliasLocalPart.isBlank()) { + if (state.roomJoinRules == RoomJoinRules.PUBLIC && state.aliasLocalPart.isNullOrBlank()) { // we require an alias for public rooms setState { copy(asyncCreateRoomRequest = Fail(CreateRoomFailure.AliasError(RoomAliasError.AliasIsBlank))) @@ -205,15 +241,30 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init name = state.roomName.takeIf { it.isNotBlank() } topic = state.roomTopic.takeIf { it.isNotBlank() } avatarUri = state.avatarUri - when (state.roomVisibilityType) { - is CreateRoomViewState.RoomVisibilityType.Public -> { + when (state.roomJoinRules) { + RoomJoinRules.PUBLIC -> { // Directory visibility visibility = RoomDirectoryVisibility.PUBLIC // Preset preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - roomAliasName = state.roomVisibilityType.aliasLocalPart + roomAliasName = state.aliasLocalPart } - is CreateRoomViewState.RoomVisibilityType.Private -> { + RoomJoinRules.RESTRICTED -> { + state.parentSpaceId?.let { + featurePreset = RestrictedRoomPreset( + session.getHomeServerCapabilities(), + listOf(RoomJoinRulesAllowEntry( + state.parentSpaceId, + listOf(state.homeServerName) + )) + ) + } + } +// RoomJoinRules.KNOCK -> +// RoomJoinRules.PRIVATE -> +// RoomJoinRules.INVITE + else -> { + // by default create invite only // Directory visibility visibility = RoomDirectoryVisibility.PRIVATE // Preset diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index f060e998ad..06742ea690 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -20,20 +20,24 @@ import android.net.Uri import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import org.matrix.android.sdk.api.extensions.orTrue +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import org.matrix.android.sdk.api.session.room.model.RoomSummary data class CreateRoomViewState( val avatarUri: Uri? = null, val roomName: String = "", val roomTopic: String = "", - val roomVisibilityType: RoomVisibilityType = RoomVisibilityType.Private, + val roomJoinRules: RoomJoinRules = RoomJoinRules.INVITE, val isEncrypted: Boolean = false, val showAdvanced: Boolean = false, val disableFederation: Boolean = false, val homeServerName: String = "", val hsAdminHasDisabledE2E: Boolean = false, val asyncCreateRoomRequest: Async = Uninitialized, - val parentSpaceId: String? + val parentSpaceId: String?, + val parentSpaceSummary: RoomSummary? = null, + val supportsRestricted: Boolean = false, + val aliasLocalPart: String? = null ) : MvRxState { constructor(args: CreateRoomArgs) : this( @@ -47,10 +51,5 @@ data class CreateRoomViewState( fun isEmpty() = avatarUri == null && roomName.isEmpty() && roomTopic.isEmpty() - && (roomVisibilityType as? RoomVisibilityType.Public)?.aliasLocalPart?.isEmpty().orTrue() - - sealed class RoomVisibilityType { - object Private : RoomVisibilityType() - data class Public(val aliasLocalPart: String) : RoomVisibilityType() - } + && aliasLocalPart.isNullOrEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 7dec6eaee7..6f0556c972 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -44,7 +44,7 @@ import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel -import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.util.toMatrixItem @@ -179,10 +179,22 @@ class RoomSettingsFragment @Inject constructor( .show(childFragmentManager, "RoomHistoryVisibilityBottomSheet") } - override fun onJoinRuleClicked() = withState(viewModel) { state -> - val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules - RoomJoinRuleBottomSheet.newInstance(currentJoinRule) - .show(childFragmentManager, "RoomJoinRuleBottomSheet") + override fun onJoinRuleClicked() { +// = withState(viewModel) { state -> +// val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules +// val allowedRules = if (state.supportsRestricted) { +// listOf( +// RoomJoinRules.INVITE, RoomJoinRules.PUBLIC, RoomJoinRules.RESTRICTED +// ) +// } else { +// listOf( +// RoomJoinRules.INVITE, RoomJoinRules.PUBLIC +// ) +// } +// RoomJoinRuleBottomSheet.newInstance(currentJoinRule, allowedRules) +// .show(childFragmentManager, "RoomJoinRuleBottomSheet") + + startActivity(RoomJoinRuleActivity.newIntent(requireContext(), roomProfileArgs.roomId)) } override fun onToggleGuestAccess() = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 0a9c32e4c2..1265e7e5ee 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.settings.VectorPreferences import io.reactivex.Completable import io.reactivex.Observable import org.matrix.android.sdk.api.extensions.tryOrNull @@ -34,6 +35,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session 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.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent @@ -44,6 +46,7 @@ import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: RoomSettingsViewState, + private val vectorPreferences: VectorPreferences, private val session: Session) : VectorViewModel(initialState) { @@ -73,6 +76,24 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: observeGuestAccess() observeRoomAvatar() observeState() + + val homeServerCapabilities = session.getHomeServerCapabilities() + val canUseRestricted = homeServerCapabilities + .isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED, room.getRoomVersion()) + + val restrictedSupport = homeServerCapabilities.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED) + val couldUpgradeToRestricted = when (restrictedSupport) { + HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED -> true + HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED_UNSTABLE -> vectorPreferences.labsUseExperimentalRestricted() + else -> false + } + + setState { + copy( + supportsRestricted = canUseRestricted, + canUpgradeToRestricted = couldUpgradeToRestricted + ) + } } private fun observeState() { @@ -247,8 +268,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: val summary = state.roomSummary.invoke() when (val avatarAction = state.avatarAction) { - RoomSettingsViewState.AvatarAction.None -> Unit - RoomSettingsViewState.AvatarAction.DeleteAvatar -> { + RoomSettingsViewState.AvatarAction.None -> Unit + RoomSettingsViewState.AvatarAction.DeleteAvatar -> { operationList.add(room.rx().deleteAvatar()) } is RoomSettingsViewState.AvatarAction.UpdateAvatar -> { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index e220687878..403836b268 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -43,7 +43,9 @@ data class RoomSettingsViewState( val newHistoryVisibility: RoomHistoryVisibility? = null, val newRoomJoinRules: NewJoinRule = NewJoinRule(), val showSaveAction: Boolean = false, - val actionPermissions: ActionPermissions = ActionPermissions() + val actionPermissions: ActionPermissions = ActionPermissions(), + val supportsRestricted: Boolean = false, + val canUpgradeToRestricted: Boolean = false ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt new file mode 100644 index 0000000000..f152352330 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt @@ -0,0 +1,69 @@ +/* + * 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.roomprofile.settings.joinrule + +import android.content.Context +import android.content.Intent +import com.airbnb.mvrx.MvRx +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel + +class RoomJoinRuleActivity : VectorBaseActivity(), + RoomJoinRuleChooseRestrictedViewModel.Factory { + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + private lateinit var roomProfileArgs: RoomProfileArgs + + private lateinit var allowListViewModelFactory: RoomJoinRuleChooseRestrictedViewModel.Factory + + override fun create(initialState: RoomJoinRuleChooseRestrictedState) = allowListViewModelFactory.create(initialState) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun initUiAndData() { + roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return + if (isFirstCreation()) { + addFragment( + R.id.simpleFragmentContainer, + RoomJoinRuleFragment::class.java, + roomProfileArgs + ) + } + } +// +// override fun onBackPressed() { +// super.onBackPressed() +// } + + companion object { + + fun newIntent(context: Context, roomId: String): Intent { + val roomProfileArgs = RoomProfileArgs(roomId) + return Intent(context, RoomJoinRuleActivity::class.java).apply { + putExtra(MvRx.KEY_ARG, roomProfileArgs) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt new file mode 100644 index 0000000000..7962023dd5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt @@ -0,0 +1,107 @@ +/* + * 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.roomprofile.settings.joinrule + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.ItemStyle +import im.vector.app.core.ui.list.genericButtonItem +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +class RoomJoinRuleAdvancedController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val avatarRenderer: AvatarRenderer +) : TypedEpoxyController() { + + interface InteractionListener { + fun didSelectRule(rules: RoomJoinRules) + } + + var interactionListener: InteractionListener? = null + + override fun buildModels(state: RoomJoinRuleChooseRestrictedState?) { + state ?: return + val choices = state.choices ?: return + + val host = this + + genericFooterItem { + id("header") + text(host.stringProvider.getString(R.string.room_settings_room_access_title)) + centered(false) + style(ItemStyle.TITLE) + textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) + } + + genericFooterItem { + id("desc") + text(host.stringProvider.getString(R.string.decide_who_can_find_and_join)) + centered(false) + } + + // invite only + RoomJoinRuleRadioAction( + roomJoinRule = RoomJoinRules.INVITE, + description = stringProvider.getString(R.string.room_settings_room_access_private_description), + title = stringProvider.getString(R.string.room_settings_room_access_private_invite_only_title), + isSelected = state.currentRoomJoinRules == RoomJoinRules.INVITE + ).toRadioBottomSheetItem().let { + it.listener { + interactionListener?.didSelectRule(RoomJoinRules.INVITE) +// listener?.didSelectAction(action) + } + add(it) + } + + if (choices.firstOrNull { it.rule == RoomJoinRules.RESTRICTED } != null) { + val restrictedRule = choices.first { it.rule == RoomJoinRules.RESTRICTED } + spaceJoinRuleItem { + id("restricted") + avatarRenderer(host.avatarRenderer) + needUpgrade(restrictedRule.needUpgrade) + selected(state.currentRoomJoinRules == RoomJoinRules.RESTRICTED) + restrictedList(state.updatedAllowList.orEmpty()) + listener { host.interactionListener?.didSelectRule(RoomJoinRules.RESTRICTED) } + } + } + + // Public + RoomJoinRuleRadioAction( + roomJoinRule = RoomJoinRules.PUBLIC, + description = stringProvider.getString(R.string.room_settings_room_access_public_description), + title = stringProvider.getString(R.string.room_settings_room_access_public_title), + isSelected = state.currentRoomJoinRules == RoomJoinRules.PUBLIC + ).toRadioBottomSheetItem().let { + it.listener { + interactionListener?.didSelectRule(RoomJoinRules.PUBLIC) + } + add(it) + } + + genericButtonItem { + id("save") + text("") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedViewModel.kt new file mode 100644 index 0000000000..bf74d4f45e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedViewModel.kt @@ -0,0 +1,191 @@ +/* + * 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.roomprofile.settings.joinrule + +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +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.homeserver.HomeServerCapabilities +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.RoomSummary +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.rx.mapOptional +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +data class RoomJoinRuleAdvancedState( + val roomId: String, + val summary: RoomSummary? = null, + val currentRoomJoinRules: RoomJoinRules? = null, + val initialAllowList: List? = null, + val updatedAllowList: List? = null, + val choices: Async> = Uninitialized +) : MvRxState { + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) +} + +sealed class RoomJoinRuleAdvancedAction : VectorViewModelAction { + data class SelectJoinRules(val rules: RoomJoinRules) : RoomJoinRuleAdvancedAction() + data class UpdateAllowList(val roomIds: List) : RoomJoinRuleAdvancedAction() +} + +sealed class RoomJoinRuleAdvancedEvents : VectorViewEvents { + object SelectAllowList : RoomJoinRuleAdvancedEvents() +} + +class RoomJoinRuleAdvancedViewModel @AssistedInject constructor( + @Assisted val initialState: RoomJoinRuleAdvancedState, + private val session: Session, + private val vectorPreferences: VectorPreferences +) : VectorViewModel(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + private val homeServerCapabilities = session.getHomeServerCapabilities() + + @AssistedFactory + interface Factory { + fun create(initialState: RoomJoinRuleAdvancedState): RoomJoinRuleAdvancedViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: RoomJoinRuleAdvancedState): RoomJoinRuleAdvancedViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + + val initialAllowList = session.getRoom(initialState.roomId)?.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + ?.content + ?.toModel() + ?.allowList + + setState { + val initialAllowItems = initialAllowList.orEmpty().map { + session.getRoomSummary(it.spaceID)?.toMatrixItem() + ?: MatrixItem.RoomItem(it.spaceID, null, null) + } + copy( + summary = session.getRoomSummary(initialState.roomId), + initialAllowList = initialAllowItems, + updatedAllowList = initialAllowItems + ) + } + + // TODO shouldn't be live + room.rx() + .liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition) + .mapOptional { it.content.toModel() } + .unwrap() + .subscribe { content -> + + content.joinRules?.let { + var safeRule: RoomJoinRules = it + // server is not really checking that, just to be sure let's check + val restrictedSupportedByThisVersion = homeServerCapabilities + .isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED, room.getRoomVersion()) + if (it == RoomJoinRules.RESTRICTED + && !restrictedSupportedByThisVersion) { + safeRule = RoomJoinRules.INVITE + } + val allowList = if (safeRule == RoomJoinRules.RESTRICTED) content.allowList else null + + val restrictedSupport = homeServerCapabilities.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED) + val couldUpgradeToRestricted = when (restrictedSupport) { + HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED -> true + HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED_UNSTABLE -> vectorPreferences.labsUseExperimentalRestricted() + else -> false + } + + val choices = if (restrictedSupportedByThisVersion || couldUpgradeToRestricted) { + listOf( + RoomJoinRules.INVITE.toOption(false), + RoomJoinRules.RESTRICTED.toOption(!restrictedSupportedByThisVersion), + RoomJoinRules.PUBLIC.toOption(false) + ) + } else { + listOf( + RoomJoinRules.INVITE.toOption(false), + RoomJoinRules.PUBLIC.toOption(false) + ) + } + + setState { + copy( + currentRoomJoinRules = safeRule, + choices = Success(choices) + ) + } + } + } + .disposeOnClear() + } + + override fun handle(action: RoomJoinRuleAdvancedAction) { + when (action) { + is RoomJoinRuleAdvancedAction.SelectJoinRules -> handleSelectRule(action) + is RoomJoinRuleAdvancedAction.UpdateAllowList -> handleUpdateAllowList(action) + } + } + + fun handleUpdateAllowList(action: RoomJoinRuleAdvancedAction.UpdateAllowList) = withState { state -> + setState { + copy( + updatedAllowList = action.roomIds.map { + session.getRoomSummary(it)?.toMatrixItem() ?: MatrixItem.RoomItem(it, null, null) + } + ) + } + } + + fun handleSelectRule(action: RoomJoinRuleAdvancedAction.SelectJoinRules) = withState { state -> + + if (action.rules == RoomJoinRules.RESTRICTED) { + // open space select? + _viewEvents.post(RoomJoinRuleAdvancedEvents.SelectAllowList) + } + setState { + copy( + currentRoomJoinRules = action.rules + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt index d2e338d077..f4c7eecf8f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleBottomSheet.kt @@ -28,9 +28,18 @@ import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject +@Parcelize +data class JoinRulesOptionSupport( + val rule: RoomJoinRules, + val needUpgrade: Boolean = false +) : Parcelable + +fun RoomJoinRules.toOption(needUpgrade: Boolean) = JoinRulesOptionSupport(this, needUpgrade) + @Parcelize data class RoomJoinRuleBottomSheetArgs( - val currentRoomJoinRule: RoomJoinRules + val currentRoomJoinRule: RoomJoinRules, + val allowedJoinedRules: List ) : Parcelable class RoomJoinRuleBottomSheet : BottomSheetGeneric() { @@ -61,9 +70,15 @@ class RoomJoinRuleBottomSheet : BottomSheetGeneric = listOf( + RoomJoinRules.INVITE, RoomJoinRules.PUBLIC + ).map { it.toOption(true) } + ): RoomJoinRuleBottomSheet { return RoomJoinRuleBottomSheet().apply { - setArguments(RoomJoinRuleBottomSheetArgs(currentRoomJoinRule)) + setArguments( + RoomJoinRuleBottomSheetArgs(currentRoomJoinRule, allowedJoinedRules) + ) } } } 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 a438e8deee..edeb6e1099 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,7 +51,7 @@ class RoomJoinRuleController @Inject constructor( description = stringProvider.getString(R.string.room_settings_room_access_restricted_description), title = span { +stringProvider.getString(R.string.room_settings_room_access_restricted_title) - + " " + +" " image( drawableProvider.getDrawable(R.drawable.ic_beta_pill)!!, "bottom" @@ -59,6 +59,6 @@ class RoomJoinRuleController @Inject constructor( }, isSelected = state.currentRoomJoinRule == RoomJoinRules.RESTRICTED ) - ) + ).filter { state.allowedJoinedRules.map { it.rule }.contains(it.roomJoinRule) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleFragment.kt new file mode 100644 index 0000000000..7ad78eeb9a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleFragment.kt @@ -0,0 +1,101 @@ +/* + * 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.roomprofile.settings.joinrule + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.OnBackPressed +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentJoinRulesRecyclerBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import javax.inject.Inject + +class RoomJoinRuleFragment @Inject constructor( + val controller: RoomJoinRuleAdvancedController, +// val viewModelFactory: RoomJoinRuleAdvancedViewModel.Factory, + val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), +// RoomJoinRuleAdvancedViewModel.Factory, + OnBackPressed, RoomJoinRuleAdvancedController.InteractionListener { + + private val viewModel: RoomJoinRuleChooseRestrictedViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentJoinRulesRecyclerBinding.inflate(inflater, container, false) + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + // TODO + requireActivity().finish() + return true + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + controller.setData(state) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) +// roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) +// setupRoomHistoryVisibilitySharedActionViewModel() +// setupRoomJoinRuleSharedActionViewModel() +// controller.callback = this + views.genericRecyclerView.configureWith(controller, hasFixedSize = true) + controller.interactionListener = this +// views.waitingView.waitingStatusText.setText(R.string.please_wait) +// views.waitingView.waitingStatusText.isVisible = true + +// // Use the Kotlin extension in the fragment-ktx artifact +// setFragmentResultListener("SelectAllowList") { requestKey, bundle -> +// // We use a String here, but any type that can be put in a Bundle is supported +// bundle.getStringArrayList("bundleKey")?.toList()?.let { +// viewModel.handle(RoomJoinRuleAdvancedAction.UpdateAllowList(it)) +// } +// } + + viewModel.observeViewEvents { + when (it) { + RoomJoinRuleAdvancedEvents.SelectAllowList -> { + parentFragmentManager.commitTransaction { + setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + val tag = RoomJoinRuleChooseRestrictedFragment::class.simpleName + replace(R.id.simpleFragmentContainer, + RoomJoinRuleChooseRestrictedFragment::class.java, + this@RoomJoinRuleFragment.arguments, + tag + ).addToBackStack(tag) + } + } + } + } + } + + override fun create(initialState: RoomJoinRuleAdvancedState) = viewModelFactory.create(initialState) + + override fun didSelectRule(rules: RoomJoinRules) { + viewModel.handle(RoomJoinRuleAdvancedAction.SelectJoinRules(rules)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt index 85253091c4..dd818a4631 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleState.kt @@ -22,10 +22,13 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules data class RoomJoinRuleState( val currentRoomJoinRule: RoomJoinRules = RoomJoinRules.INVITE, + val allowedJoinedRules: List = + listOf(RoomJoinRules.INVITE, RoomJoinRules.PUBLIC).map { it.toOption(true) }, val currentGuestAccess: GuestAccess? = null ) : BottomSheetGenericState() { constructor(args: RoomJoinRuleBottomSheetArgs) : this( - currentRoomJoinRule = args.currentRoomJoinRule + currentRoomJoinRule = args.currentRoomJoinRule, + allowedJoinedRules = args.allowedJoinedRules ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt new file mode 100644 index 0000000000..261796c612 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.kt @@ -0,0 +1,99 @@ +/* + * 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.roomprofile.settings.joinrule + +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_bottom_sheet_joinrule_restricted) +abstract class SpaceJoinRuleItem : VectorEpoxyModel() { + + @EpoxyAttribute + var selected: Boolean = false + + @EpoxyAttribute + var needUpgrade: Boolean = false + + @EpoxyAttribute + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var restrictedList: List = emptyList() + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var listener: ClickListener + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.view.onClick(listener) + + if (selected) { + holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on)) + holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked) + } else { + holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off)) + holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked) + } + + holder.upgradeRequiredButton.isVisible = needUpgrade + holder.helperText.isVisible = selected + + val items = listOf(holder.space1, holder.space2, holder.space3, holder.space4, holder.space5) + holder.spaceMore.isVisible = false + if (restrictedList.isEmpty()) { + holder.listTitle.isVisible = false + items.onEach { it.isVisible = false } + } else { + holder.listTitle.isVisible = true + restrictedList.forEachIndexed { index, matrixItem -> + if (index < items.size) { + avatarRenderer.render(matrixItem, items[index]) + } else if (index == items.size) { + holder.spaceMore.isVisible = true + } + } + } + } + + class Holder : VectorEpoxyHolder() { + val radioImage by bind(R.id.radioIcon) + val actionTitle by bind(R.id.actionTitle) + val actionDescription by bind(R.id.actionDescription) + val upgradeRequiredButton by bind + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f421e451a9..5f4ec301ab 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1475,11 +1475,21 @@ Anyone can knock on the room, members can then accept or reject Unknown access setting (%s) Private + Private (Invite Only) Only people invited can find and join Public Anyone can find the room and join - Spaces + Space members only Anyone in a space with this room can find and join it. Only admins of this room can add it to a space. + Members of Space %s can find, preview and join. + Allow space members to find and access. + Spaces which can access + Decide which spaces can access this room. If a space is selected its members will be able to find and join Room name. + Select spaces + Tap to edit spaces + Decide who can find and join this room. + Space you know that contain this room + Other spaces or rooms you might not know Banned users @@ -3435,6 +3445,7 @@ Please be patient, it may take some time. Upgrade + Upgrade Required Upgrade public room Upgrade private room Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.\nThis usually only affects how the room is processed on the server. @@ -3442,6 +3453,7 @@ Automatically invite users Automatically update space parent You need permission to upgrade a room + Allow anyone in %s to find and access. You can select other spaces too. This room is running room version %s, which this homeserver has marked as unstable. Upgrade to the recommended room version