Merge pull request #3302 from vector-im/feature/bca/spaces_admin_manage

Feature/bca/spaces admin manage
This commit is contained in:
Valere 2021-05-11 08:04:27 +02:00 committed by GitHub
commit d81d971ce0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1481 additions and 102 deletions

View File

@ -29,5 +29,6 @@ data class SpaceChildInfo(
val activeMemberCount: Int?,
val autoJoin: Boolean,
val viaServers: List<String>,
val parentRoomId: String?
val parentRoomId: String?,
val suggested: Boolean?
)

View File

@ -46,5 +46,8 @@ interface Space {
@Throws
suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
@Throws
suspend fun setChildrenSuggested(roomId: String, suggested: Boolean)
// fun getChildren() : List<IRoomSummary>
}

View File

@ -269,5 +269,9 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
obj.setString(RoomSummaryEntityFields.JOIN_RULES_STR, roomJoinRules?.name)
}
realm.schema.get("SpaceChildSummaryEntity")
?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
}
}

View File

@ -89,7 +89,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
order = it.order,
autoJoin = it.autoJoin ?: false,
viaServers = it.viaServers.toList(),
parentRoomId = roomSummaryEntity.roomId
parentRoomId = roomSummaryEntity.roomId,
suggested = it.suggested
)
},
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()

View File

@ -29,6 +29,8 @@ internal open class SpaceChildSummaryEntity(
var autoJoin: Boolean? = null,
var suggested: Boolean? = null,
var childRoomId: String? = null,
// Link to the actual space summary if it is known locally
var childSummaryEntity: RoomSummaryEntity? = null,

View File

@ -63,20 +63,21 @@ internal class DefaultSpace(
}
override suspend fun removeChildren(roomId: String) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
.firstOrNull()
?.content.toModel<SpaceChildContent>()
?: // should we throw here?
return
// val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
// .firstOrNull()
// ?.content.toModel<SpaceChildContent>()
// ?: // should we throw here?
// return
// edit state event and set via to null
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
order = existing.order,
order = null,
via = null,
autoJoin = existing.autoJoin
autoJoin = null,
suggested = null
).toContent()
)
}
@ -94,7 +95,8 @@ internal class DefaultSpace(
body = SpaceChildContent(
order = order,
via = existing.via,
autoJoin = existing.autoJoin
autoJoin = existing.autoJoin,
suggested = existing.suggested
).toContent()
)
}
@ -105,6 +107,11 @@ internal class DefaultSpace(
?.content.toModel<SpaceChildContent>()
?: throw IllegalArgumentException("$roomId is not a child of this space")
if (existing.autoJoin == autoJoin) {
// nothing to do?
return
}
// edit state event and set via to null
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
@ -112,7 +119,31 @@ internal class DefaultSpace(
body = SpaceChildContent(
order = existing.order,
via = existing.via,
autoJoin = autoJoin
autoJoin = autoJoin,
suggested = existing.suggested
).toContent()
)
}
override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
.firstOrNull()
?.content.toModel<SpaceChildContent>()
?: throw IllegalArgumentException("$roomId is not a child of this space")
if (existing.suggested == suggested) {
// nothing to do?
return
}
// edit state event and set via to null
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
order = existing.order,
via = existing.via,
autoJoin = existing.autoJoin,
suggested = suggested
).toContent()
)
}

View File

@ -145,7 +145,8 @@ internal class DefaultSpaceService @Inject constructor(
autoJoin = childStateEvContent.autoJoin ?: false,
viaServers = childStateEvContent.via.orEmpty(),
activeMemberCount = childSummary.numJoinedMembers,
parentRoomId = childStateEv.roomId
parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested
)
}
}.orEmpty()

View File

@ -125,6 +125,8 @@ import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
import im.vector.app.features.spaces.explore.SpaceDirectoryFragment
import im.vector.app.features.spaces.manage.SpaceAddRoomFragment
import im.vector.app.features.spaces.manage.SpaceManageRoomsFragment
import im.vector.app.features.spaces.manage.SpaceSettingsFragment
import im.vector.app.features.spaces.people.SpacePeopleFragment
import im.vector.app.features.spaces.preview.SpacePreviewFragment
import im.vector.app.features.terms.ReviewTermsFragment
@ -684,4 +686,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(SpacePeopleFragment::class)
fun bindSpacePeopleFragment(fragment: SpacePeopleFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SpaceSettingsFragment::class)
fun bindSpaceSettingsFragment(fragment: SpaceSettingsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SpaceManageRoomsFragment::class)
fun bindSpaceManageRoomsFragment(fragment: SpaceManageRoomsFragment): Fragment
}

View File

@ -0,0 +1,41 @@
/*
* 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.core.epoxy
import android.widget.CompoundButton
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
fun VectorEpoxyHolder.setValueOnce(textInputEditText: TextInputEditText, value: String?) {
if (view.isAttachedToWindow) {
// the view is attached to the window
// So it is a rebind of new data and you could ignore it assuming this is text that was already inputted into the view.
// Downside is if you ever wanted to programmatically change the content of the edit text while it is on screen you would not be able to
} else {
textInputEditText.setText(value)
}
}
fun VectorEpoxyHolder.setValueOnce(switchView: SwitchMaterial, switchChecked: Boolean, listener: CompoundButton.OnCheckedChangeListener) {
if (view.isAttachedToWindow) {
// the view is attached to the window
// So it is a rebind of new data and you could ignore it assuming this is value that was already inputted into the view.
} else {
switchView.isChecked = switchChecked
switchView.setOnCheckedChangeListener(listener)
}
}

View File

@ -57,15 +57,3 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_searc
return@OnTouchListener false
})
}
/**
* Update the edit text value, only if necessary and move the cursor to the end of the text
*/
fun EditText.setTextSafe(value: String?) {
if (value != null && text.toString() != value) {
setText(value)
// To fix jumping cursor to the start https://github.com/airbnb/epoxy/issues/426
// Note: there is still a known bug if deleting char in the middle of the text, by long pressing on the backspace button.
setSelection(value.length)
}
}

View File

@ -27,7 +27,7 @@ import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextSafe
import im.vector.app.core.epoxy.setValueOnce
import im.vector.app.core.platform.SimpleTextWatcher
@EpoxyModelClass(layout = R.layout.item_form_text_input)
@ -60,7 +60,7 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
@EpoxyAttribute
var endIconMode: Int? = null
@EpoxyAttribute
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var onTextChange: ((String) -> Unit)? = null
private val onTextChangeListener = object : SimpleTextWatcher() {
@ -76,8 +76,8 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
holder.textInputLayout.error = errorMessage
holder.textInputLayout.endIconMode = endIconMode ?: TextInputLayout.END_ICON_NONE
// Update only if text is different and value is not null
holder.textInputEditText.setTextSafe(value)
holder.setValueOnce(holder.textInputEditText, value)
holder.textInputEditText.isEnabled = enabled
inputType?.let { holder.textInputEditText.inputType = it }
holder.textInputEditText.isSingleLine = singleLine ?: false

View File

@ -26,7 +26,7 @@ import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextSafe
import im.vector.app.core.epoxy.setValueOnce
import im.vector.app.core.platform.SimpleTextWatcher
@EpoxyModelClass(layout = R.layout.item_form_text_input_with_button)
@ -61,8 +61,8 @@ abstract class FormEditTextWithButtonItem : VectorEpoxyModel<FormEditTextWithBut
holder.textInputLayout.isEnabled = enabled
holder.textInputLayout.hint = hint
// Update only if text is different
holder.textInputEditText.setTextSafe(value)
holder.setValueOnce(holder.textInputEditText, value)
holder.textInputEditText.isEnabled = enabled
holder.textInputEditText.addTextChangedListener(onTextChangeListener)

View File

@ -83,7 +83,6 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder<FormEditableS
override fun unbind(holder: Holder) {
avatarRenderer?.clear(holder.image)
GlideApp.with(holder.image).clear(holder.image)
super.unbind(holder)
}

View File

@ -27,7 +27,7 @@ import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextSafe
import im.vector.app.core.epoxy.setValueOnce
import im.vector.app.core.platform.SimpleTextWatcher
@EpoxyModelClass(layout = R.layout.item_form_multiline_text_input)
@ -57,7 +57,7 @@ abstract class FormMultiLineEditTextItem : VectorEpoxyModel<FormMultiLineEditTex
@EpoxyAttribute
var typeFace: Typeface = Typeface.DEFAULT
@EpoxyAttribute
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var onTextChange: ((String) -> Unit)? = null
private val onTextChangeListener = object : SimpleTextWatcher() {
@ -76,8 +76,8 @@ abstract class FormMultiLineEditTextItem : VectorEpoxyModel<FormMultiLineEditTex
holder.textInputEditText.textSize = textSizeSp?.toFloat() ?: 14f
holder.textInputEditText.minLines = minLines
// Update only if text is different and value is not null
holder.textInputEditText.setTextSafe(value)
holder.setValueOnce(holder.textInputEditText, value)
holder.textInputEditText.isEnabled = enabled
holder.textInputEditText.addTextChangedListener(onTextChangeListener)

View File

@ -25,6 +25,7 @@ import com.google.android.material.switchmaterial.SwitchMaterial
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.setValueOnce
import im.vector.app.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_form_switch)
@ -40,7 +41,7 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
var switchChecked: Boolean = false
@EpoxyAttribute
var title: String? = null
var title: CharSequence? = null
@EpoxyAttribute
var summary: String? = null
@ -61,11 +62,10 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
holder.switchView.isEnabled = enabled
holder.switchView.setOnCheckedChangeListener(null)
holder.switchView.isChecked = switchChecked
holder.switchView.setOnCheckedChangeListener { _, isChecked ->
holder.setValueOnce(holder.switchView, switchChecked) { _, isChecked ->
listener?.invoke(isChecked)
}
holder.divider.isVisible = showDivider
}

View File

@ -74,6 +74,7 @@ import im.vector.app.features.share.SharedData
import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet
import im.vector.app.features.spaces.SpaceExploreActivity
import im.vector.app.features.spaces.SpacePreviewActivity
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity
import im.vector.app.features.spaces.people.SpacePeopleActivity
import im.vector.app.features.terms.ReviewTermsActivity
@ -123,7 +124,7 @@ class DefaultNavigator @Inject constructor(
}
}
Navigator.PostSwitchSpaceAction.OpenAddExistingRooms -> {
startActivity(context, SpaceManageActivity.newIntent(context, spaceId), false)
startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false)
}
is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> {
val args = RoomDetailArgs(

View File

@ -27,7 +27,7 @@ import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.setTextSafe
import im.vector.app.core.epoxy.setValueOnce
import im.vector.app.core.platform.SimpleTextWatcher
@EpoxyModelClass(layout = R.layout.item_room_alias_text_input)
@ -62,8 +62,7 @@ abstract class RoomAliasEditItem : VectorEpoxyModel<RoomAliasEditItem.Holder>()
holder.textInputLayout.isEnabled = enabled
holder.textInputLayout.error = errorMessage
// Update only if text is different and value is not null
holder.textInputEditText.setTextSafe(value)
holder.setValueOnce(holder.textInputEditText, value)
holder.textInputEditText.isEnabled = enabled
holder.textInputEditText.addTextChangedListener(onTextChangeListener)
holder.homeServerText.text = homeServer

View File

@ -121,7 +121,7 @@ class RoomSettingsController @Inject constructor(
buildProfileAction(
id = "joinRule",
title = stringProvider.getString(R.string.room_settings_room_access_title),
subtitle = data.getJoinRuleWording(),
subtitle = data.getJoinRuleWording(stringProvider),
dividerColor = dividerColor,
divider = false,
editable = data.actionPermissions.canChangeJoinRule,
@ -142,24 +142,4 @@ class RoomSettingsController @Inject constructor(
}
}
}
private fun RoomSettingsViewState.getJoinRuleWording(): String {
return when (val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules) {
RoomJoinRules.INVITE -> {
stringProvider.getString(R.string.room_settings_room_access_private_title)
}
RoomJoinRules.PUBLIC -> {
stringProvider.getString(R.string.room_settings_room_access_public_title)
}
RoomJoinRules.KNOCK -> {
stringProvider.getString(R.string.room_settings_room_access_entry_knock)
}
RoomJoinRules.RESTRICTED -> {
stringProvider.getString(R.string.room_settings_room_access_restricted_title)
}
else -> {
stringProvider.getString(R.string.room_settings_room_access_entry_unknown, joinRule.value)
}
}
}
}

View File

@ -60,7 +60,8 @@ class RoomSettingsFragment @Inject constructor(
VectorBaseFragment<FragmentRoomSettingGenericBinding>(),
RoomSettingsController.Callback,
OnBackPressed,
GalleryOrCameraDialogHelper.Listener {
GalleryOrCameraDialogHelper.Listener,
RoomSettingsViewModel.Factory {
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel
@ -76,6 +77,10 @@ class RoomSettingsFragment @Inject constructor(
override fun getMenuRes() = R.menu.vector_room_settings
override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel {
return viewModelFactory.create(initialState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java)

View File

@ -17,6 +17,7 @@
package im.vector.app.features.roomprofile.settings
import androidx.core.net.toFile
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@ -55,8 +56,11 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomSettingsViewState): RoomSettingsViewModel? {
val fragment: RoomSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
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")
}
}
@ -123,7 +127,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_JOIN_RULES)
&& powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_ROOM_GUEST_ACCESS)
EventType.STATE_ROOM_GUEST_ACCESS),
canAddChildren = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_SPACE_CHILD)
)
setState { copy(actionPermissions = permissions) }
}

View File

@ -20,6 +20,8 @@ import android.net.Uri
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.roomprofile.RoomProfileArgs
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
@ -51,7 +53,8 @@ data class RoomSettingsViewState(
val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false,
val canChangeHistoryVisibility: Boolean = false,
val canChangeJoinRule: Boolean = false
val canChangeJoinRule: Boolean = false,
val canAddChildren: Boolean = false
)
sealed class AvatarAction {
@ -67,4 +70,24 @@ data class RoomSettingsViewState(
) {
fun hasChanged() = newJoinRules != null || newGuestAccess != null
}
fun getJoinRuleWording(stringProvider: StringProvider): String {
return when (val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules) {
RoomJoinRules.INVITE -> {
stringProvider.getString(R.string.room_settings_room_access_private_title)
}
RoomJoinRules.PUBLIC -> {
stringProvider.getString(R.string.room_settings_room_access_public_title)
}
RoomJoinRules.KNOCK -> {
stringProvider.getString(R.string.room_settings_room_access_entry_knock)
}
RoomJoinRules.RESTRICTED -> {
stringProvider.getString(R.string.room_settings_room_access_restricted_title)
}
else -> {
stringProvider.getString(R.string.room_settings_room_access_entry_unknown, joinRule.value)
}
}
}
}

View File

@ -35,6 +35,7 @@ import im.vector.app.features.navigation.Navigator
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.roomprofile.RoomProfileActivity
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.GlobalScope
@ -94,6 +95,13 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
val powerLevelsHelper = PowerLevelsHelper(powerLevelContent)
val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId)
val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD)
val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR)
val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME)
val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC)
views.spaceSettings.isVisible = canChangeAvatar || canChangeName || canChangeTopic
views.invitePeople.isVisible = canInvite
views.addRooms.isVisible = canAddChild
}.disposeOnDestroyView()
@ -107,9 +115,9 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
navigator.openRoomProfile(requireContext(), spaceArgs.spaceId, RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_MEMBERS)
}
views.spaceSettings.isVisible = vectorPreferences.developerMode()
views.spaceSettings.views.bottomSheetActionClickableZone.debouncedClicks {
navigator.openRoomProfile(requireContext(), spaceArgs.spaceId)
// navigator.openRoomProfile(requireContext(), spaceArgs.spaceId)
startActivity(SpaceManageActivity.newIntent(requireActivity(), spaceArgs.spaceId, ManageType.Settings))
}
views.exploreRooms.views.bottomSheetActionClickableZone.debouncedClicks {
@ -118,7 +126,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
views.addRooms.views.bottomSheetActionClickableZone.debouncedClicks {
dismiss()
startActivity(SpaceManageActivity.newIntent(requireActivity(), spaceArgs.spaceId))
startActivity(SpaceManageActivity.newIntent(requireActivity(), spaceArgs.spaceId, ManageType.AddRooms))
}
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks {

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import android.view.View
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.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_room_to_manage_in_space)
abstract class RoomManageSelectionItem : VectorEpoxyModel<RoomManageSelectionItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var space: Boolean = false
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var suggested: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
if (space) {
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
} else {
avatarRenderer.render(matrixItem, holder.avatarImageView)
}
holder.titleText.text = matrixItem.getBestName()
if (selected) {
holder.checkboxImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_checkbox_on))
holder.checkboxImage.contentDescription = holder.view.context.getString(R.string.a11y_checked)
} else {
holder.checkboxImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_checkbox_off))
holder.checkboxImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked)
}
holder.suggestedText.isVisible = suggested
holder.view.setOnClickListener {
itemClickListener?.onClick(it)
}
}
class Holder : VectorEpoxyHolder() {
val avatarImageView by bind<ImageView>(R.id.itemAddRoomRoomAvatar)
val titleText by bind<TextView>(R.id.itemAddRoomRoomNameText)
val suggestedText by bind<TextView>(R.id.itemManageRoomSuggested)
val checkboxImage by bind<ImageView>(R.id.itemAddRoomRoomCheckBox)
}
}

View File

@ -54,6 +54,11 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
private val session: Session
) : VectorViewModel<SpaceAddRoomsState, SpaceAddRoomActions, SpaceAddRoomsViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel
}
val updatableLiveSpacePageResult: UpdatableLivePageResult by lazy {
session.getFilteredPagedRoomSummariesLive(
roomSummaryQueryParams {
@ -106,11 +111,6 @@ class SpaceAddRoomsViewModel @AssistedInject constructor(
}
}
@AssistedFactory
interface Factory {
fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel
}
companion object : MvRxViewModelFactory<SpaceAddRoomsViewModel, SpaceAddRoomsState> {
override fun create(viewModelContext: ViewModelContext, state: SpaceAddRoomsState): SpaceAddRoomsViewModel? {
val factory = when (viewModelContext) {

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import io.reactivex.functions.Predicate
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
class SpaceChildInfoMatchFilter : Predicate<SpaceChildInfo> {
var filter: String = ""
override fun test(spaceChildInfo: SpaceChildInfo): Boolean {
if (filter.isEmpty()) {
// No filter
return true
}
// if filter is "Jo Do", it should match "John Doe"
return filter.split(" ").all {
spaceChildInfo.name?.contains(it, ignoreCase = true).orFalse()
|| spaceChildInfo.topic?.contains(it, ignoreCase = true).orFalse()
}
}
}

View File

@ -20,28 +20,37 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.databinding.ActivitySimpleLoadingBinding
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs
import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.app.features.roomprofile.RoomProfileArgs
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
@Parcelize
data class SpaceManageArgs(
val spaceId: String
val spaceId: String,
val manageType: ManageType
) : Parcelable
class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceManageSharedViewModel.Factory {
class SpaceManageActivity : VectorBaseActivity<ActivitySimpleLoadingBinding>(),
ToolbarConfigurable,
SpaceManageSharedViewModel.Factory {
@Inject lateinit var sharedViewModelFactory: SpaceManageSharedViewModel.Factory
private lateinit var sharedDirectoryActionViewModel: RoomDirectorySharedActionViewModel
@ -50,12 +59,26 @@ class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceMa
injector.inject(this)
}
override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater)
override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater)
override fun getTitleRes(): Int = R.string.space_add_existing_rooms
val sharedViewModel: SpaceManageSharedViewModel by viewModel()
override fun showWaitingView(text: String?) {
hideKeyboard()
views.waitingView.waitingStatusText.isGone = views.waitingView.waitingStatusText.text.isNullOrBlank()
super.showWaitingView(text)
}
override fun hideWaitingView() {
views.waitingView.waitingStatusText.text = null
views.waitingView.waitingStatusText.isGone = true
views.waitingView.waitingHorizontalProgress.progress = 0
views.waitingView.waitingHorizontalProgress.isVisible = false
super.hideWaitingView()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -72,14 +95,35 @@ class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceMa
val args = intent?.getParcelableExtra<SpaceManageArgs>(MvRx.KEY_ARG)
if (isFirstCreation()) {
val simpleName = SpaceAddRoomFragment::class.java.simpleName
if (supportFragmentManager.findFragmentByTag(simpleName) == null) {
supportFragmentManager.commitTransaction {
replace(R.id.simpleFragmentContainer,
SpaceAddRoomFragment::class.java,
Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) },
simpleName
)
withState(sharedViewModel) {
when (it.manageType) {
ManageType.AddRooms -> {
val simpleName = SpaceAddRoomFragment::class.java.simpleName
if (supportFragmentManager.findFragmentByTag(simpleName) == null) {
supportFragmentManager.commitTransaction {
replace(R.id.simpleFragmentContainer,
SpaceAddRoomFragment::class.java,
Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) },
simpleName
)
}
}
}
ManageType.Settings -> {
val simpleName = SpaceSettingsFragment::class.java.simpleName
if (supportFragmentManager.findFragmentByTag(simpleName) == null && args?.spaceId != null) {
supportFragmentManager.commitTransaction {
replace(R.id.simpleFragmentContainer,
SpaceSettingsFragment::class.java,
Bundle().apply { this.putParcelable(MvRx.KEY_ARG, RoomProfileArgs(args.spaceId)) },
simpleName
)
}
}
}
ManageType.ManageRooms -> {
// no direct access for now
}
}
}
}
@ -90,10 +134,10 @@ class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceMa
finish()
}
SpaceManagedSharedViewEvents.HideLoading -> {
views.simpleActivityWaitingView.isVisible = false
hideWaitingView()
}
SpaceManagedSharedViewEvents.ShowLoading -> {
views.simpleActivityWaitingView.isVisible = true
showWaitingView()
}
SpaceManagedSharedViewEvents.NavigateToCreateRoom -> {
addFragmentToBackstack(
@ -102,17 +146,30 @@ class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceMa
CreateRoomArgs("", parentSpaceId = args?.spaceId)
)
}
SpaceManagedSharedViewEvents.NavigateToManageRooms -> {
args?.spaceId?.let { spaceId ->
addFragmentToBackstack(
R.id.simpleFragmentContainer,
SpaceManageRoomsFragment::class.java,
SpaceManageArgs(spaceId, ManageType.ManageRooms)
)
}
}
}
}
}
companion object {
fun newIntent(context: Context, spaceId: String): Intent {
fun newIntent(context: Context, spaceId: String, manageType: ManageType): Intent {
return Intent(context, SpaceManageActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, SpaceManageArgs(spaceId))
putExtra(MvRx.KEY_ARG, SpaceManageArgs(spaceId, manageType))
}
}
}
override fun create(initialState: SpaceManageViewState) = sharedViewModelFactory.create(initialState)
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import im.vector.app.core.platform.VectorViewModelAction
sealed class SpaceManageRoomViewAction : VectorViewModelAction {
data class ToggleSelection(val roomId: String) : SpaceManageRoomViewAction()
data class UpdateFilter(val filter: String) : SpaceManageRoomViewAction()
object ClearSelection : SpaceManageRoomViewAction()
data class MarkAllAsSuggested(val suggested: Boolean) : SpaceManageRoomViewAction()
object BulkRemove : SpaceManageRoomViewAction()
object RefreshFromServer : SpaceManageRoomViewAction()
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import im.vector.app.core.platform.VectorViewEvents
sealed class SpaceManageRoomViewEvents : VectorViewEvents {
// object BulkActionSuccess: SpaceManageRoomViewEvents()
data class BulkActionFailure(val errorList: List<Throwable>) : SpaceManageRoomViewEvents()
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
data class SpaceManageRoomViewState(
val spaceId: String,
val spaceSummary: Async<RoomSummary> = Uninitialized,
val childrenInfo: Async<List<SpaceChildInfo>> = Uninitialized,
val selectedRooms: List<String> = emptyList(),
val currentFilter: String = "",
val actionState: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: SpaceManageArgs) : this(
spaceId = args.spaceId
)
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class SpaceManageRoomsController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter
) : TypedEpoxyController<SpaceManageRoomViewState>() {
interface Listener {
fun toggleSelection(childInfo: SpaceChildInfo)
fun retry()
}
var listener: Listener? = null
private val matchFilter = SpaceChildInfoMatchFilter()
override fun buildModels(data: SpaceManageRoomViewState?) {
val roomListAsync = data?.childrenInfo
if (roomListAsync is Incomplete) {
loadingItem { id("loading") }
return
}
if (roomListAsync is Fail) {
errorWithRetryItem {
id("Api Error")
text(errorFormatter.toHumanReadable(roomListAsync.error))
listener { listener?.retry() }
}
return
}
val roomList = roomListAsync?.invoke() ?: return
val directChildren = roomList.filter {
it.parentRoomId == data.spaceId
/** Only direct children **/
}
matchFilter.filter = data.currentFilter
val filteredResult = directChildren.filter { matchFilter.test(it) }
filteredResult.forEach { childInfo ->
roomManageSelectionItem {
id(childInfo.childRoomId)
matrixItem(childInfo.toMatrixItem())
avatarRenderer(avatarRenderer)
suggested(childInfo.suggested ?: false)
space(childInfo.roomType == RoomType.SPACE)
selected(data.selectedRooms.contains(childInfo.childRoomId))
itemClickListener(DebouncedClickListener({
listener?.toggleSelection(childInfo)
}))
}
}
}
}

View File

@ -0,0 +1,195 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.view.ActionMode.Callback
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.appcompat.queryTextChanges
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import im.vector.app.databinding.FragmentSpaceAddRoomsBinding
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class SpaceManageRoomsFragment @Inject constructor(
private val viewModelFactory: SpaceManageRoomsViewModel.Factory,
private val epoxyController: SpaceManageRoomsController
) : VectorBaseFragment<FragmentSpaceAddRoomsBinding>(),
SpaceManageRoomsViewModel.Factory,
OnBackPressed,
SpaceManageRoomsController.Listener,
Callback {
private val viewModel by fragmentViewModel(SpaceManageRoomsViewModel::class)
private val sharedViewModel: SpaceManageSharedViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceAddRoomsBinding.inflate(inflater)
override fun onBackPressed(toolbarButton: Boolean): Boolean {
parentFragmentManager.popBackStack()
return true
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.addRoomToSpaceToolbar)
views.appBarTitle.text = getString(R.string.space_manage_rooms_and_spaces)
views.createNewRoom.isVisible = false
epoxyController.listener = this
views.roomList.configureWith(epoxyController, hasFixedSize = true, showDivider = true)
views.publicRoomsFilter.queryTextChanges()
.debounce(200, TimeUnit.MILLISECONDS)
.subscribeBy {
viewModel.handle(SpaceManageRoomViewAction.UpdateFilter(it.toString()))
}
.disposeOnDestroyView()
viewModel.selectSubscribe(SpaceManageRoomViewState::actionState) { actionState ->
when (actionState) {
is Loading -> {
sharedViewModel.handle(SpaceManagedSharedAction.ShowLoading)
}
else -> {
sharedViewModel.handle(SpaceManagedSharedAction.HideLoading)
}
}
}
viewModel.observeViewEvents {
when (it) {
is SpaceManageRoomViewEvents.BulkActionFailure -> {
vectorBaseActivity.toast(errorFormatter.toHumanReadable(it.errorList.firstOrNull()))
}
}
}
}
override fun onDestroyView() {
epoxyController.listener = null
views.roomList.cleanup()
super.onDestroyView()
}
override fun create(initialState: SpaceManageRoomViewState) = viewModelFactory.create(initialState)
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
state.spaceSummary.invoke()?.let {
views.appBarSpaceInfo.text = it.displayName
}
if (state.selectedRooms.isNotEmpty()) {
if (currentActionMode == null) {
views.addRoomToSpaceToolbar.isVisible = true
vectorBaseActivity.startSupportActionMode(this)
} else {
currentActionMode?.title = "${state.selectedRooms.size} selected"
}
// views.addRoomToSpaceToolbar.isVisible = false
// views.addRoomToSpaceToolbar.startActionMode(this)
} else {
currentActionMode?.finish()
}
Unit
}
var currentActionMode: ActionMode? = null
override fun toggleSelection(childInfo: SpaceChildInfo) {
viewModel.handle(SpaceManageRoomViewAction.ToggleSelection(childInfo.childRoomId))
}
override fun retry() {
viewModel.handle(SpaceManageRoomViewAction.RefreshFromServer)
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
val inflater = mode?.menuInflater
inflater?.inflate(R.menu.menu_manage_space, menu)
withState(viewModel) {
mode?.title = resources.getQuantityString(R.plurals.room_details_selected, it.selectedRooms.size, it.selectedRooms.size)
}
currentActionMode = mode
views.addRoomToSpaceToolbar.isVisible = false
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
withState(viewModel) { state ->
// check if we show mark as suggested or not
val areAllSuggested = state.childrenInfo.invoke().orEmpty().filter { state.selectedRooms.contains(it.childRoomId) }
.all { it.suggested == true }
menu?.findItem(R.id.action_mark_as_suggested)?.isVisible = !areAllSuggested
menu?.findItem(R.id.action_mark_as_not_suggested)?.isVisible = areAllSuggested
}
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.action_delete -> {
handleDeleteSelection()
}
R.id.action_mark_as_suggested -> {
viewModel.handle(SpaceManageRoomViewAction.MarkAllAsSuggested(true))
}
R.id.action_mark_as_not_suggested -> {
viewModel.handle(SpaceManageRoomViewAction.MarkAllAsSuggested(false))
}
else -> {
}
}
mode?.finish()
return true
}
private fun handleDeleteSelection() {
viewModel.handle(SpaceManageRoomViewAction.BulkRemove)
}
override fun onDestroyActionMode(mode: ActionMode?) {
// should force a refresh
currentActionMode = null
viewModel.handle(SpaceManageRoomViewAction.ClearSelection)
views.coordinatorLayout.post {
if (isAdded) {
TransitionManager.beginDelayedTransition(views.coordinatorLayout)
views.addRoomToSpaceToolbar.isVisible = true
}
}
}
}

View File

@ -0,0 +1,182 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
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.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
class SpaceManageRoomsViewModel @AssistedInject constructor(
@Assisted val initialState: SpaceManageRoomViewState,
private val session: Session
) : VectorViewModel<SpaceManageRoomViewState, SpaceManageRoomViewAction, SpaceManageRoomViewEvents>(initialState) {
init {
val spaceSummary = session.getRoomSummary(initialState.spaceId)
setState {
copy(
spaceSummary = spaceSummary?.let { Success(it) } ?: Uninitialized,
childrenInfo = Loading()
)
}
viewModelScope.launch(Dispatchers.IO) {
val apiResult = runCatchingToAsync {
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second
}
setState {
copy(
childrenInfo = apiResult
)
}
}
}
@AssistedFactory
interface Factory {
fun create(initialState: SpaceManageRoomViewState): SpaceManageRoomsViewModel
}
companion object : MvRxViewModelFactory<SpaceManageRoomsViewModel, SpaceManageRoomViewState> {
override fun create(viewModelContext: ViewModelContext, state: SpaceManageRoomViewState): SpaceManageRoomsViewModel? {
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")
}
}
override fun handle(action: SpaceManageRoomViewAction) {
when (action) {
is SpaceManageRoomViewAction.ToggleSelection -> handleToggleSelection(action)
is SpaceManageRoomViewAction.UpdateFilter -> {
setState { copy(currentFilter = action.filter) }
}
SpaceManageRoomViewAction.ClearSelection -> {
setState { copy(selectedRooms = emptyList()) }
}
SpaceManageRoomViewAction.BulkRemove -> {
handleBulkRemove()
}
is SpaceManageRoomViewAction.MarkAllAsSuggested -> {
handleBulkMarkAsSuggested(action.suggested)
}
SpaceManageRoomViewAction.RefreshFromServer -> {
refreshSummaryAPI()
}
}
}
private fun handleBulkRemove() = withState { state ->
setState { copy(actionState = Loading()) }
val selection = state.selectedRooms
session.coroutineScope.launch(Dispatchers.IO) {
val errorList = mutableListOf<Throwable>()
selection.forEach {
try {
session.spaceService().getSpace(state.spaceId)?.removeChildren(it)
} catch (failure: Throwable) {
errorList.add(failure)
}
}
if (errorList.isEmpty()) {
// success
} else {
_viewEvents.post(SpaceManageRoomViewEvents.BulkActionFailure(errorList))
}
refreshSummaryAPI()
setState { copy(actionState = Uninitialized) }
}
}
private fun handleBulkMarkAsSuggested(suggested: Boolean) = withState { state ->
setState { copy(actionState = Loading()) }
val selection = state.childrenInfo.invoke()?.filter {
state.selectedRooms.contains(it.childRoomId)
}.orEmpty()
session.coroutineScope.launch(Dispatchers.IO) {
val errorList = mutableListOf<Throwable>()
selection.forEach { info ->
try {
session.spaceService().getSpace(state.spaceId)?.addChildren(
roomId = info.childRoomId,
viaServers = info.viaServers,
order = info.order,
suggested = suggested,
autoJoin = info.autoJoin
)
} catch (failure: Throwable) {
errorList.add(failure)
}
}
if (errorList.isEmpty()) {
// success
} else {
_viewEvents.post(SpaceManageRoomViewEvents.BulkActionFailure(errorList))
}
refreshSummaryAPI()
setState { copy(actionState = Uninitialized) }
}
}
private fun refreshSummaryAPI() {
setState {
copy(
childrenInfo = Loading()
)
}
viewModelScope.launch(Dispatchers.IO) {
val apiResult = runCatchingToAsync {
session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second
}
setState {
copy(
childrenInfo = apiResult
)
}
}
}
private fun handleToggleSelection(action: SpaceManageRoomViewAction.ToggleSelection) = withState { state ->
val existing = state.selectedRooms.toMutableList()
if (existing.contains(action.roomId)) {
existing.remove(action.roomId)
} else {
existing.add(action.roomId)
}
setState {
copy(
selectedRooms = existing.toList()
)
}
}
}

View File

@ -55,6 +55,7 @@ class SpaceManageSharedViewModel @AssistedInject constructor(
SpaceManagedSharedAction.HideLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.HideLoading)
SpaceManagedSharedAction.ShowLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.ShowLoading)
SpaceManagedSharedAction.CreateRoom -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToCreateRoom)
SpaceManagedSharedAction.ManageRooms -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToManageRooms)
}
}
}

View File

@ -18,10 +18,17 @@ package im.vector.app.features.spaces.manage
import com.airbnb.mvrx.MvRxState
enum class ManageType {
AddRooms,
Settings,
ManageRooms
}
data class SpaceManageViewState(
val spaceId: String = ""
val spaceId: String = "",
val manageType: ManageType
) : MvRxState {
constructor(args: SpaceManageArgs) : this(
spaceId = args.spaceId
spaceId = args.spaceId,
manageType = args.manageType
)
}

View File

@ -23,4 +23,5 @@ sealed class SpaceManagedSharedAction : VectorViewModelAction {
object ShowLoading : SpaceManagedSharedAction()
object HideLoading : SpaceManagedSharedAction()
object CreateRoom : SpaceManagedSharedAction()
object ManageRooms : SpaceManagedSharedAction()
}

View File

@ -23,4 +23,5 @@ sealed class SpaceManagedSharedViewEvents : VectorViewEvents {
object ShowLoading : SpaceManagedSharedViewEvents()
object HideLoading : SpaceManagedSharedViewEvents()
object NavigateToCreateRoom : SpaceManagedSharedViewEvents()
object NavigateToManageRooms : SpaceManagedSharedViewEvents()
}

View File

@ -0,0 +1,172 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formEditableSquareAvatarItem
import im.vector.app.features.form.formMultiLineEditTextItem
import im.vector.app.features.form.formSwitchItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.settings.RoomSettingsViewState
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class SpaceSettingsController @Inject constructor(
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
colorProvider: ColorProvider,
private val vectorPreferences: VectorPreferences
) : TypedEpoxyController<RoomSettingsViewState>() {
interface Callback {
// Delete the avatar, or cancel an avatar change
fun onAvatarDelete()
fun onAvatarChange()
fun onNameChanged(name: String)
fun onTopicChanged(topic: String)
fun onHistoryVisibilityClicked()
fun onJoinRuleClicked()
fun onToggleGuestAccess()
fun onDevTools()
fun onDevRoomSettings()
fun onManageRooms()
fun setIsPublic(public: Boolean)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var callback: Callback? = null
override fun buildModels(data: RoomSettingsViewState?) {
val roomSummary = data?.roomSummary?.invoke() ?: return
formEditableSquareAvatarItem {
id("avatar")
enabled(data.actionPermissions.canChangeAvatar)
when (val avatarAction = data.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> {
// Use the current value
avatarRenderer(avatarRenderer)
// We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl))
}
RoomSettingsViewState.AvatarAction.DeleteAvatar ->
imageUri(null)
is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
imageUri(avatarAction.newAvatarUri)
}
clickListener { callback?.onAvatarChange() }
deleteListener { callback?.onAvatarDelete() }
}
buildProfileSection(
stringProvider.getString(R.string.settings)
)
formEditTextItem {
id("name")
enabled(data.actionPermissions.canChangeName)
value(data.newName ?: roomSummary.displayName)
hint(stringProvider.getString(R.string.create_room_name_hint))
showBottomSeparator(false)
onTextChange { text ->
callback?.onNameChanged(text)
}
}
formMultiLineEditTextItem {
id("topic")
enabled(data.actionPermissions.canChangeTopic)
value(data.newTopic ?: roomSummary.topic)
hint(stringProvider.getString(R.string.create_space_topic_hint))
showBottomSeparator(false)
onTextChange { text ->
callback?.onTopicChanged(text)
}
}
if (vectorPreferences.labsUseExperimentalRestricted()) {
buildProfileAction(
id = "joinRule",
title = stringProvider.getString(R.string.room_settings_room_access_title),
subtitle = data.getJoinRuleWording(stringProvider),
dividerColor = dividerColor,
divider = true,
editable = data.actionPermissions.canChangeJoinRule,
action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() }
)
} else {
val isPublic = (data.newRoomJoinRules.newJoinRules ?: data.currentRoomJoinRules) == RoomJoinRules.PUBLIC
formSwitchItem {
id("isPublic")
enabled(data.actionPermissions.canChangeJoinRule)
title(stringProvider.getString(R.string.make_this_space_public))
switchChecked(isPublic)
listener { value ->
callback?.setIsPublic(value)
}
}
}
buildProfileAction(
id = "manage_rooms",
title = stringProvider.getString(R.string.space_settings_manage_rooms),
// subtitle = data.getJoinRuleWording(stringProvider),
dividerColor = dividerColor,
divider = vectorPreferences.developerMode(),
editable = data.actionPermissions.canAddChildren,
action = {
if (data.actionPermissions.canAddChildren) callback?.onManageRooms()
}
)
if (vectorPreferences.developerMode()) {
buildProfileAction(
id = "dev_tools",
title = stringProvider.getString(R.string.settings_dev_tools),
icon = R.drawable.ic_verification_glasses,
tintIcon = false,
dividerColor = dividerColor,
divider = true,
action = {
callback?.onDevTools()
}
)
buildProfileAction(
id = "room_tools",
title = stringProvider.getString(R.string.room_list_quick_actions_room_settings),
icon = R.drawable.ic_verification_glasses,
tintIcon = false,
dividerColor = dividerColor,
divider = false,
action = {
callback?.onDevRoomSettings()
}
)
}
}
}

View File

@ -0,0 +1,267 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.spaces.manage
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.utils.toast
import im.vector.app.databinding.FragmentRoomSettingGenericBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.app.features.roomprofile.settings.RoomSettingsAction
import im.vector.app.features.roomprofile.settings.RoomSettingsViewEvents
import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel
import im.vector.app.features.roomprofile.settings.RoomSettingsViewState
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet
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.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.UUID
import javax.inject.Inject
class SpaceSettingsFragment @Inject constructor(
private val epoxyController: SpaceSettingsController,
private val colorProvider: ColorProvider,
val viewModelFactory: RoomSettingsViewModel.Factory,
private val avatarRenderer: AvatarRenderer,
private val drawableProvider: DrawableProvider
) : VectorBaseFragment<FragmentRoomSettingGenericBinding>(),
RoomSettingsViewModel.Factory,
SpaceSettingsController.Callback,
GalleryOrCameraDialogHelper.Listener,
OnBackPressed {
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
private val sharedViewModel: SpaceManageSharedViewModel by activityViewModel()
private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentRoomSettingGenericBinding.inflate(inflater)
private val roomProfileArgs: RoomProfileArgs by args()
override fun getMenuRes() = R.menu.vector_room_settings
override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel {
return viewModelFactory.create(initialState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.roomSettingsToolbar)
// roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
// setupRoomHistoryVisibilitySharedActionViewModel()
setupRoomJoinRuleSharedActionViewModel()
epoxyController.callback = this
views.roomSettingsRecyclerView.configureWith(epoxyController, hasFixedSize = true)
views.waitingView.waitingStatusText.setText(R.string.please_wait)
views.waitingView.waitingStatusText.isVisible = true
viewModel.observeViewEvents {
when (it) {
is RoomSettingsViewEvents.Failure -> showFailure(it.throwable)
RoomSettingsViewEvents.Success -> showSuccess()
RoomSettingsViewEvents.GoBack -> {
ignoreChanges = true
vectorBaseActivity.onBackPressed()
}
}.exhaustive
}
}
override fun onDestroyView() {
epoxyController.callback = null
views.roomSettingsRecyclerView.cleanup()
super.onDestroyView()
}
override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { state ->
menu.findItem(R.id.roomSettingsSaveAction).isVisible = state.showSaveAction
}
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.roomSettingsSaveAction) {
viewModel.handle(RoomSettingsAction.Save)
}
return super.onOptionsItemSelected(item)
}
private fun renderRoomSummary(state: RoomSettingsViewState) {
views.waitingView.root.isVisible = state.isLoading
state.roomSummary()?.let {
views.roomSettingsToolbarTitleView.text = it.displayName
views.roomSettingsToolbarTitleView.setCompoundDrawablesWithIntrinsicBounds(
null,
null,
drawableProvider.getDrawable(R.drawable.ic_beta_pill),
null
)
avatarRenderer.renderSpace(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
}
invalidateOptionsMenu()
}
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
renderRoomSummary(state)
}
private fun setupRoomJoinRuleSharedActionViewModel() {
roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java)
roomJoinRuleSharedActionViewModel
.observe()
.subscribe { action ->
viewModel.handle(RoomSettingsAction.SetRoomJoinRule(action.roomJoinRule))
}
.disposeOnDestroyView()
}
private var ignoreChanges = false
override fun onBackPressed(toolbarButton: Boolean): Boolean {
if (ignoreChanges) return false
return withState(viewModel) {
return@withState if (it.showSaveAction) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.warning_unsaved_change)
.setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ ->
viewModel.handle(RoomSettingsAction.Cancel)
}
.setNegativeButton(R.string.cancel, null)
.show()
true
} else {
false
}
}
}
private fun showSuccess() {
activity?.toast(R.string.room_settings_save_success)
}
override fun onNameChanged(name: String) {
viewModel.handle(RoomSettingsAction.SetRoomName(name))
}
override fun onTopicChanged(topic: String) {
viewModel.handle(RoomSettingsAction.SetRoomTopic(topic))
}
override fun onHistoryVisibilityClicked() {
// N/A for space settings screen
}
override fun onJoinRuleClicked() = withState(viewModel) { state ->
val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules
RoomJoinRuleBottomSheet.newInstance(currentJoinRule)
.show(childFragmentManager, "RoomJoinRuleBottomSheet")
}
override fun onToggleGuestAccess() = withState(viewModel) { state ->
val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess
val toggled = if (currentGuestAccess == GuestAccess.Forbidden) GuestAccess.CanJoin else GuestAccess.Forbidden
viewModel.handle(RoomSettingsAction.SetRoomGuestAccess(toggled))
}
override fun onDevTools() = withState(viewModel) { state ->
navigator.openDevTools(requireContext(), state.roomId)
}
override fun onDevRoomSettings() = withState(viewModel) { state ->
navigator.openRoomProfile(requireContext(), state.roomId)
}
override fun onManageRooms() {
sharedViewModel.handle(SpaceManagedSharedAction.ManageRooms)
}
override fun setIsPublic(public: Boolean) {
if (public) {
viewModel.handle(RoomSettingsAction.SetRoomJoinRule(RoomJoinRules.PUBLIC))
viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(RoomHistoryVisibility.WORLD_READABLE))
} else {
viewModel.handle(RoomSettingsAction.SetRoomJoinRule(RoomJoinRules.INVITE))
viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(RoomHistoryVisibility.INVITED))
}
}
override fun onImageReady(uri: Uri?) {
uri ?: return
viewModel.handle(
RoomSettingsAction.SetAvatarAction(
RoomSettingsViewState.AvatarAction.UpdateAvatar(
newAvatarUri = uri,
newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString())
)
)
}
override fun onAvatarDelete() {
withState(viewModel) {
when (it.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> {
viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar))
}
RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
/* Should not happen */
}
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
// Cancel the update of the avatar
viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.None))
}
}
}
}
override fun onAvatarChange() {
galleryOrCameraDialogHelper.show()
}
}

View File

@ -94,12 +94,10 @@
android:id="@+id/spaceSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:actionTitle="@string/settings"
app:leftIcon="@drawable/ic_settings_root_general"
app:tint="?attr/riotx_text_primary"
app:titleTextColor="?attr/riotx_text_primary"
tools:visibility="visible" />
app:titleTextColor="?attr/riotx_text_primary"/>
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/exploreRooms"

View File

@ -5,7 +5,7 @@
android:id="@+id/rootConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_header_panel_background">
android:background="?riotx_background">
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomSettingsToolbar"

View File

@ -50,7 +50,7 @@
android:id="@+id/formSwitchDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?riotx_header_panel_border_mobile"
android:background="?vctr_list_divider_color"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View File

@ -0,0 +1,73 @@
<?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:id="@+id/itemPublicRoomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp">
<ImageView
android:id="@+id/itemAddRoomRoomAvatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/avatar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/itemAddRoomRoomNameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="17dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/itemManageRoomSuggested"
app:layout_constraintStart_toEndOf="@id/itemAddRoomRoomAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@sample/matrix.json/data/roomName" />
<TextView
android:id="@+id/itemManageRoomSuggested"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/itemAddRoomRoomCheckBox"
app:layout_constraintStart_toEndOf="@id/itemAddRoomRoomNameText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
android:text="@string/space_suggested" />
<ImageView
android:id="@+id/itemAddRoomRoomCheckBox"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/a11y_checked"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/itemManageRoomSuggested"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_checkbox_on" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:title="@string/delete"
android:icon="@drawable/ic_delete_unsent_messages"
app:showAsAction="always" />
<item
android:id="@+id/action_mark_as_suggested"
android:title="@string/space_mark_as_suggested"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_mark_as_not_suggested"
android:title="@string/space_mark_as_not_suggested"
app:showAsAction="ifRoom" />
</menu>

View File

@ -3356,4 +3356,10 @@
<string name="user_invites_you">%s invites you</string>
<string name="looking_for_someone_not_in_space">Looking for someone not in %s?</string>
<string name="space_settings_manage_rooms">Manage rooms</string>
<string name="make_this_space_public">Make this space public</string>
<string name="space_suggested">Suggested</string>
<string name="space_mark_as_suggested">Mark as suggested</string>
<string name="space_mark_as_not_suggested">Mark as not suggested</string>
<string name="space_manage_rooms_and_spaces">Manage rooms and spaces</string>
</resources>

View File

@ -11,6 +11,13 @@
<item name="android:background">?riotx_background</item>
</style>
<style name="ActionModeTheme" parent="Widget.AppCompat.ActionMode">
<item name="background">?riotx_background</item>
<item name="titleTextStyle">@style/Vector.Toolbar.Title</item>
<item name="subtitleTextStyle">@style/Vector.Toolbar.SubTitle</item>
<item name="actionMenuTextColor">?colorOnPrimary</item>
</style>
<style name="VectorToolbarStyle" parent="VectorToolbarStyleWithPadding">
<item name="contentInsetStartWithNavigation">0dp</item>
</style>
@ -65,7 +72,7 @@
<!-- actionbar icons color -->
<style name="Vector.ActionBarTheme" parent="ThemeOverlay.MaterialComponents.ActionBar">
<item name="colorControlNormal">@android:color/white</item>
<item name="colorControlNormal">?android:attr/textColorPrimary</item>
</style>
<!-- custom action bar -->

View File

@ -181,6 +181,8 @@
<!-- chat effect -->
<item name="vctr_chat_effect_snow_background">@android:color/transparent</item>
<item name="actionModeStyle">@style/ActionModeTheme</item>
</style>
<style name="AppTheme.Dark" parent="AppTheme.Base.Dark" />

View File

@ -183,6 +183,9 @@
<!-- chat effect -->
<item name="vctr_chat_effect_snow_background">@color/black_alpha</item>
<item name="actionModeStyle">@style/ActionModeTheme</item>
</style>
<style name="AppTheme.Light" parent="AppTheme.Base.Light" />

View File

@ -7,6 +7,7 @@
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:icon="@drawable/ic_verification_glasses"
android:summary="@string/settings_developer_mode_summary"
android:title="@string/settings_developer_mode" />