diff --git a/changelog.d/3509.feature b/changelog.d/3509.feature new file mode 100644 index 0000000000..82c8bc6a39 --- /dev/null +++ b/changelog.d/3509.feature @@ -0,0 +1 @@ +Spaces - Support Restricted Room via room capabilities API \ No newline at end of file diff --git a/changelog.d/3665.feature b/changelog.d/3665.feature new file mode 100644 index 0000000000..2195c761b4 --- /dev/null +++ b/changelog.d/3665.feature @@ -0,0 +1 @@ +Spaces | Support restricted room access in room settings \ No newline at end of file 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..b49236c338 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,63 @@ data class HomeServerCapabilities( */ val roomVersions: RoomVersionCapabilities? = null ) { + + enum class RoomCapabilitySupport { + SUPPORTED, + SUPPORTED_UNSTABLE, + UNSUPPORTED, + UNKNOWN + } + + /** + * Check if a feature is supported by the homeserver. + * @return + * UNKNOWN if the server does not implement room caps + * UNSUPPORTED if this feature is not supported + * SUPPORTED if this feature is supported by a stable version + * SUPPORTED_UNSTABLE if this feature is supported by an unstable version + * (unstable version should only be used for dev/experimental purpose) + */ + 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) + } + + /** + * Use this method to know if you should force a version when creating + * a room that requires this feature. + * You can also use #isFeatureSupported prior to this call to check if the + * feature is supported and report some feedback to user. + */ + 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..9f8e9aa1d1 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 @@ -18,7 +18,9 @@ package org.matrix.android.sdk.api.session.homeserver data class RoomVersionCapabilities( val defaultRoomVersion: String, - val supportedVersion: List + val supportedVersion: List, + // Keys are capabilities defined per spec, as for now knock or restricted + val capabilities: Map? ) data class RoomVersionInfo( @@ -26,6 +28,11 @@ data class RoomVersionInfo( val status: RoomVersionStatus ) +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..f5f722d783 --- /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 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.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/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index e614ea91d6..4d3f95233d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.Event 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.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -53,7 +54,7 @@ interface StateService { /** * Update the join rule and/or the guest access */ - suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) + suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, allowList: List? = null) /** * Update the avatar of the room @@ -91,4 +92,8 @@ interface StateService { * @param eventTypes Set of eventType to observe. If empty, all state events will be observed */ fun getStateEventsLive(eventTypes: Set, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData> + + suspend fun setJoinRulePublic() + suspend fun setJoinRuleInviteOnly() + suspend fun setJoinRuleRestricted(allowList: List) } 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..8b6d263f8c 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,28 @@ 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() + // Just for debug purpose +// ?: 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/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/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt index 82565d8118..21f55bbc42 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -17,17 +17,24 @@ package org.matrix.android.sdk.internal.session.permalinks import org.matrix.android.sdk.api.MatrixPatterns.getDomain +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams 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.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomGetter +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import java.net.URLEncoder import javax.inject.Inject import javax.inject.Provider internal class ViaParameterFinder @Inject constructor( @UserId private val userId: String, - private val roomGetterProvider: Provider + private val roomGetterProvider: Provider, + private val stateEventDataSource: StateEventDataSource ) { fun computeViaParams(roomId: String, max: Int): List { @@ -70,4 +77,28 @@ internal class ViaParameterFinder @Inject constructor( .orEmpty() .toSet() } + + fun computeViaParamsForRestricted(roomId: String, max: Int): List { + val userThatCanInvite = roomGetterProvider.get().getRoom(roomId) + ?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) }) + ?.map { it.userId } + ?.filter { userCanInvite(userId, roomId) } + .orEmpty() + .toSet() + + return userThatCanInvite.map { it.getDomain() } + .groupBy { it } + .mapValues { it.value.size } + .toMutableMap() + .let { map -> map.keys.sortedByDescending { map[it] } } + .take(max) + } + + fun userCanInvite(userId: String, roomId: String): Boolean { + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + ?.content?.toModel() + ?.let { PowerLevelsHelper(it) } + + return powerLevelsHelper?.isUserAbleToInvite(userId) ?: false + } } 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..9bb3899f2f 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,18 @@ 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,20 +150,6 @@ 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() - ) - } - } - /** * Add the crypto algorithm to the room creation parameters. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index ff2afb5d61..7eed22f65f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.room.state import android.net.Uri import androidx.lifecycle.LiveData import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -29,17 +29,20 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent 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 import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.content.FileUploader -import java.lang.UnsupportedOperationException +import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, private val sendStateTask: SendStateTask, - private val fileUploader: FileUploader + private val fileUploader: FileUploader, + private val viaParameterFinder: ViaParameterFinder ) : StateService { @AssistedFactory @@ -126,12 +129,19 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } - override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) { + override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, allowList: List?) { if (joinRules != null) { - if (joinRules == RoomJoinRules.RESTRICTED) throw UnsupportedOperationException("No yet supported") + val body = if (joinRules == RoomJoinRules.RESTRICTED) { + RoomJoinRulesContent( + _joinRules = RoomJoinRules.RESTRICTED.value, + allowList = allowList + ).toContent() + } else { + mapOf("join_rule" to joinRules) + } sendStateEvent( eventType = EventType.STATE_ROOM_JOIN_RULES, - body = mapOf("join_rule" to joinRules), + body = body, stateKey = null ) } @@ -160,4 +170,20 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private stateKey = null ) } + + override suspend fun setJoinRulePublic() { + updateJoinRule(RoomJoinRules.PUBLIC, null) + } + + override suspend fun setJoinRuleInviteOnly() { + updateJoinRule(RoomJoinRules.INVITE, null) + } + + override suspend fun setJoinRuleRestricted(allowList: List) { + // we need to compute correct via parameters and check if PL are correct + val allowEntries = allowList.map { spaceId -> + RoomJoinRulesAllowEntry(spaceId, viaParameterFinder.computeViaParamsForRestricted(spaceId, 3)) + } + updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries) + } } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 70eea6c26e..391140b9f3 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -162,7 +162,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===103 +enum class===105 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 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 @@ + (), MigrateRoomViewModel.Factory { + enum class MigrationReason { + MANUAL, + FOR_RESTRICTED + } + @Parcelize data class Args( val roomId: String, - val newVersion: String + val newVersion: String, + val reason: MigrationReason = MigrationReason.MANUAL, + val customDescription: CharSequence? = null ) : Parcelable @Inject @@ -62,11 +69,22 @@ class MigrateRoomBottomSheet : override fun invalidate() = withState(viewModel) { state -> views.headerText.setText(if (state.isPublic) R.string.upgrade_public_room else R.string.upgrade_private_room) - views.upgradeFromTo.text = getString(R.string.upgrade_public_room_from_to, state.currentVersion, state.newVersion) - views.autoInviteSwitch.isVisible = !state.isPublic && state.otherMemberCount > 0 + if (state.migrationReason == MigrationReason.MANUAL) { + views.descriptionText.text = getString(R.string.upgrade_room_warning) + views.upgradeFromTo.text = getString(R.string.upgrade_public_room_from_to, state.currentVersion, state.newVersion) + } else if (state.migrationReason == MigrationReason.FOR_RESTRICTED) { + views.descriptionText.setTextOrHide(state.customDescription) + views.upgradeFromTo.text = getString(R.string.upgrade_room_for_restricted_note) + } - views.autoUpdateParent.isVisible = state.knownParents.isNotEmpty() + if (state.autoMigrateMembersAndParents) { + views.autoUpdateParent.isVisible = false + views.autoInviteSwitch.isVisible = false + } else { + views.autoInviteSwitch.isVisible = !state.isPublic && state.otherMemberCount > 0 + views.autoUpdateParent.isVisible = state.knownParents.isNotEmpty() + } when (state.upgradingStatus) { is Loading -> { @@ -143,9 +161,12 @@ class MigrateRoomBottomSheet : const val REQUEST_KEY = "MigrateRoomBottomSheetRequest" const val BUNDLE_KEY_REPLACEMENT_ROOM = "BUNDLE_KEY_REPLACEMENT_ROOM" - fun newInstance(roomId: String, newVersion: String): MigrateRoomBottomSheet { + fun newInstance(roomId: String, newVersion: String, + reason: MigrationReason = MigrationReason.MANUAL, + customDescription: CharSequence? = null + ): MigrateRoomBottomSheet { return MigrateRoomBottomSheet().apply { - setArguments(Args(roomId, newVersion)) + setArguments(Args(roomId, newVersion, reason, customDescription)) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt index 231bb319f0..3dc5d0e3ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewModel.kt @@ -90,11 +90,23 @@ class MigrateRoomViewModel @AssistedInject constructor( copy(upgradingStatus = Loading()) } session.coroutineScope.launch { + val userToInvite = if (state.autoMigrateMembersAndParents) { + summary?.otherMemberIds?.takeIf { !state.isPublic } + } else { + summary?.otherMemberIds?.takeIf { state.shouldIssueInvites } + }.orEmpty() + + val parentSpaceToUpdate = if (state.autoMigrateMembersAndParents) { + summary?.flattenParentIds + } else { + summary?.flattenParentIds?.takeIf { state.shouldUpdateKnownParents } + }.orEmpty() + val result = upgradeRoomViewModelTask.execute(UpgradeRoomViewModelTask.Params( roomId = state.roomId, newVersion = state.newVersion, - userIdsToAutoInvite = summary?.otherMemberIds?.takeIf { state.shouldIssueInvites } ?: emptyList(), - parentSpaceToUpdate = summary?.flattenParentIds?.takeIf { state.shouldUpdateKnownParents } ?: emptyList(), + userIdsToAutoInvite = userToInvite, + parentSpaceToUpdate = parentSpaceToUpdate, progressReporter = { indeterminate, progress, total -> setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt index e3936de42f..e3ea98682b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/MigrateRoomViewState.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.Uninitialized data class MigrateRoomViewState( val roomId: String, val newVersion: String, + val customDescription: CharSequence? = null, val currentVersion: String? = null, val isPublic: Boolean = false, val shouldIssueInvites: Boolean = false, @@ -32,10 +33,15 @@ data class MigrateRoomViewState( val upgradingStatus: Async = Uninitialized, val upgradingProgress: Int = 0, val upgradingProgressTotal: Int = 0, - val upgradingProgressIndeterminate: Boolean = true + val upgradingProgressIndeterminate: Boolean = true, + val migrationReason: MigrateRoomBottomSheet.MigrationReason = MigrateRoomBottomSheet.MigrationReason.MANUAL, + val autoMigrateMembersAndParents: Boolean = false ) : MvRxState { constructor(args: MigrateRoomBottomSheet.Args) : this( roomId = args.roomId, - newVersion = args.newVersion + newVersion = args.newVersion, + migrationReason = args.reason, + autoMigrateMembersAndParents = args.reason == MigrateRoomBottomSheet.MigrationReason.FOR_RESTRICTED, + customDescription = args.customDescription ) } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt index b50c56e1db..a44a0206aa 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt @@ -18,12 +18,13 @@ package im.vector.app.features.roomdirectory.createroom import android.net.Uri import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules sealed class CreateRoomAction : VectorViewModelAction { data class SetAvatar(val imageUri: Uri?) : CreateRoomAction() data class SetName(val name: String) : CreateRoomAction() data class SetTopic(val topic: String) : CreateRoomAction() - data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction() + data class SetVisibility(val rule: RoomJoinRules) : CreateRoomAction() data class SetRoomAliasLocalPart(val aliasLocalPart: String) : CreateRoomAction() data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 6c441c355c..2676096b6b 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import im.vector.app.R import im.vector.app.core.epoxy.dividerItem +import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.resources.StringProvider import im.vector.app.features.discovery.settingsSectionTitleItem import im.vector.app.features.form.formAdvancedToggleItem @@ -29,6 +30,7 @@ import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formSubmitButtonItem import im.vector.app.features.form.formSwitchItem import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import javax.inject.Inject class CreateRoomController @Inject constructor( @@ -83,26 +85,59 @@ class CreateRoomController @Inject constructor( host.listener?.onTopicChange(text) } } + + settingsSectionTitleItem { + id("visibility") + titleResId(R.string.room_settings_room_access_title) + } + + when (viewState.roomJoinRules) { + RoomJoinRules.INVITE -> { + 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() } + ) + } + else -> { + // not yet supported + } + } + 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 +172,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 +205,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..b7821c056c 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,8 @@ 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() { + 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..21c39ad49d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt @@ -0,0 +1,149 @@ +/* + * 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 android.os.Bundle +import androidx.core.view.isVisible +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.utils.toast +import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedActions +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedEvents +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState +import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel +import javax.inject.Inject + +class RoomJoinRuleActivity : VectorBaseActivity(), + RoomJoinRuleChooseRestrictedViewModel.Factory { + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + private lateinit var roomProfileArgs: RoomProfileArgs + + @Inject + lateinit var allowListViewModelFactory: RoomJoinRuleChooseRestrictedViewModel.Factory + + @Inject + lateinit var errorFormatter: ErrorFormatter + + val viewModel: RoomJoinRuleChooseRestrictedViewModel by viewModel() + + 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 onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.selectSubscribe(this, RoomJoinRuleChooseRestrictedState::updatingStatus) { + when (it) { + Uninitialized -> { + // nop + } + is Loading -> { + views.simpleActivityWaitingView.isVisible = true + } + is Success -> { + withState(viewModel) { state -> + if (state.didSwitchToReplacementRoom) { + // we should navigate to new room + navigator.openRoom(this, state.roomId, null, true) + } + finish() + } + } + is Fail -> { + views.simpleActivityWaitingView.isVisible = false + toast(errorFormatter.toHumanReadable(it.error)) + } + } + } + + viewModel.observeViewEvents { + when (it) { + RoomJoinRuleChooseRestrictedEvents.NavigateToChooseRestricted -> navigateToChooseRestricted() + is RoomJoinRuleChooseRestrictedEvents.NavigateToUpgradeRoom -> navigateToUpgradeRoom(it) + } + } + + supportFragmentManager.setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY, this) { _, bundle -> + bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> + viewModel.handle(RoomJoinRuleChooseRestrictedActions.SwitchToRoomAfterMigration(replacementRoomId)) + } + } + } + + private fun navigateToUpgradeRoom(events: RoomJoinRuleChooseRestrictedEvents.NavigateToUpgradeRoom) { + MigrateRoomBottomSheet.newInstance( + events.roomId, + events.toVersion, + MigrateRoomBottomSheet.MigrationReason.FOR_RESTRICTED, + events.description + ).show(supportFragmentManager, "migrate") + } + + private fun navigateToChooseRestricted() { + supportFragmentManager.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@RoomJoinRuleActivity.roomProfileArgs.toMvRxBundle(), + tag + ).addToBackStack(tag) + } + } + + 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..7adfc594b7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleAdvancedController.kt @@ -0,0 +1,109 @@ +/* + * 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 timber.log.Timber +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 } + Timber.w("##@@ ${state.updatedAllowList}") + spaceJoinRuleItem { + id("restricted") + avatarRenderer(host.avatarRenderer) + needUpgrade(restrictedRule.needUpgrade) + selected(state.currentRoomJoinRules == RoomJoinRules.RESTRICTED) + restrictedList(state.updatedAllowList) + 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/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..eaf19fe075 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleFragment.kt @@ -0,0 +1,108 @@ +/* + * 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 androidx.core.view.isVisible +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +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.RoomJoinRuleChooseRestrictedActions +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 avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), + 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 { + val hasUnsavedChanges = withState(viewModel) { it.hasUnsavedChanges } + val isLoading = withState(viewModel) { it.updatingStatus is Loading } + if (!hasUnsavedChanges || isLoading) { + requireActivity().finish() + } else { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.dialog_title_warning) + .setMessage(R.string.warning_unsaved_change) + .setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ -> + requireActivity().finish() + } + .setNegativeButton(R.string.cancel, null) + .show() + return true + } + return true + } + + override fun invalidate() = withState(viewModel) { state -> + super.invalidate() + controller.setData(state) + if (state.hasUnsavedChanges) { + // show discard and save + views.cancelButton.isVisible = true + views.positiveButton.text = getString(R.string.warning_unsaved_change_discard) + views.positiveButton.isVisible = true + views.positiveButton.text = getString(R.string.save) + views.positiveButton.debouncedClicks { + viewModel.handle(RoomJoinRuleChooseRestrictedActions.DoUpdateJoinRules) + } + } else { + views.cancelButton.isVisible = false + views.positiveButton.isVisible = true + views.positiveButton.text = getString(R.string.ok) + views.positiveButton.debouncedClicks { requireActivity().finish() } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.genericRecyclerView.configureWith(controller, hasFixedSize = true) + controller.interactionListener = this + views.cancelButton.debouncedClicks { requireActivity().finish() } + } + + override fun onDestroyView() { + views.genericRecyclerView.cleanup() + super.onDestroyView() + } + + override fun didSelectRule(rules: RoomJoinRules) { + val isLoading = withState(viewModel) { it.updatingStatus is Loading } + if (isLoading) return + + viewModel.handle(RoomJoinRuleChooseRestrictedActions.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..9110f9b32e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/SpaceJoinRuleItem.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 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.core.utils.DebouncedClickListener +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) + holder.upgradeRequiredButton.setOnClickListener(DebouncedClickListener(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 + items.onEach { it.isVisible = false } + if (!needUpgrade) { + if (restrictedList.isEmpty()) { + holder.listTitle.isVisible = false + } else { + holder.listTitle.isVisible = true + restrictedList.forEachIndexed { index, matrixItem -> + if (index < items.size) { + items[index].isVisible = true + avatarRenderer.render(matrixItem, items[index]) + } else if (index == items.size) { + holder.spaceMore.isVisible = true + } + } + } + } else { + holder.listTitle.isVisible = false + holder.helperText.isVisible = false + } + } + + 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..8e6bf85eae 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 @@ -3463,4 +3475,9 @@ Cannot record a voice message Cannot reply or edit while voice message is active Voice Message (%1$s) + + Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. + Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. + + Please note upgrading will make a new version of the room. All current messages will stay in this archived room.