This commit is contained in:
Valere 2021-07-08 17:00:44 +02:00
parent c7c589354e
commit 6c2a917d9f
39 changed files with 1922 additions and 135 deletions

View File

@ -40,7 +40,48 @@ data class HomeServerCapabilities(
*/
val roomVersions: RoomVersionCapabilities? = null
) {
enum class RoomCapabilitySupport {
SUPPORTED,
SUPPORTED_UNSTABLE,
UNSUPPORTED,
UNKNOWN
}
fun isFeatureSupported(feature: String): RoomCapabilitySupport {
if (roomVersions?.capabilities == null) return RoomCapabilitySupport.UNKNOWN
val info = roomVersions.capabilities[feature] ?: return RoomCapabilitySupport.UNSUPPORTED
val preferred = info.preferred ?: info.support.lastOrNull()
val versionCap = roomVersions.supportedVersion.firstOrNull { it.version == preferred }
return when {
versionCap == null -> {
RoomCapabilitySupport.UNKNOWN
}
versionCap.status == RoomVersionStatus.STABLE -> {
RoomCapabilitySupport.SUPPORTED
}
else -> {
RoomCapabilitySupport.SUPPORTED_UNSTABLE
}
}
}
fun isFeatureSupported(feature: String, byRoomVersion: String): Boolean {
if (roomVersions?.capabilities == null) return false
val info = roomVersions.capabilities[feature] ?: return false
return info.preferred == byRoomVersion || info.support.contains(byRoomVersion)
}
fun versionOverrideForFeature(feature: String) : String? {
val cap = roomVersions?.capabilities?.get(feature)
return cap?.preferred ?: cap?.support?.lastOrNull()
}
companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
const val ROOM_CAP_KNOCK = "knock"
const val ROOM_CAP_RESTRICTED = "restricted"
}
}

View File

@ -16,9 +16,12 @@
package org.matrix.android.sdk.api.session.homeserver
import com.squareup.moshi.JsonClass
data class RoomVersionCapabilities(
val defaultRoomVersion: String,
val supportedVersion: List<RoomVersionInfo>
val supportedVersion: List<RoomVersionInfo>,
val capabilities: Map<String, RoomCapabilitySupport>?
)
data class RoomVersionInfo(
@ -26,6 +29,12 @@ data class RoomVersionInfo(
val status: RoomVersionStatus
)
@JsonClass(generateAdapter = true)
data class RoomCapabilitySupport(
val preferred: String?,
val support: List<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 (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.create
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
interface RoomFeaturePreset {
fun updateRoomParams(params: CreateRoomParams)
fun setupInitialStates(): List<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

@ -15,6 +15,7 @@
*/
package org.matrix.android.sdk.internal.crypto.tasks
import dagger.Lazy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
@ -34,7 +35,7 @@ internal interface SendVerificationMessageTask : Task<SendVerificationMessageTas
internal class DefaultSendVerificationMessageTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask,
private val encryptEventTask: Lazy<DefaultEncryptEventTask>,
private val roomAPI: RoomAPI,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val globalErrorReceiver: GlobalErrorReceiver) : SendVerificationMessageTask {
@ -64,7 +65,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
return encryptEventTask.get().execute(EncryptEventTask.Params(
params.event.roomId ?: "",
params.event,
listOf("m.relates_to")

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,26 @@ internal object HomeServerCapabilitiesMapper {
roomVersionsJson ?: return null
return tryOrNull {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let { roomVersions ->
RoomVersionCapabilities(
defaultRoomVersion = it.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION,
supportedVersion = it.available.entries.map { entry ->
RoomVersionInfo(
version = entry.key,
status = if (entry.value == "stable") {
RoomVersionStatus.STABLE
} else {
RoomVersionStatus.UNSTABLE
}
)
}
defaultRoomVersion = roomVersions.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION,
supportedVersion = roomVersions.available?.entries?.map { entry ->
RoomVersionInfo(entry.key, RoomVersionStatus.STABLE
.takeIf { entry.value == "stable" }
?: RoomVersionStatus.UNSTABLE)
}.orEmpty(),
capabilities = roomVersions.roomCapabilities?.entries?.mapNotNull { entry ->
(entry.value as? Map<*, *>)?.let {
val preferred = it["preferred"] as? String ?: return@mapNotNull null
val support = (it["support"] as? List<*>)?.filterIsInstance<String>()
entry.key to RoomCapabilitySupport(preferred, support.orEmpty())
}
}?.toMap() ?: mapOf(
HomeServerCapabilities.ROOM_CAP_RESTRICTED to RoomCapabilitySupport(
preferred = null,
support = listOf("org.matrix.msc3083")
)
)
)
}
}

View File

@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
import timber.log.Timber
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(private val timelineEventMapper: TimelineEventMapper,
@ -38,7 +39,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
}
// typings are updated through the sync where room summary entity gets updated no matter what, so it's ok get there
val typingUsers = typingUsersTracker.getTypingUsers(roomSummaryEntity.roomId)
Timber.i("[${roomSummaryEntity.displayName ?: "?"}] roomSummaryEntity.flattenParentIds: <${roomSummaryEntity.flattenParentIds?.take(400)}>")
return RoomSummary(
roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "",
@ -97,7 +98,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
)
},
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
flattenParentIds = roomSummaryEntity.flattenParentIds?.split('|', ignoreCase = false, limit = 100) ?: emptyList()
)
}
}

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,16 +17,10 @@
package org.matrix.android.sdk.internal.session.room.create
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.DeviceListManager
@ -45,7 +39,6 @@ import javax.inject.Inject
internal class CreateRoomBodyBuilder @Inject constructor(
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
private val crossSigningService: CrossSigningService,
private val deviceListManager: DeviceListManager,
private val identityStore: IdentityStore,
private val fileUploader: FileUploader,
@ -76,19 +69,20 @@ internal class CreateRoomBodyBuilder @Inject constructor(
}
}
if (params.joinRuleRestricted != null) {
params.roomVersion = "org.matrix.msc3083"
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
}
val initialStates = (listOfNotNull(
buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params),
buildAvatarEvent(params),
buildGuestAccess(params),
buildJoinRulesRestricted(params)
)
+ buildCustomInitialStates(params))
params.featurePreset?.updateRoomParams(params)
val initialStates = (
listOfNotNull(
buildEncryptionWithAlgorithmEvent(params),
buildHistoryVisibilityEvent(params),
buildAvatarEvent(params),
buildGuestAccess(params)
)
+ params.featurePreset?.setupInitialStates().orEmpty()
+ buildCustomInitialStates(params)
)
.takeIf { it.isNotEmpty() }
return CreateRoomBody(
@ -158,19 +152,19 @@ internal class CreateRoomBodyBuilder @Inject constructor(
}
}
private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? {
return params.joinRuleRestricted
?.let { allowList ->
Event(
type = EventType.STATE_ROOM_JOIN_RULES,
stateKey = "",
content = RoomJoinRulesContent(
_joinRules = RoomJoinRules.RESTRICTED.value,
allowList = allowList
).toContent()
)
}
}
// private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? {
// return params.joinRuleRestricted
// ?.let { allowList ->
// Event(
// type = EventType.STATE_ROOM_JOIN_RULES,
// stateKey = "",
// content = RoomJoinRulesContent(
// _joinRules = RoomJoinRules.RESTRICTED.value,
// allowList = allowList
// ).toContent()
// )
// }
// }
/**
* Add the crypto algorithm to the room creation parameters.

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

@ -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,56 @@ 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() }
)
}
}
settingsSectionTitleItem {
id("settingsSection")
titleResId(R.string.create_room_settings_section)
}
formSwitchItem {
id("public")
enabled(enableFormElement)
title(host.stringProvider.getString(R.string.create_room_public_title))
summary(host.stringProvider.getString(R.string.create_room_public_description))
switchChecked(viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public)
listener { value ->
host.listener?.setIsPublic(value)
}
}
if (viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) {
if (viewState.roomJoinRules == RoomJoinRules.PUBLIC) {
// Room alias for public room
formEditTextItem {
id("alias")
enabled(enableFormElement)
value(viewState.roomVisibilityType.aliasLocalPart)
value(viewState.aliasLocalPart)
suffixText(":" + viewState.homeServerName)
prefixText("#")
hint(host.stringProvider.getString(R.string.room_alias_address_hint))
@ -137,9 +169,10 @@ class CreateRoomController @Inject constructor(
}
}
}
dividerItem {
id("divider1")
}
// dividerItem {
// id("divider1")
// }
formAdvancedToggleItem {
id("showAdvanced")
title(host.stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced))
@ -169,7 +202,7 @@ class CreateRoomController @Inject constructor(
fun onAvatarChange()
fun onNameChange(newName: String)
fun onTopicChange(newTopic: String)
fun setIsPublic(isPublic: Boolean)
fun selectVisibility()
fun setAliasLocalPart(aliasLocalPart: String)
fun setIsEncrypted(isEncrypted: Boolean)
fun toggleShowAdvanced()

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,35 +176,45 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) }
private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState {
if (action.isPublic) {
copy(
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(""),
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized,
isEncrypted = false
)
} else {
copy(
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Private,
isEncrypted = adminE2EByDefault
)
private fun setVisibility(action: CreateRoomAction.SetVisibility) = setState {
when (action.rule) {
RoomJoinRules.PUBLIC -> {
copy(
roomJoinRules = RoomJoinRules.PUBLIC,
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized,
isEncrypted = false
)
}
RoomJoinRules.RESTRICTED -> {
copy(
roomJoinRules = RoomJoinRules.RESTRICTED,
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized,
isEncrypted = adminE2EByDefault
)
}
// RoomJoinRules.INVITE,
// RoomJoinRules.KNOCK,
// RoomJoinRules.PRIVATE,
else -> {
// default to invite
copy(
roomJoinRules = RoomJoinRules.INVITE,
isEncrypted = adminE2EByDefault
)
}
}
}
private fun setRoomAliasLocalPart(action: CreateRoomAction.SetRoomAliasLocalPart) {
withState { state ->
if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) {
setState {
copy(
roomVisibilityType = CreateRoomViewState.RoomVisibilityType.Public(action.aliasLocalPart),
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized
)
}
}
setState {
copy(
aliasLocalPart = action.aliasLocalPart,
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized
)
}
// Else ignore
}
private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) }
@ -187,8 +224,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
return@withState
}
if (state.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public
&& state.roomVisibilityType.aliasLocalPart.isBlank()) {
if (state.roomJoinRules == RoomJoinRules.PUBLIC && state.aliasLocalPart.isNullOrBlank()) {
// we require an alias for public rooms
setState {
copy(asyncCreateRoomRequest = Fail(CreateRoomFailure.AliasError(RoomAliasError.AliasIsBlank)))
@ -205,15 +241,30 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted private val init
name = state.roomName.takeIf { it.isNotBlank() }
topic = state.roomTopic.takeIf { it.isNotBlank() }
avatarUri = state.avatarUri
when (state.roomVisibilityType) {
is CreateRoomViewState.RoomVisibilityType.Public -> {
when (state.roomJoinRules) {
RoomJoinRules.PUBLIC -> {
// Directory visibility
visibility = RoomDirectoryVisibility.PUBLIC
// Preset
preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
roomAliasName = state.roomVisibilityType.aliasLocalPart
roomAliasName = state.aliasLocalPart
}
is CreateRoomViewState.RoomVisibilityType.Private -> {
RoomJoinRules.RESTRICTED -> {
state.parentSpaceId?.let {
featurePreset = RestrictedRoomPreset(
session.getHomeServerCapabilities(),
listOf(RoomJoinRulesAllowEntry(
state.parentSpaceId,
listOf(state.homeServerName)
))
)
}
}
// RoomJoinRules.KNOCK ->
// RoomJoinRules.PRIVATE ->
// RoomJoinRules.INVITE
else -> {
// by default create invite only
// Directory visibility
visibility = RoomDirectoryVisibility.PRIVATE
// Preset

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,22 @@ class RoomSettingsFragment @Inject constructor(
.show(childFragmentManager, "RoomHistoryVisibilityBottomSheet")
}
override fun onJoinRuleClicked() = withState(viewModel) { state ->
val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules
RoomJoinRuleBottomSheet.newInstance(currentJoinRule)
.show(childFragmentManager, "RoomJoinRuleBottomSheet")
override fun onJoinRuleClicked() {
// = withState(viewModel) { state ->
// val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules
// val allowedRules = if (state.supportsRestricted) {
// listOf(
// RoomJoinRules.INVITE, RoomJoinRules.PUBLIC, RoomJoinRules.RESTRICTED
// )
// } else {
// listOf(
// RoomJoinRules.INVITE, RoomJoinRules.PUBLIC
// )
// }
// RoomJoinRuleBottomSheet.newInstance(currentJoinRule, allowedRules)
// .show(childFragmentManager, "RoomJoinRuleBottomSheet")
startActivity(RoomJoinRuleActivity.newIntent(requireContext(), roomProfileArgs.roomId))
}
override fun onToggleGuestAccess() = withState(viewModel) { state ->

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() {
@ -247,8 +268,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
val summary = state.roomSummary.invoke()
when (val avatarAction = state.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> Unit
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
RoomSettingsViewState.AvatarAction.None -> Unit
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
operationList.add(room.rx().deleteAvatar())
}
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {

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,69 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomprofile.settings.joinrule
import android.content.Context
import android.content.Intent
import com.airbnb.mvrx.MvRx
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
class RoomJoinRuleActivity : VectorBaseActivity<ActivitySimpleBinding>(),
RoomJoinRuleChooseRestrictedViewModel.Factory {
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
private lateinit var roomProfileArgs: RoomProfileArgs
private lateinit var allowListViewModelFactory: RoomJoinRuleChooseRestrictedViewModel.Factory
override fun create(initialState: RoomJoinRuleChooseRestrictedState) = allowListViewModelFactory.create(initialState)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun initUiAndData() {
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
if (isFirstCreation()) {
addFragment(
R.id.simpleFragmentContainer,
RoomJoinRuleFragment::class.java,
roomProfileArgs
)
}
}
//
// override fun onBackPressed() {
// super.onBackPressed()
// }
companion object {
fun newIntent(context: Context, roomId: String): Intent {
val roomProfileArgs = RoomProfileArgs(roomId)
return Intent(context, RoomJoinRuleActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, roomProfileArgs)
}
}
}
}

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 com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericButtonItem
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedState
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import javax.inject.Inject
class RoomJoinRuleAdvancedController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val avatarRenderer: AvatarRenderer
) : TypedEpoxyController<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 }
spaceJoinRuleItem {
id("restricted")
avatarRenderer(host.avatarRenderer)
needUpgrade(restrictedRule.needUpgrade)
selected(state.currentRoomJoinRules == RoomJoinRules.RESTRICTED)
restrictedList(state.updatedAllowList.orEmpty())
listener { host.interactionListener?.didSelectRule(RoomJoinRules.RESTRICTED) }
}
}
// Public
RoomJoinRuleRadioAction(
roomJoinRule = RoomJoinRules.PUBLIC,
description = stringProvider.getString(R.string.room_settings_room_access_public_description),
title = stringProvider.getString(R.string.room_settings_room_access_public_title),
isSelected = state.currentRoomJoinRules == RoomJoinRules.PUBLIC
).toRadioBottomSheetItem().let {
it.listener {
interactionListener?.didSelectRule(RoomJoinRules.PUBLIC)
}
add(it)
}
genericButtonItem {
id("save")
text("")
}
}
}

View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomprofile.settings.joinrule
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.mapOptional
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
data class RoomJoinRuleAdvancedState(
val roomId: String,
val summary: RoomSummary? = null,
val currentRoomJoinRules: RoomJoinRules? = null,
val initialAllowList: List<MatrixItem>? = null,
val updatedAllowList: List<MatrixItem>? = null,
val choices: Async<List<JoinRulesOptionSupport>> = Uninitialized
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}
sealed class RoomJoinRuleAdvancedAction : VectorViewModelAction {
data class SelectJoinRules(val rules: RoomJoinRules) : RoomJoinRuleAdvancedAction()
data class UpdateAllowList(val roomIds: List<String>) : RoomJoinRuleAdvancedAction()
}
sealed class RoomJoinRuleAdvancedEvents : VectorViewEvents {
object SelectAllowList : RoomJoinRuleAdvancedEvents()
}
class RoomJoinRuleAdvancedViewModel @AssistedInject constructor(
@Assisted val initialState: RoomJoinRuleAdvancedState,
private val session: Session,
private val vectorPreferences: VectorPreferences
) : VectorViewModel<RoomJoinRuleAdvancedState, RoomJoinRuleAdvancedAction, RoomJoinRuleAdvancedEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
private val homeServerCapabilities = session.getHomeServerCapabilities()
@AssistedFactory
interface Factory {
fun create(initialState: RoomJoinRuleAdvancedState): RoomJoinRuleAdvancedViewModel
}
companion object : MvRxViewModelFactory<RoomJoinRuleAdvancedViewModel, RoomJoinRuleAdvancedState> {
override fun create(viewModelContext: ViewModelContext, state: RoomJoinRuleAdvancedState): RoomJoinRuleAdvancedViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
init {
val initialAllowList = session.getRoom(initialState.roomId)?.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition)
?.content
?.toModel<RoomJoinRulesContent>()
?.allowList
setState {
val initialAllowItems = initialAllowList.orEmpty().map {
session.getRoomSummary(it.spaceID)?.toMatrixItem()
?: MatrixItem.RoomItem(it.spaceID, null, null)
}
copy(
summary = session.getRoomSummary(initialState.roomId),
initialAllowList = initialAllowItems,
updatedAllowList = initialAllowItems
)
}
// TODO shouldn't be live
room.rx()
.liveStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition)
.mapOptional { it.content.toModel<RoomJoinRulesContent>() }
.unwrap()
.subscribe { content ->
content.joinRules?.let {
var safeRule: RoomJoinRules = it
// server is not really checking that, just to be sure let's check
val restrictedSupportedByThisVersion = homeServerCapabilities
.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED, room.getRoomVersion())
if (it == RoomJoinRules.RESTRICTED
&& !restrictedSupportedByThisVersion) {
safeRule = RoomJoinRules.INVITE
}
val allowList = if (safeRule == RoomJoinRules.RESTRICTED) content.allowList else null
val restrictedSupport = homeServerCapabilities.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED)
val couldUpgradeToRestricted = when (restrictedSupport) {
HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED -> true
HomeServerCapabilities.RoomCapabilitySupport.SUPPORTED_UNSTABLE -> vectorPreferences.labsUseExperimentalRestricted()
else -> false
}
val choices = if (restrictedSupportedByThisVersion || couldUpgradeToRestricted) {
listOf(
RoomJoinRules.INVITE.toOption(false),
RoomJoinRules.RESTRICTED.toOption(!restrictedSupportedByThisVersion),
RoomJoinRules.PUBLIC.toOption(false)
)
} else {
listOf(
RoomJoinRules.INVITE.toOption(false),
RoomJoinRules.PUBLIC.toOption(false)
)
}
setState {
copy(
currentRoomJoinRules = safeRule,
choices = Success(choices)
)
}
}
}
.disposeOnClear()
}
override fun handle(action: RoomJoinRuleAdvancedAction) {
when (action) {
is RoomJoinRuleAdvancedAction.SelectJoinRules -> handleSelectRule(action)
is RoomJoinRuleAdvancedAction.UpdateAllowList -> handleUpdateAllowList(action)
}
}
fun handleUpdateAllowList(action: RoomJoinRuleAdvancedAction.UpdateAllowList) = withState { state ->
setState {
copy(
updatedAllowList = action.roomIds.map {
session.getRoomSummary(it)?.toMatrixItem() ?: MatrixItem.RoomItem(it, null, null)
}
)
}
}
fun handleSelectRule(action: RoomJoinRuleAdvancedAction.SelectJoinRules) = withState { state ->
if (action.rules == RoomJoinRules.RESTRICTED) {
// open space select?
_viewEvents.post(RoomJoinRuleAdvancedEvents.SelectAllowList)
}
setState {
copy(
currentRoomJoinRules = action.rules
)
}
}
}

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,101 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomprofile.settings.joinrule
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentJoinRulesRecyclerBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.settings.joinrule.advanced.RoomJoinRuleChooseRestrictedViewModel
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import javax.inject.Inject
class RoomJoinRuleFragment @Inject constructor(
val controller: RoomJoinRuleAdvancedController,
// val viewModelFactory: RoomJoinRuleAdvancedViewModel.Factory,
val avatarRenderer: AvatarRenderer
) : VectorBaseFragment<FragmentJoinRulesRecyclerBinding>(),
// RoomJoinRuleAdvancedViewModel.Factory,
OnBackPressed, RoomJoinRuleAdvancedController.InteractionListener {
private val viewModel: RoomJoinRuleChooseRestrictedViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentJoinRulesRecyclerBinding.inflate(inflater, container, false)
override fun onBackPressed(toolbarButton: Boolean): Boolean {
// TODO
requireActivity().finish()
return true
}
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
controller.setData(state)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
// setupRoomHistoryVisibilitySharedActionViewModel()
// setupRoomJoinRuleSharedActionViewModel()
// controller.callback = this
views.genericRecyclerView.configureWith(controller, hasFixedSize = true)
controller.interactionListener = this
// views.waitingView.waitingStatusText.setText(R.string.please_wait)
// views.waitingView.waitingStatusText.isVisible = true
// // Use the Kotlin extension in the fragment-ktx artifact
// setFragmentResultListener("SelectAllowList") { requestKey, bundle ->
// // We use a String here, but any type that can be put in a Bundle is supported
// bundle.getStringArrayList("bundleKey")?.toList()?.let {
// viewModel.handle(RoomJoinRuleAdvancedAction.UpdateAllowList(it))
// }
// }
viewModel.observeViewEvents {
when (it) {
RoomJoinRuleAdvancedEvents.SelectAllowList -> {
parentFragmentManager.commitTransaction {
setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
val tag = RoomJoinRuleChooseRestrictedFragment::class.simpleName
replace(R.id.simpleFragmentContainer,
RoomJoinRuleChooseRestrictedFragment::class.java,
this@RoomJoinRuleFragment.arguments,
tag
).addToBackStack(tag)
}
}
}
}
}
override fun create(initialState: RoomJoinRuleAdvancedState) = viewModelFactory.create(initialState)
override fun didSelectRule(rules: RoomJoinRules) {
viewModel.handle(RoomJoinRuleAdvancedAction.SelectJoinRules(rules))
}
}

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,99 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomprofile.settings.joinrule
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_joinrule_restricted)
abstract class SpaceJoinRuleItem : VectorEpoxyModel<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)
if (selected) {
holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on))
holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked)
} else {
holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off))
holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked)
}
holder.upgradeRequiredButton.isVisible = needUpgrade
holder.helperText.isVisible = selected
val items = listOf(holder.space1, holder.space2, holder.space3, holder.space4, holder.space5)
holder.spaceMore.isVisible = false
if (restrictedList.isEmpty()) {
holder.listTitle.isVisible = false
items.onEach { it.isVisible = false }
} else {
holder.listTitle.isVisible = true
restrictedList.forEachIndexed { index, matrixItem ->
if (index < items.size) {
avatarRenderer.render(matrixItem, items[index])
} else if (index == items.size) {
holder.spaceMore.isVisible = true
}
}
}
}
class Holder : VectorEpoxyHolder() {
val radioImage by bind<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,114 @@
/*
* 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?) {
val summary = data?.roomSummary?.invoke() ?: 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") }
} else {
results.invoke().forEach { matrixItem ->
roomSelectionItem {
id(matrixItem.id)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
selected(data.selectedRoomList.firstOrNull { it == 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)
}
// val testList = mutableListOf<MatrixItem>()
// for(i in 0..20) {
// testList.addAll(data.knownSpaceParents)
// }
// testList
data.possibleSpaceCandidate.forEach { matrixItem ->
roomSelectionItem {
id(matrixItem.id)
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
selected(data.selectedRoomList.firstOrNull { it == 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.selectedRoomList.firstOrNull { it == matrixItem.id } != null)
itemClickListener { host.listener?.onItemSelected(matrixItem) }
}
}
}
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.util.MatrixItem
sealed class RoomJoinRuleChooseRestrictedActions : VectorViewModelAction {
data class FilterWith(val filter: String): RoomJoinRuleChooseRestrictedActions()
data class ToggleSelection(val matrixItem: MatrixItem): RoomJoinRuleChooseRestrictedActions()
}

View File

@ -0,0 +1,21 @@
/*
* 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

View File

@ -0,0 +1,100 @@
/*
* 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 viewModelFactory: RoomJoinRuleChooseRestrictedViewModel.Factory,
val avatarRenderer: AvatarRenderer
) : VectorBaseFragment<FragmentSpaceRestrictedSelectBinding>(),
// RoomJoinRuleChooseRestrictedViewModel.Factory,
ChooseRestrictedController.Listener,
OnBackPressed {
// override fun create(initialState: RoomJoinRuleChooseRestrictedState) = viewModelFactory.create(initialState)
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 {
// withState(viewModel) {
// // let's return the updated selection list
// setFragmentResult("SelectAllowList", bundleOf("bundleKey" to it.selectedRoomList))
// 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,45 @@
/*
* 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>? = null,
val choices: List<JoinRulesOptionSupport>? = null,
val initialAllowList: List<RoomJoinRulesAllowEntry> = emptyList(),
val selectedRoomList: List<String> = emptyList(),
val possibleSpaceCandidate: List<MatrixItem> = emptyList(),
val unknownRestricted: List<MatrixItem> = emptyList(),
val filter: String = "",
val filteredResults: Async<List<MatrixItem>> = Uninitialized
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}

View File

@ -0,0 +1,235 @@
/*
* 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 androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
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 val initialState: RoomJoinRuleChooseRestrictedState,
private val session: Session,
private val vectorPreferences: VectorPreferences
) : VectorViewModel<RoomJoinRuleChooseRestrictedState, RoomJoinRuleChooseRestrictedActions, RoomJoinRuleChooseRestrictedEvents>(initialState) {
val room = session.getRoom(initialState.roomId)!!
init {
viewModelScope.launch {
session.getRoomSummary(initialState.roomId)?.let { roomSummary ->
val joinRulesContent = session.getRoom(initialState.roomId)?.getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition)
?.content
?.toModel<RoomJoinRulesContent>()
val initialAllowList = joinRulesContent?.allowList
// val others = session.spaceService().getSpaceSummaries(spaceSummaryQueryParams {
// memberships = listOf(Membership.JOIN)
// }).map { it.toMatrixItem() }
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(),
selectedRoomList = initialAllowList.orEmpty().map { it.spaceID },
possibleSpaceCandidate = possibleSpaceCandidate,
unknownRestricted = unknownAllowedOrRooms
)
}
}
}
}
@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)
}.exhaustive
}
private fun handleToggleSelection(action: RoomJoinRuleChooseRestrictedActions.ToggleSelection) = withState { state ->
val selection = state.selectedRoomList.toMutableList()
if (selection.contains(action.matrixItem.id)) {
selection.remove(action.matrixItem.id)
} else {
selection.add(action.matrixItem.id)
}
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
state.initialAllowList.map { it.spaceID }.union(selection).sorted().forEach { entry ->
val summary = session.getRoomSummary(entry)
if (summary == null) {
unknownAllowedOrRooms.add(
MatrixItem.RoomItem(entry, null, null)
)
} else if (summary.roomType != RoomType.SPACE) {
unknownAllowedOrRooms.add(
summary.toMatrixItem()
)
} else if (!state.roomSummary.invoke()!!.flattenParentIds.contains(entry)) {
// it's a space but not a direct parent
unknownAllowedOrRooms.add(
summary.toMatrixItem()
)
} else {
// nop
}
}
setState {
copy(
selectedRoomList = selection.toList(),
unknownRestricted = unknownAllowedOrRooms
)
}
}
private fun handleFilter(action: RoomJoinRuleChooseRestrictedActions.FilterWith) {
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(initialState.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,13 +95,26 @@ 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(
RoomJoinRulesAllowEntry(
spaceID = spaceID,
via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList()
this.featurePreset = RestrictedRoomPreset(
homeServerCapabilities,
listOf(
RoomJoinRulesAllowEntry(
spaceID = spaceID,
via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList()
)
)
)
if (e2eByDefault) {

View File

@ -0,0 +1,58 @@
<?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
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/cancel" />
<Button
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">
<androidx.appcompat.widget.Toolbar
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>
</androidx.appcompat.widget.Toolbar>
<!-- <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>