diff --git a/CHANGES.md b/CHANGES.md index 7d4678fe40..ff457cb539 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ Changes in Element 1.0.14 (2020-XX-XX) Features ✨: - Enable url previews for notices (#2562) + - Edit room permissions (#2471) Improvements 🙌: - Add System theme option and set as default (#904) (#2387) diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 87ab875746..407aa2fc73 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -84,6 +84,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.alias.RoomAliasFragment +import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment @@ -364,6 +365,11 @@ interface FragmentModule { @FragmentKey(RoomAliasFragment::class) fun bindRoomAliasFragment(fragment: RoomAliasFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomPermissionsFragment::class) + fun bindRoomPermissionsFragment(fragment: RoomPermissionsFragment): Fragment + @Binds @IntoMap @FragmentKey(RoomMemberProfileFragment::class) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 08a251834e..688f74ba5d 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -324,7 +324,7 @@ class RoomMemberProfileFragment @Inject constructor( } override fun onEditPowerLevel(currentRole: Role) { - EditPowerLevelDialogs.showChoice(requireActivity(), currentRole) { newPowerLevel -> + EditPowerLevelDialogs.showChoice(requireActivity(), R.string.power_level_edit_title, currentRole) { newPowerLevel -> viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true)) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt index 4316a4bd0d..764271f7ce 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt @@ -19,6 +19,7 @@ package im.vector.app.features.roommemberprofile.powerlevel import android.app.Activity import android.content.DialogInterface import android.view.KeyEvent +import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import im.vector.app.R @@ -29,7 +30,10 @@ import org.matrix.android.sdk.api.session.room.powerlevels.Role object EditPowerLevelDialogs { - fun showChoice(activity: Activity, currentRole: Role, listener: (Int) -> Unit) { + fun showChoice(activity: Activity, + @StringRes titleRes: Int, + currentRole: Role, + listener: (Int) -> Unit) { val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null) val views = DialogEditPowerLevelBinding.bind(dialogLayout) views.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId -> @@ -45,7 +49,7 @@ object EditPowerLevelDialogs { } AlertDialog.Builder(activity) - .setTitle(R.string.power_level_edit_title) + .setTitle(titleRes) .setView(dialogLayout) .setPositiveButton(R.string.edit) { _, _ -> val newValue = when (views.powerLevelRadioGroup.checkedRadioButtonId) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 76649d53b3..7cb713d378 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -27,6 +27,7 @@ import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding @@ -38,6 +39,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.alias.RoomAliasFragment +import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import javax.inject.Inject @@ -102,12 +104,13 @@ class RoomProfileActivity : .observe() .subscribe { sharedAction -> when (sharedAction) { - is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() - is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() - is RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() - is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() - is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() - } + RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() + RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() + RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() + RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions() + RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() + RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() + }.exhaustive } .disposeOnDestroy() @@ -144,6 +147,10 @@ class RoomProfileActivity : addFragmentToBackstack(R.id.simpleFragmentContainer, RoomAliasFragment::class.java, roomProfileArgs) } + private fun openRoomPermissions() { + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomPermissionsFragment::class.java, roomProfileArgs) + } + private fun openRoomMembers() { addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt index 83a610cf1b..2a5775d1af 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt @@ -24,6 +24,7 @@ import im.vector.app.core.platform.VectorSharedAction sealed class RoomProfileSharedAction : VectorSharedAction { object OpenRoomSettings : RoomProfileSharedAction() object OpenRoomAliasesSettings : RoomProfileSharedAction() + object OpenRoomPermissionsSettings : RoomProfileSharedAction() object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt new file mode 100644 index 0000000000..bb1054b704 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/EditablePermission.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.permissions + +import androidx.annotation.StringRes +import im.vector.app.R +import org.matrix.android.sdk.api.session.events.model.EventType + +/** + * Change on each permission has an effect on the power level event. Try to sort the effect by category. + */ +sealed class EditablePermission(@StringRes val labelResId: Int) { + // Updates `content.events.[eventType]` + open class EventTypeEditablePermission(val eventType: String, @StringRes labelResId: Int) : EditablePermission(labelResId) + + class ModifyWidgets : EventTypeEditablePermission( + // Note: Element Web still use legacy value + EventType.STATE_ROOM_WIDGET_LEGACY, + R.string.room_permissions_modify_widgets + ) + + class ChangeRoomAvatar : EventTypeEditablePermission( + EventType.STATE_ROOM_AVATAR, + R.string.room_permissions_change_room_avatar + ) + + class ChangeMainAddressForTheRoom : EventTypeEditablePermission( + EventType.STATE_ROOM_CANONICAL_ALIAS, + R.string.room_permissions_change_main_address_for_the_room + ) + + class EnableRoomEncryption : EventTypeEditablePermission( + EventType.STATE_ROOM_ENCRYPTION, + R.string.room_permissions_enable_room_encryption + ) + + class ChangeHistoryVisibility : EventTypeEditablePermission( + EventType.STATE_ROOM_HISTORY_VISIBILITY, + R.string.room_permissions_change_history_visibility + ) + + class ChangeRoomName : EventTypeEditablePermission( + EventType.STATE_ROOM_NAME, + R.string.room_permissions_change_room_name + ) + + class ChangePermissions : EventTypeEditablePermission( + EventType.STATE_ROOM_POWER_LEVELS, + R.string.room_permissions_change_permissions + ) + + class SendRoomServerAclEvents : EventTypeEditablePermission( + EventType.STATE_ROOM_SERVER_ACL, + R.string.room_permissions_send_m_room_server_acl_events + ) + + class UpgradeTheRoom : EventTypeEditablePermission( + EventType.STATE_ROOM_TOMBSTONE, + R.string.room_permissions_upgrade_the_room + ) + + class ChangeTopic : EventTypeEditablePermission( + EventType.STATE_ROOM_TOPIC, + R.string.room_permissions_change_topic + ) + + // Updates `content.users_default` + class DefaultRole : EditablePermission(R.string.room_permissions_default_role) + + // Updates `content.events_default` + class SendMessages : EditablePermission(R.string.room_permissions_send_messages) + + // Updates `content.invites` + class InviteUsers : EditablePermission(R.string.room_permissions_invite_users) + + // Updates `content.state_default` + class ChangeSettings : EditablePermission(R.string.room_permissions_change_settings) + + // Updates `content.kick` + class KickUsers : EditablePermission(R.string.room_permissions_kick_users) + + // Updates `content.ban` + class BanUsers : EditablePermission(R.string.room_permissions_ban_users) + + // Updates `content.redact` + class RemoveMessagesSentByOthers : EditablePermission(R.string.room_permissions_remove_messages_sent_by_others) + + // Updates `content.notification.room` + class NotifyEveryone : EditablePermission(R.string.room_permissions_notify_everyone) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt new file mode 100644 index 0000000000..32d9236fb6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.permissions + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class RoomPermissionsAction : VectorViewModelAction { + data class UpdatePermission(val editablePermission: EditablePermission, val powerLevel: Int) : RoomPermissionsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt new file mode 100644 index 0000000000..058f5a9ce7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.permissions + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Success +import im.vector.app.R +import im.vector.app.core.epoxy.loadingItem +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.discovery.settingsInfoItem +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.Role +import javax.inject.Inject + +class RoomPermissionsController @Inject constructor( + private val stringProvider: StringProvider, + colorProvider: ColorProvider +) : TypedEpoxyController() { + + interface Callback { + fun onEditPermission(editablePermission: EditablePermission, currentRole: Role) + } + + var callback: Callback? = null + + private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) + + // Order is the order applied in the UI + private val allEditablePermissions = listOf( + EditablePermission.DefaultRole(), + EditablePermission.SendMessages(), + EditablePermission.InviteUsers(), + EditablePermission.ChangeSettings(), + EditablePermission.KickUsers(), + EditablePermission.BanUsers(), + EditablePermission.RemoveMessagesSentByOthers(), + EditablePermission.NotifyEveryone(), + EditablePermission.ModifyWidgets(), + EditablePermission.ChangeRoomAvatar(), + EditablePermission.ChangeMainAddressForTheRoom(), + EditablePermission.EnableRoomEncryption(), + EditablePermission.ChangeHistoryVisibility(), + EditablePermission.ChangeRoomName(), + EditablePermission.ChangePermissions(), + EditablePermission.SendRoomServerAclEvents(), + EditablePermission.UpgradeTheRoom(), + EditablePermission.ChangeTopic() + ) + + init { + setData(null) + } + + override fun buildModels(data: RoomPermissionsViewState?) { + buildProfileSection( + stringProvider.getString(R.string.room_permissions_title) + ) + + settingsInfoItem { + id("notice") + helperText(stringProvider.getString(R.string.room_permissions_notice)) + } + + when (val content = data?.currentPowerLevelsContent) { + is Success -> buildPermissions(data, content()) + else -> { + loadingItem { + id("loading") + loadingText(stringProvider.getString(R.string.loading)) + } + } + } + } + + private fun buildPermissions(data: RoomPermissionsViewState, content: PowerLevelsContent) { + allEditablePermissions.forEach { editablePermission -> + val currentRole = getCurrentRole(editablePermission, content) + buildProfileAction( + id = editablePermission.labelResId.toString(), + title = stringProvider.getString(editablePermission.labelResId), + subtitle = getSubtitle(currentRole), + dividerColor = dividerColor, + divider = true, + editable = data.actionPermissions.canChangePowerLevels, + action = { callback?.onEditPermission(editablePermission, currentRole) } + ) + } + } + + private fun getSubtitle(currentRole: Role): String { + return when (currentRole) { + Role.Admin, + Role.Moderator, + Role.Default -> stringProvider.getString(currentRole.res) + is Role.Custom -> stringProvider.getString(currentRole.res, currentRole.value) + } + } + + private fun getCurrentRole(editablePermission: EditablePermission, content: PowerLevelsContent): Role { + val value = when (editablePermission) { + is EditablePermission.EventTypeEditablePermission -> content.events[editablePermission.eventType] ?: content.stateDefault + is EditablePermission.DefaultRole -> content.usersDefault + is EditablePermission.SendMessages -> content.eventsDefault + is EditablePermission.InviteUsers -> content.invite + is EditablePermission.ChangeSettings -> content.stateDefault + is EditablePermission.KickUsers -> content.kick + is EditablePermission.BanUsers -> content.ban + is EditablePermission.RemoveMessagesSentByOthers -> content.redact + is EditablePermission.NotifyEveryone -> (content.notifications["room"] as? Int) ?: Role.Moderator.value + } + + return Role.fromValue( + value, + when (editablePermission) { + is EditablePermission.EventTypeEditablePermission -> content.stateDefault + is EditablePermission.DefaultRole -> Role.Default.value + is EditablePermission.SendMessages -> Role.Default.value + is EditablePermission.InviteUsers -> Role.Moderator.value + is EditablePermission.ChangeSettings -> Role.Moderator.value + is EditablePermission.KickUsers -> Role.Moderator.value + is EditablePermission.BanUsers -> Role.Moderator.value + is EditablePermission.RemoveMessagesSentByOthers -> Role.Moderator.value + is EditablePermission.NotifyEveryone -> Role.Moderator.value + } + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt new file mode 100644 index 0000000000..73e0b00de9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.permissions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +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.platform.VectorBaseFragment +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.roommemberprofile.powerlevel.EditPowerLevelDialogs +import im.vector.app.features.roomprofile.RoomProfileArgs +import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class RoomPermissionsFragment @Inject constructor( + val viewModelFactory: RoomPermissionsViewModel.Factory, + private val controller: RoomPermissionsController, + private val avatarRenderer: AvatarRenderer +) : + VectorBaseFragment(), + RoomPermissionsController.Callback { + + private val viewModel: RoomPermissionsViewModel by fragmentViewModel() + + private val roomProfileArgs: RoomProfileArgs by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomSettingGenericBinding { + return FragmentRoomSettingGenericBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + controller.callback = this + setupToolbar(views.roomSettingsToolbar) + views.roomSettingsRecyclerView.configureWith(controller, hasFixedSize = true) + views.waitingView.waitingStatusText.setText(R.string.please_wait) + views.waitingView.waitingStatusText.isVisible = true + + viewModel.observeViewEvents { + when (it) { + is RoomPermissionsViewEvents.Failure -> showFailure(it.throwable) + RoomPermissionsViewEvents.Success -> showSuccess() + }.exhaustive + } + } + + private fun showSuccess() { + activity?.toast(R.string.room_settings_save_success) + } + + override fun onDestroyView() { + controller.callback = null + views.roomSettingsRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + views.waitingView.root.isVisible = state.isLoading + controller.setData(state) + renderRoomSummary(state) + } + + private fun renderRoomSummary(state: RoomPermissionsViewState) { + state.roomSummary()?.let { + views.roomSettingsToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + } + } + + override fun onEditPermission(editablePermission: EditablePermission, currentRole: Role) { + EditPowerLevelDialogs.showChoice(requireActivity(), editablePermission.labelResId, currentRole) { newPowerLevel -> + viewModel.handle(RoomPermissionsAction.UpdatePermission(editablePermission, newPowerLevel)) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewEvents.kt new file mode 100644 index 0000000000..8994398cf3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewEvents.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.app.features.roomprofile.permissions + +import im.vector.app.core.platform.VectorViewEvents + +/** + * Transient events for room settings screen + */ +sealed class RoomPermissionsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : RoomPermissionsViewEvents() + object Success : RoomPermissionsViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt new file mode 100644 index 0000000000..95217c1885 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.permissions + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialState: RoomPermissionsViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomPermissionsViewState): RoomPermissionsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomPermissionsViewState): RoomPermissionsViewModel? { + val fragment: RoomPermissionsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + observeRoomSummary() + observePowerLevel() + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { async -> + copy( + roomSummary = async + ) + } + } + + private fun observePowerLevel() { + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { powerLevelContent -> + val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) + val permissions = RoomPermissionsViewState.ActionPermissions( + canChangePowerLevels = powerLevelsHelper.isUserAllowedToSend( + userId = session.myUserId, + isState = true, + eventType = EventType.STATE_ROOM_POWER_LEVELS + ) + ) + setState { + copy( + actionPermissions = permissions, + currentPowerLevelsContent = Success(powerLevelContent) + ) + } + } + .disposeOnClear() + } + + override fun handle(action: RoomPermissionsAction) { + when (action) { + is RoomPermissionsAction.UpdatePermission -> updatePermission(action) + }.exhaustive + } + + private fun updatePermission(action: RoomPermissionsAction.UpdatePermission) { + withState { state -> + val currentPowerLevel = state.currentPowerLevelsContent.invoke() ?: return@withState + postLoading(true) + viewModelScope.launch { + try { + val newPowerLevelsContent = when(action.editablePermission) { + is EditablePermission.EventTypeEditablePermission -> currentPowerLevel.copy( + events = currentPowerLevel.events.toMutableMap().apply { + put(action.editablePermission.eventType, action.powerLevel) + } + ) + is EditablePermission.DefaultRole -> currentPowerLevel.copy(usersDefault = action.powerLevel) + is EditablePermission.SendMessages -> currentPowerLevel.copy(eventsDefault = action.powerLevel) + is EditablePermission.InviteUsers -> currentPowerLevel.copy(invite = action.powerLevel) + is EditablePermission.ChangeSettings -> currentPowerLevel.copy(stateDefault = action.powerLevel) + is EditablePermission.KickUsers -> currentPowerLevel.copy(kick = action.powerLevel) + is EditablePermission.BanUsers -> currentPowerLevel.copy(ban = action.powerLevel) + is EditablePermission.RemoveMessagesSentByOthers -> currentPowerLevel.copy(redact = action.powerLevel) + is EditablePermission.NotifyEveryone -> currentPowerLevel.copy( + notifications = currentPowerLevel.notifications.toMutableMap().apply { + put("room", action.powerLevel) + } + ) + } + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, newPowerLevelsContent.toContent()) + setState { + copy( + isLoading = false + ) + } + } catch (failure: Throwable) { + postLoading(false) + _viewEvents.post(RoomPermissionsViewEvents.Failure(failure)) + } + } + } + } + + private fun postLoading(isLoading: Boolean) { + setState { + copy(isLoading = isLoading) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt new file mode 100644 index 0000000000..556c44b730 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewState.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.permissions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.roomprofile.RoomProfileArgs +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class RoomPermissionsViewState( + val roomId: String, + val roomSummary: Async = Uninitialized, + val actionPermissions: ActionPermissions = ActionPermissions(), + val currentPowerLevelsContent: Async = Uninitialized, + val newPowerLevelsContent: PowerLevelsContent? = null, + val isLoading: Boolean = false +) : MvRxState { + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + + data class ActionPermissions( + val canChangePowerLevels: Boolean = false + ) +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index bf3c1f87f8..1984be078d 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -46,6 +46,7 @@ class RoomSettingsController @Inject constructor( fun onTopicChanged(topic: String) fun onHistoryVisibilityClicked() fun onRoomAliasesClicked() + fun onRoomPermissionsClicked() fun onJoinRuleClicked() } @@ -115,6 +116,16 @@ class RoomSettingsController @Inject constructor( action = { callback?.onRoomAliasesClicked() } ) + buildProfileAction( + id = "permissions", + title = stringProvider.getString(R.string.room_settings_permissions_title), + subtitle = stringProvider.getString(R.string.room_settings_permissions_subtitle), + dividerColor = dividerColor, + divider = true, + editable = true, + action = { callback?.onRoomPermissionsClicked() } + ) + buildProfileAction( id = "historyReadability", title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 52e273f3d4..1ca539ea7e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -178,6 +178,10 @@ class RoomSettingsFragment @Inject constructor( roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings) } + override fun onRoomPermissionsClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings) + } + override fun onJoinRuleClicked() = withState(viewModel) { state -> val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 355ac4d6d6..467270b58e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -609,6 +609,32 @@ The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint. Only accept the certificate if the server administrator has published a fingerprint that matches the one above. + + Room permissions + View and update the roles required to change various parts of the room. + + "Permissions" + "Select the roles required to change various parts of the room" + + Default role + Send messages + Invite users + Change settings + Kick users + Ban users + Remove messages sent by others + Notify everyone + Modify widgets + Change room avatar + Change main address for the room + Enable room encryption + Change history visibility + Change room name + Change permissions + Send m.room.server_acl events + Upgrade the room + Change topic + Room Details People