Commit f9e608a7 authored by Valere's avatar Valere
Browse files

Manage Spaces as Admin

parent 5be3faf9
......@@ -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?
)
......@@ -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>
}
......@@ -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)
}
}
......@@ -89,7 +89,9 @@ 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,
// What to do here?
suggested = it.suggested
)
},
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
......
......@@ -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,
......
......@@ -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,34 @@ 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,
stateKey = roomId,
body = SpaceChildContent(
order = existing.order,
via = existing.via,
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,
......@@ -112,7 +142,8 @@ internal class DefaultSpace(
body = SpaceChildContent(
order = existing.order,
via = existing.via,
autoJoin = autoJoin
autoJoin = existing.autoJoin,
suggested = suggested
).toContent()
)
}
......
......@@ -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()
......
......@@ -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
}
......@@ -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(
......
......@@ -27,6 +27,7 @@ import im.vector.app.features.form.formEditableAvatarItem
import im.vector.app.features.form.formSwitchItem
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
import im.vector.app.features.roomprofile.settings.RoomSettingsViewState.Companion.getJoinRuleWording
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
......@@ -121,7 +122,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 +143,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)
}
}
}
}
......@@ -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)
......
......@@ -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) }
}
......
......@@ -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,26 @@ data class RoomSettingsViewState(
) {
fun hasChanged() = newJoinRules != null || newGuestAccess != null
}
companion object {
fun RoomSettingsViewState.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)
}
}
}
}
}
......@@ -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 {
......
/*
* 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)
}
}
......@@ -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) {
......
......@@ -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,6 +95,9 @@ class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceMa
val args = intent?.getParcelableExtra<SpaceManageArgs>(MvRx.KEY_ARG)
if (isFirstCreation()) {
withState(sharedViewModel) {
when (it.manageType) {
ManageType.AddRooms -> {
val simpleName = SpaceAddRoomFragment::class.java.simpleName
if (supportFragmentManager.findFragmentByTag(simpleName) == null) {
supportFragmentManager.commitTransaction {
......@@ -83,6 +109,22 @@ class SpaceManageActivity : VectorBaseActivity<ActivitySimpleBinding>(), SpaceMa
}
}
}
ManageType.Settings -> {
val simpleName = SpaceSettingsFragment::class.java.simpleName
if (supportFragmentManager.findFragmentByTag(simpleName) == null) {
supportFragmentManager.commitTransaction {
replace(R.id.simpleFragmentContainer,
SpaceSettingsFragment::class.java,
Bundle().apply { this.putParcelable(MvRx.KEY_ARG, RoomProfileArgs(args?.spaceId ?: "")) },
simpleName
)
}
}
}