Merge pull request #3663 from vector-im/feature/bca/room_caps_restriced

Feature/bca/room caps restricted
This commit is contained in:
Valere 2021-07-30 19:41:16 +02:00 committed by GitHub
commit 60caac4214
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 2104 additions and 151 deletions

1
changelog.d/3509.feature Normal file
View file

@ -0,0 +1 @@
Spaces - Support Restricted Room via room capabilities API

1
changelog.d/3665.feature Normal file
View file

@ -0,0 +1 @@
Spaces | Support restricted room access in room settings

View file

@ -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"
}
}

View file

@ -18,7 +18,9 @@ package org.matrix.android.sdk.api.session.homeserver
data class RoomVersionCapabilities(
val defaultRoomVersion: String,
val supportedVersion: List<RoomVersionInfo>
val supportedVersion: List<RoomVersionInfo>,
// Keys are capabilities defined per spec, as for now knock or restricted
val capabilities: Map<String, RoomCapabilitySupport>?
)
data class RoomVersionInfo(
@ -26,6 +28,11 @@ data class RoomVersionInfo(
val status: RoomVersionStatus
)
data class RoomCapabilitySupport(
val preferred: String?,
val support: List<String>
)
enum class RoomVersionStatus {
STABLE,
UNSTABLE

View file

@ -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<RoomJoinRulesAllowEntry>? = null
var featurePreset: RoomFeaturePreset? = null
companion object {
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"

View file

@ -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<Event>?
}
class RestrictedRoomPreset(val homeServerCapabilities: HomeServerCapabilities, val restrictedList: List<RoomJoinRulesAllowEntry>) : 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<Event>? {
return listOf(
Event(
type = EventType.STATE_ROOM_JOIN_RULES,
stateKey = "",
content = RoomJoinRulesContent(
_joinRules = RoomJoinRules.RESTRICTED.value,
allowList = restrictedList
).toContent()
)
)
}
}

View file

@ -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<RoomJoinRulesAllowEntry>? = 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<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
suspend fun setJoinRulePublic()
suspend fun setJoinRuleInviteOnly()
suspend fun setJoinRuleRestricted(allowList: List<String>)
}

View file

@ -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<String>()
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")
// )
// )
)
}
}

View file

@ -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

View file

@ -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<RoomGetter>
private val roomGetterProvider: Provider<RoomGetter>,
private val stateEventDataSource: StateEventDataSource
) {
fun computeViaParams(roomId: String, max: Int): List<String> {
@ -70,4 +77,28 @@ internal class ViaParameterFinder @Inject constructor(
.orEmpty()
.toSet()
}
fun computeViaParamsForRestricted(roomId: String, max: Int): List<String> {
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<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
return powerLevelsHelper?.isUserAbleToInvite(userId) ?: false
}
}

View file

@ -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(
params.featurePreset?.updateRoomParams(params)
val initialStates = (
listOfNotNull(
buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params),
buildAvatarEvent(params),
buildGuestAccess(params),
buildJoinRulesRestricted(params)
buildGuestAccess(params)
)
+ params.featurePreset?.setupInitialStates().orEmpty()
+ buildCustomInitialStates(params)
)
+ 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.
*/

View file

@ -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<RoomJoinRulesAllowEntry>?) {
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<String>) {
// 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)
}
}

View file

@ -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

View file

@ -224,6 +224,7 @@
</activity>
<activity android:name=".features.roomprofile.RoomProfileActivity" />
<activity android:name=".features.roomprofile.settings.joinrule.RoomJoinRuleActivity" />
<activity android:name=".features.signout.hard.SignedOutActivity" />
<activity

View file

@ -108,6 +108,8 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleChooseRestrictedFragment
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
@ -804,4 +806,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SpaceManageRoomsFragment::class)
fun bindSpaceManageRoomsFragment(fragment: SpaceManageRoomsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomJoinRuleFragment::class)
fun bindRoomJoinRuleFragment(fragment: RoomJoinRuleFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomJoinRuleChooseRestrictedFragment::class)
fun bindRoomJoinRuleChooseRestrictedFragment(fragment: RoomJoinRuleChooseRestrictedFragment): Fragment
}

View file

@ -77,6 +77,7 @@ import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet
import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.roomprofile.alias.detail.RoomAliasBottomSheet
import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet
@ -169,6 +170,7 @@ interface ScreenComponent {
fun inject(activity: SpaceCreationActivity)
fun inject(activity: SpaceExploreActivity)
fun inject(activity: SpaceManageActivity)
fun inject(activity: RoomJoinRuleActivity)
/* ==========================================================================================
* BottomSheets

View file

@ -40,10 +40,17 @@ class MigrateRoomBottomSheet :
VectorBaseBottomSheetDialogFragment<BottomSheetRoomUpgradeBinding>(),
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)
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)
}
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))
}
}
}

View file

@ -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(

View file

@ -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<UpgradeRoomViewModelTask.Result> = 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
)
}

View file

@ -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()

View file

@ -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()

View file

@ -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))

View file

@ -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<CreateRoomViewState, CreateRoomAction, CreateRoomViewEvents>(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,36 +176,46 @@ 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) {
private fun setVisibility(action: CreateRoomAction.SetVisibility) = setState {
when (action.rule) {
RoomJoinRules.PUBLIC -> {
copy(
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(""),
roomJoinRules = RoomJoinRules.PUBLIC,
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized,
isEncrypted = false
)
} else {
}
RoomJoinRules.RESTRICTED -> {
copy(
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Private,
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),
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

View file

@ -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<String> = 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()
}

View file

@ -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 ->

View file

@ -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<RoomSettingsViewState, RoomSettingsAction, RoomSettingsViewEvents>(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() {

View file

@ -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)

View file

@ -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<ActivitySimpleBinding>(),
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)
}
}
}
}

View file

@ -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<RoomJoinRuleChooseRestrictedState>() {
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("")
}
}
}

View file

@ -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<JoinRulesOptionSupport>
) : Parcelable
class RoomJoinRuleBottomSheet : BottomSheetGeneric<RoomJoinRuleState, RoomJoinRuleRadioAction>() {
@ -61,9 +70,15 @@ class RoomJoinRuleBottomSheet : BottomSheetGeneric<RoomJoinRuleState, RoomJoinRu
}
companion object {
fun newInstance(currentRoomJoinRule: RoomJoinRules): RoomJoinRuleBottomSheet {
fun newInstance(currentRoomJoinRule: RoomJoinRules,
allowedJoinedRules: List<JoinRulesOptionSupport> = listOf(
RoomJoinRules.INVITE, RoomJoinRules.PUBLIC
).map { it.toOption(true) }
): RoomJoinRuleBottomSheet {
return RoomJoinRuleBottomSheet().apply {
setArguments(RoomJoinRuleBottomSheetArgs(currentRoomJoinRule))
setArguments(
RoomJoinRuleBottomSheetArgs(currentRoomJoinRule, allowedJoinedRules)
)
}
}
}

View file

@ -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) }
}
}

View file

@ -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<FragmentJoinRulesRecyclerBinding>(),
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))
}
}

View file

@ -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<JoinRulesOptionSupport> =
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
)
}

View file

@ -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<SpaceJoinRuleItem.Holder>() {
@EpoxyAttribute
var selected: Boolean = false
@EpoxyAttribute
var needUpgrade: Boolean = false
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var restrictedList: List<MatrixItem> = 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<ImageView>(R.id.radioIcon)
val actionTitle by bind<TextView>(R.id.actionTitle)
val actionDescription by bind<TextView>(R.id.actionDescription)
val upgradeRequiredButton by bind<Button>(R.id.upgradeRequiredButton)
val listTitle by bind<TextView>(R.id.listTitle)
val space1 by bind<ImageView>(R.id.rest1)
val space2 by bind<ImageView>(R.id.rest2)
val space3 by bind<ImageView>(R.id.rest3)
val space4 by bind<ImageView>(R.id.rest4)
val space5 by bind<ImageView>(R.id.rest5)
val spaceMore by bind<ImageView>(R.id.rest6)
val helperText by bind<TextView>(R.id.helperText)
}
}

View file

@ -0,0 +1,111 @@
/*
* 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.advanced
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.spaces.manage.roomSelectionItem
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
class ChooseRestrictedController @Inject constructor(
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer
) : TypedEpoxyController<RoomJoinRuleChooseRestrictedState>() {
interface Listener {
fun onItemSelected(matrixItem: MatrixItem)
}
var listener: Listener? = null
override fun buildModels(data: RoomJoinRuleChooseRestrictedState?) {
data ?: return
val host = this
if (data.filter.isNotEmpty()) {
when (val results = data.filteredResults) {
Uninitialized,
is Fail -> return
is Loading -> loadingItem { id("filter_load") }
is Success -> {
if (results.invoke().isEmpty()) {
noResultItem {
id("empty")
text(host.stringProvider.getString(R.string.no_result_placeholder))
}
} else {
results.invoke().forEach { matrixItem ->
roomSelectionItem {
id(matrixItem.id)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
selected(data.updatedAllowList.firstOrNull { it.id == matrixItem.id } != null)
itemClickListener { host.listener?.onItemSelected(matrixItem) }
}
}
}
}
}
return
}
// when no filters
genericFooterItem {
id("h1")
text(host.stringProvider.getString(R.string.space_you_know_that_contains_this_room))
centered(false)
}
data.possibleSpaceCandidate.forEach { matrixItem ->
roomSelectionItem {
id(matrixItem.id)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
selected(data.updatedAllowList.firstOrNull { it.id == matrixItem.id } != null)
itemClickListener { host.listener?.onItemSelected(matrixItem) }
}
}
if (data.unknownRestricted.isNotEmpty()) {
genericFooterItem {
id("others")
text(host.stringProvider.getString(R.string.other_spaces_or_rooms_you_might_not_know))
centered(false)
}
data.unknownRestricted.forEach { matrixItem ->
roomSelectionItem {
id(matrixItem.id)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
selected(data.updatedAllowList.firstOrNull { it.id == matrixItem.id } != null)
itemClickListener { host.listener?.onItemSelected(matrixItem) }
}
}
}
}
}

View file

@ -0,0 +1,29 @@
/*
* 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.advanced
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.util.MatrixItem
sealed class RoomJoinRuleChooseRestrictedActions : VectorViewModelAction {
data class FilterWith(val filter: String) : RoomJoinRuleChooseRestrictedActions()
data class ToggleSelection(val matrixItem: MatrixItem) : RoomJoinRuleChooseRestrictedActions()
data class SelectJoinRules(val rules: RoomJoinRules) : RoomJoinRuleChooseRestrictedActions()
object DoUpdateJoinRules : RoomJoinRuleChooseRestrictedActions()
data class SwitchToRoomAfterMigration(val roomId: String) : RoomJoinRuleChooseRestrictedActions()
}

View file

@ -0,0 +1,24 @@
/*
* 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.advanced
import im.vector.app.core.platform.VectorViewEvents
sealed class RoomJoinRuleChooseRestrictedEvents : VectorViewEvents {
object NavigateToChooseRestricted : RoomJoinRuleChooseRestrictedEvents()
data class NavigateToUpgradeRoom(val roomId: String, val toVersion: String, val description: CharSequence) : RoomJoinRuleChooseRestrictedEvents()
}

View file

@ -0,0 +1,92 @@
/*
* 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 com.jakewharton.rxbinding3.appcompat.queryTextChanges
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.FragmentSpaceRestrictedSelectBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.settings.joinrule.advanced.ChooseRestrictedController
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedActions
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.util.MatrixItem
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class RoomJoinRuleChooseRestrictedFragment @Inject constructor(
val controller: ChooseRestrictedController,
val avatarRenderer: AvatarRenderer
) : VectorBaseFragment<FragmentSpaceRestrictedSelectBinding>(),
ChooseRestrictedController.Listener,
OnBackPressed {
private val viewModel: RoomJoinRuleChooseRestrictedViewModel by activityViewModel(RoomJoinRuleChooseRestrictedViewModel::class)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentSpaceRestrictedSelectBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
controller.listener = this
views.recyclerView.configureWith(controller)
views.roomsFilter.queryTextChanges()
.debounce(500, TimeUnit.MILLISECONDS)
.subscribeBy {
viewModel.handle(RoomJoinRuleChooseRestrictedActions.FilterWith(it.toString()))
}
.disposeOnDestroyView()
views.okButton.debouncedClicks {
parentFragmentManager.popBackStack()
}
}
override fun onDestroyView() {
controller.listener = null
views.recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
controller.setData(state)
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
val filter = views.roomsFilter.query
if (filter.isEmpty()) {
parentFragmentManager.popBackStack()
} else {
views.roomsFilter.setQuery("", true)
}
return true
}
override fun onItemSelected(matrixItem: MatrixItem) {
viewModel.handle(RoomJoinRuleChooseRestrictedActions.ToggleSelection(matrixItem))
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.advanced
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.app.features.roomprofile.settings.joinrule.JoinRulesOptionSupport
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.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
data class RoomJoinRuleChooseRestrictedState(
// the currentRoomId
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val initialRoomJoinRules: RoomJoinRules? = null,
val currentRoomJoinRules: RoomJoinRules? = null,
val updatedAllowList: List<MatrixItem> = emptyList(),
val choices: List<JoinRulesOptionSupport>? = null,
val initialAllowList: List<RoomJoinRulesAllowEntry> = emptyList(),
val possibleSpaceCandidate: List<MatrixItem> = emptyList(),
val unknownRestricted: List<MatrixItem> = emptyList(),
val filter: String = "",
val filteredResults: Async<List<MatrixItem>> = Uninitialized,
val hasUnsavedChanges: Boolean = false,
val updatingStatus: Async<Unit> = Uninitialized,
val upgradeNeededForRestricted: Boolean = false,
val restrictedSupportedByThisVersion: Boolean = false,
val restrictedVersionNeeded: String? = null,
val didSwitchToReplacementRoom: Boolean = false
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}

View file

@ -0,0 +1,398 @@
/*
* 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.advanced
import android.graphics.Typeface
import androidx.core.text.toSpannable
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
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.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.styleMatchingText
import im.vector.app.features.roomprofile.settings.joinrule.toOption
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.launch
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.Membership
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.RoomType
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
class RoomJoinRuleChooseRestrictedViewModel @AssistedInject constructor(
@Assisted initialState: RoomJoinRuleChooseRestrictedState,
private val session: Session,
private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider
) : VectorViewModel<RoomJoinRuleChooseRestrictedState, RoomJoinRuleChooseRestrictedActions, RoomJoinRuleChooseRestrictedEvents>(initialState) {
var room = session.getRoom(initialState.roomId)!!
init {
viewModelScope.launch {
initializeForRoom(initialState.roomId)
}
}
private fun initializeForRoom(roomId: String) {
room = session.getRoom(roomId)!!
session.getRoomSummary(roomId)?.let { roomSummary ->
val joinRulesContent = room.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition)
?.content
?.toModel<RoomJoinRulesContent>()
val initialAllowList = joinRulesContent?.allowList
val knownParentSpacesAllowed = mutableListOf<MatrixItem>()
val unknownAllowedOrRooms = mutableListOf<MatrixItem>()
initialAllowList.orEmpty().forEach { entry ->
val summary = session.getRoomSummary(entry.spaceID)
if (summary == null // it's not known by me
|| summary.roomType != RoomType.SPACE // it's not a space
|| !roomSummary.flattenParentIds.contains(summary.roomId) // it's not a parent space
) {
unknownAllowedOrRooms.add(
summary?.toMatrixItem() ?: MatrixItem.RoomItem(entry.spaceID, null, null)
)
} else {
knownParentSpacesAllowed.add(summary.toMatrixItem())
}
}
val possibleSpaceCandidate = knownParentSpacesAllowed.toMutableList()
roomSummary.flattenParentIds.mapNotNull {
session.getRoomSummary(it)?.toMatrixItem()
}.forEach {
if (!possibleSpaceCandidate.contains(it)) {
possibleSpaceCandidate.add(it)
}
}
val homeServerCapabilities = session.getHomeServerCapabilities()
var safeRule: RoomJoinRules = joinRulesContent?.joinRules ?: RoomJoinRules.INVITE
// server is not really checking that, just to be sure let's check
val restrictedSupportedByThisVersion = homeServerCapabilities
.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED, room.getRoomVersion())
if (safeRule == RoomJoinRules.RESTRICTED
&& !restrictedSupportedByThisVersion) {
safeRule = RoomJoinRules.INVITE
}
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(
roomSummary = Success(roomSummary),
initialRoomJoinRules = safeRule,
currentRoomJoinRules = safeRule,
choices = choices,
initialAllowList = initialAllowList.orEmpty(),
updatedAllowList = initialAllowList.orEmpty().map {
session.getRoomSummary(it.spaceID)?.toMatrixItem() ?: MatrixItem.RoomItem(it.spaceID, null, null)
},
possibleSpaceCandidate = possibleSpaceCandidate,
unknownRestricted = unknownAllowedOrRooms,
restrictedSupportedByThisVersion = restrictedSupportedByThisVersion,
upgradeNeededForRestricted = !restrictedSupportedByThisVersion && couldUpgradeToRestricted,
restrictedVersionNeeded = homeServerCapabilities.versionOverrideForFeature(HomeServerCapabilities.ROOM_CAP_RESTRICTED)
)
}
}
}
fun checkForChanges() = withState { state ->
if (state.initialRoomJoinRules != state.currentRoomJoinRules) {
setState {
copy(hasUnsavedChanges = true)
}
return@withState
}
if (state.currentRoomJoinRules == RoomJoinRules.RESTRICTED) {
val allowDidChange = state.initialAllowList.map { it.spaceID } != state.updatedAllowList.map { it.id }
setState {
copy(hasUnsavedChanges = allowDidChange)
}
return@withState
}
setState {
copy(hasUnsavedChanges = false)
}
}
@AssistedFactory
interface Factory {
fun create(initialState: RoomJoinRuleChooseRestrictedState): RoomJoinRuleChooseRestrictedViewModel
}
override fun handle(action: RoomJoinRuleChooseRestrictedActions) {
when (action) {
is RoomJoinRuleChooseRestrictedActions.FilterWith -> handleFilter(action)
is RoomJoinRuleChooseRestrictedActions.ToggleSelection -> handleToggleSelection(action)
is RoomJoinRuleChooseRestrictedActions.SelectJoinRules -> handleSelectRule(action)
is RoomJoinRuleChooseRestrictedActions.SwitchToRoomAfterMigration -> handleSwitchToRoom(action)
RoomJoinRuleChooseRestrictedActions.DoUpdateJoinRules -> handleSubmit()
}.exhaustive
checkForChanges()
}
fun handleSubmit() = withState { state ->
setState { copy(updatingStatus = Loading()) }
viewModelScope.launch {
try {
when (state.currentRoomJoinRules) {
RoomJoinRules.PUBLIC -> room.setJoinRulePublic()
RoomJoinRules.INVITE -> room.setJoinRuleInviteOnly()
RoomJoinRules.RESTRICTED -> room.setJoinRuleRestricted(state.updatedAllowList.map { it.id })
RoomJoinRules.KNOCK,
RoomJoinRules.PRIVATE,
null -> {
throw UnsupportedOperationException()
}
}
setState { copy(updatingStatus = Success(Unit)) }
} catch (failure: Throwable) {
setState { copy(updatingStatus = Fail(failure)) }
}
}
}
fun handleSelectRule(action: RoomJoinRuleChooseRestrictedActions.SelectJoinRules) = withState { state ->
val currentRoomJoinRules = state.currentRoomJoinRules
val candidate = session.getRoomSummary(state.roomId)
?.flattenParentIds
?.filter {
session.getRoomSummary(it)?.spaceChildren?.firstOrNull { it.childRoomId == state.roomId } != null
}?.mapNotNull {
session.getRoomSummary(it)?.toMatrixItem()
}?.firstOrNull()
val description = if (candidate != null) {
stringProvider.getString(R.string.upgrade_room_for_restricted, candidate.getBestName()).toSpannable().let {
it.styleMatchingText(candidate.getBestName(), Typeface.BOLD)
}
} else {
stringProvider.getString(R.string.upgrade_room_for_restricted_no_param)
}
if (action.rules == RoomJoinRules.RESTRICTED && state.upgradeNeededForRestricted) {
// let's show the room upgrade bottom sheet
_viewEvents.post(
RoomJoinRuleChooseRestrictedEvents.NavigateToUpgradeRoom(
state.roomId,
state.restrictedVersionNeeded ?: "",
description
)
)
return@withState
}
if (action.rules == RoomJoinRules.RESTRICTED && currentRoomJoinRules != RoomJoinRules.RESTRICTED) {
// switching to restricted
// if allow list is empty, then default to current space parents
if (state.updatedAllowList.isEmpty()) {
val candidates = session.getRoomSummary(state.roomId)
?.flattenParentIds
?.filter {
session.getRoomSummary(it)?.spaceChildren?.firstOrNull { it.childRoomId == state.roomId } != null
}?.mapNotNull {
session.getRoomSummary(it)?.toMatrixItem()
}.orEmpty()
setState {
copy(updatedAllowList = candidates)
}
}
}
setState {
copy(
currentRoomJoinRules = action.rules
)
}
if (action.rules == RoomJoinRules.RESTRICTED && currentRoomJoinRules == RoomJoinRules.RESTRICTED) {
_viewEvents.post(RoomJoinRuleChooseRestrictedEvents.NavigateToChooseRestricted)
}
}
private fun handleSwitchToRoom(action: RoomJoinRuleChooseRestrictedActions.SwitchToRoomAfterMigration) = withState { state ->
viewModelScope.launch {
val oldRoomSummary = session.getRoomSummary(state.roomId)
val replacementRoomSummary = session.getRoomSummary(action.roomId)
setState {
copy(
roomId = action.roomId,
roomSummary = replacementRoomSummary?.let { Success(it) } ?: Uninitialized,
didSwitchToReplacementRoom = true
)
}
initializeForRoom(action.roomId)
// set as restricted now
val candidates = oldRoomSummary
?.flattenParentIds
?.filter {
session.getRoomSummary(it)?.spaceChildren?.firstOrNull { it.childRoomId == state.roomId } != null
}?.mapNotNull {
session.getRoomSummary(it)?.toMatrixItem()
}.orEmpty()
setState {
copy(
currentRoomJoinRules = RoomJoinRules.RESTRICTED,
updatedAllowList = candidates
)
}
setState { copy(updatingStatus = Loading()) }
viewModelScope.launch {
try {
room.setJoinRuleRestricted(candidates.map { it.id })
setState { copy(updatingStatus = Success(Unit)) }
} catch (failure: Throwable) {
setState { copy(updatingStatus = Fail(failure)) }
}
}
}
}
private fun handleToggleSelection(action: RoomJoinRuleChooseRestrictedActions.ToggleSelection) = withState { state ->
val selection = state.updatedAllowList.toMutableList()
if (selection.indexOfFirst { action.matrixItem.id == it.id } != -1) {
selection.removeAll { it.id == action.matrixItem.id }
} else {
selection.add(action.matrixItem)
}
val unknownAllowedOrRooms = mutableListOf<MatrixItem>()
// we would like to keep initial allowed here to show them unchecked
// to make it easier for users to spot the changes
val union = mutableListOf<MatrixItem>().apply {
addAll(
state.initialAllowList.map {
session.getRoomSummary(it.spaceID)?.toMatrixItem() ?: MatrixItem.RoomItem(it.spaceID, null, null)
}
)
addAll(selection)
}.distinctBy { it.id }.sortedBy { it.id }
union.forEach { entry ->
val summary = session.getRoomSummary(entry.id)
if (summary == null) {
unknownAllowedOrRooms.add(
entry
)
} else if (summary.roomType != RoomType.SPACE) {
unknownAllowedOrRooms.add(entry)
} else if (!state.roomSummary.invoke()!!.flattenParentIds.contains(entry.id)) {
// it's a space but not a direct parent
unknownAllowedOrRooms.add(entry)
} else {
// nop
}
}
setState {
copy(
updatedAllowList = selection.toList(),
unknownRestricted = unknownAllowedOrRooms
)
}
}
private fun handleFilter(action: RoomJoinRuleChooseRestrictedActions.FilterWith) = withState { state ->
setState {
copy(filter = action.filter, filteredResults = Loading())
}
viewModelScope.launch {
if (vectorPreferences.developerMode()) {
// in developer mode we let you choose any room or space to restrict to
val filteredCandidates = session.getRoomSummaries(
roomSummaryQueryParams {
excludeType = null
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
memberships = listOf(Membership.JOIN)
}
).map { it.toMatrixItem() }
setState {
copy(
filteredResults = Success(filteredCandidates)
)
}
} else {
// in normal mode you can only restrict to space parents
setState {
copy(
filteredResults = Success(
session.getRoomSummary(state.roomId)?.flattenParentIds?.mapNotNull {
session.getRoomSummary(it)?.toMatrixItem()
}?.filter {
it.displayName?.contains(filter, true) == true
}.orEmpty()
)
)
}
}
}
}
companion object : MvRxViewModelFactory<RoomJoinRuleChooseRestrictedViewModel, RoomJoinRuleChooseRestrictedState> {
override fun create(viewModelContext: ViewModelContext, state: RoomJoinRuleChooseRestrictedState)
: RoomJoinRuleChooseRestrictedViewModel? {
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")
}
}
}

View file

@ -24,11 +24,13 @@ import im.vector.app.features.settings.VectorPreferences
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.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
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
import javax.inject.Inject
@ -93,15 +95,28 @@ class CreateSpaceViewModelTask @Inject constructor(
}
)
} else {
if (vectorPreferences.labsUseExperimentalRestricted()) {
val homeServerCapabilities = session
.getHomeServerCapabilities()
val restrictedSupport = homeServerCapabilities
.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED)
val createRestricted = when (restrictedSupport) {
HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED -> true
HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED_UNSTABLE -> vectorPreferences.labsUseExperimentalRestricted()
else -> false
}
if (createRestricted) {
session.createRoom(CreateRoomParams().apply {
this.name = roomName
this.joinRuleRestricted = listOf(
this.featurePreset = RestrictedRoomPreset(
homeServerCapabilities,
listOf(
RoomJoinRulesAllowEntry(
spaceID = spaceID,
via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList()
)
)
)
if (e2eByDefault) {
this.enableEncryption()
}

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/genericRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:itemSpacing="1dp"
app:layout_constraintBottom_toTopOf="@+id/buttonBar"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="1"
tools:listitem="@layout/item_bottom_sheet_joinrule_restricted" />
<LinearLayout
android:id="@+id/buttonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?vctr_toolbar_background"
android:elevation="1dp"
android:gravity="end"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/genericRecyclerView">
<Button
android:id="@+id/cancelButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/cancel" />
<Button
android:id="@+id/positiveButton"
style="@style/Widget.Vector.Button.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/save" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:elevation="2dp"
android:layout_height="match_parent">
<include
android:id="@+id/waiting_view"
layout="@layout/merge_overlay_waiting_view" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/spaceExploreCollapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?android:colorBackground"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:scrimAnimationDuration="250"
app:scrimVisibleHeightTrigger="120dp"
app:titleEnabled="false"
app:toolbarId="@+id/toolbar">
<com.google.android.material.appbar.MaterialToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
style="@style/Widget.Vector.TextView.ActionBarTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/select_spaces" />
<TextView
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:text="@string/decide_which_spaces_can_access"
android:textColor="?vctr_content_secondary" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<!-- <com.google.android.material.appbar.MaterialToolbar-->
<!-- android:id="@+id/toolbar"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:layout_collapseMode="pin"-->
<!-- app:title="@string/select_spaces" />-->
<!-- <TextView-->
<!-- style="@style/Widget.Vector.TextView.Body"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="@string/decide_which_spaces_can_access" />-->
</com.google.android.material.appbar.CollapsingToolbarLayout>
<androidx.appcompat.widget.SearchView
android:id="@+id/roomsFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:queryHint="@string/search_hint_room_name" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_room_to_add_in_space" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?vctr_toolbar_background"
android:elevation="4dp"
android:gravity="end"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<Button
style="@style/Widget.Vector.Button.Positive"
android:id="@+id/okButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?><!-- https://www.figma.com/file/HOGxCoUWoedha639SjD90n/%5BBeta%5D-Restricted-room-access?node-id=58%3A656 -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/radioIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="15dp"
android:contentDescription="@string/a11y_checked"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_radio_on" />
<TextView
android:id="@+id/actionTitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="2"
android:text="@string/room_settings_room_access_restricted_title"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/radioIcon"
app:layout_constraintTop_toTopOf="@id/radioIcon" />
<TextView
android:id="@+id/actionDescription"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/allow_space_member_to_find_and_access"
android:textColor="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="@id/actionTitle"
app:layout_constraintTop_toBottomOf="@id/actionTitle"
app:layout_goneMarginTop="0dp" />
<Button
android:id="@+id/upgradeRequiredButton"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/upgrade_required"
android:textAllCaps="true"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/actionTitle"
app:layout_constraintTop_toBottomOf="@id/actionDescription"
tools:visibility="visible">
</Button>
<TextView
android:id="@+id/listTitle"
style="@style/Widget.Vector.TextView.Body.Medium"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/spaces_which_can_access"
android:textAllCaps="true"
android:textColor="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="@id/actionTitle"
app:layout_constraintTop_toBottomOf="@id/upgradeRequiredButton"
app:layout_goneMarginTop="8dp" />
<!-- <androidx.recyclerview.widget.RecyclerView-->
<!-- android:id="@+id/restrictedSpaceList"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="@id/actionTitle"-->
<!-- app:layout_constraintTop_toBottomOf="@id/listTitle"-->
<!-- tools:itemCount="3"-->
<!-- tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"-->
<!-- tools:listitem="@layout/item_space_simple"-->
<!-- tools:orientation="horizontal" />-->
<ImageView
android:id="@+id/rest1"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:contentDescription="@string/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints"
tools:src="@sample/space_avatars" />
<ImageView
android:id="@+id/rest2"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:contentDescription="@string/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints"
tools:src="@sample/space_avatars" />
<ImageView
android:id="@+id/rest3"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:contentDescription="@string/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints"
tools:src="@sample/space_avatars" />
<ImageView
android:id="@+id/rest4"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:contentDescription="@string/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints"
tools:src="@sample/space_avatars" />
<ImageView
android:id="@+id/rest5"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:contentDescription="@string/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="MissingConstraints"
tools:src="@sample/space_avatars" />
<ImageView
android:id="@+id/rest6"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="@drawable/rounded_rect_shape_8"
android:backgroundTint="?vctr_header_background"
android:contentDescription="@string/avatar"
android:padding="8dp"
android:src="@drawable/ic_more_horizontal"
app:layout_constraintTop_toTopOf="parent"
app:tint="?vctr_content_secondary"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/spacesFlow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:constraint_referenced_ids="rest1,rest2,rest3,rest4, rest5, rest6"
app:flow_horizontalBias="0"
app:flow_horizontalGap="4dp"
app:flow_horizontalStyle="packed"
app:flow_verticalGap="4dp"
app:flow_wrapMode="aligned"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/actionTitle"
app:layout_constraintTop_toBottomOf="@id/listTitle" />
<TextView
android:id="@+id/helperText"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/tap_to_edit_spaces"
android:textColor="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="@id/actionTitle"
app:layout_constraintTop_toBottomOf="@id/spacesFlow"
app:layout_goneMarginTop="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1475,11 +1475,21 @@
<string name="room_settings_room_access_entry_knock">Anyone can knock on the room, members can then accept or reject</string>
<string name="room_settings_room_access_entry_unknown">Unknown access setting (%s)</string>
<string name="room_settings_room_access_private_title">Private</string>
<string name="room_settings_room_access_private_invite_only_title">Private (Invite Only)</string>
<string name="room_settings_room_access_private_description">Only people invited can find and join</string>
<string name="room_settings_room_access_public_title">Public</string>
<string name="room_settings_room_access_public_description">Anyone can find the room and join</string>
<string name="room_settings_room_access_restricted_title">Spaces</string>
<string name="room_settings_room_access_restricted_title">Space members only</string>
<string name="room_settings_room_access_restricted_description">Anyone in a space with this room can find and join it. Only admins of this room can add it to a space.</string>
<string name="room_create_member_of_space_name_can_join">Members of Space %s can find, preview and join.</string>
<string name="allow_space_member_to_find_and_access">Allow space members to find and access.</string>
<string name="spaces_which_can_access">Spaces which can access</string>
<string name="decide_which_spaces_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.</string>
<string name="select_spaces">Select spaces</string>
<string name="tap_to_edit_spaces">Tap to edit spaces</string>
<string name="decide_who_can_find_and_join">Decide who can find and join this room.</string>
<string name="space_you_know_that_contains_this_room">Space you know that contain this room</string>
<string name="other_spaces_or_rooms_you_might_not_know">Other spaces or rooms you might not know</string>
<!-- Room settings: banned users -->
<string name="room_settings_banned_users_title">Banned users</string>
@ -3435,6 +3445,7 @@
<string name="it_may_take_some_time">Please be patient, it may take some time.</string>
<string name="upgrade">Upgrade</string>
<string name="upgrade_required">Upgrade Required</string>
<string name="upgrade_public_room">Upgrade public room</string>
<string name="upgrade_private_room">Upgrade private room</string>
<string name="upgrade_room_warning">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.</string>
@ -3442,6 +3453,7 @@
<string name="upgrade_room_auto_invite">Automatically invite users</string>
<string name="upgrade_room_update_parent_space">Automatically update space parent</string>
<string name="upgrade_room_no_power_to_manage">You need permission to upgrade a room</string>
<string name="allow_anyone_in_room_to_access">Allow anyone in %s to find and access. You can select other spaces too.</string>
<string name="room_using_unstable_room_version">This room is running room version %s, which this homeserver has marked as unstable.</string>
<string name="room_upgrade_to_recommended_version">Upgrade to the recommended room version</string>
@ -3463,4 +3475,9 @@
<string name="error_voice_message_unable_to_record">Cannot record a voice message</string>
<string name="error_voice_message_cannot_reply_or_edit">Cannot reply or edit while voice message is active</string>
<string name="voice_message_reply_content">Voice Message (%1$s)</string>
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
<string name="upgrade_room_for_restricted_note">Please note upgrading will make a new version of the room. All current messages will stay in this archived room.</string>
</resources>