Unverified Commit d81d971c authored by Valere's avatar Valere Committed by GitHub
Browse files

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

Feature/bca/spaces admin manage
parents e6d4f9a1 723f7cc3
......@@ -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)
}
}
}
}
......@@ -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) {
......
/*
* 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()
}
}
}
......@@ -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)
}
}
/*
* 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()
}
/*
* 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()
}
/*
* 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
)
}
/*
* 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)
}))
}
}
}
}
/*
* 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
*