diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt index 16af3d216e..0b1f5212ee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt @@ -22,4 +22,6 @@ object PowerLevelsConstants { const val DEFAULT_ROOM_ADMIN_LEVEL = 100 const val DEFAULT_ROOM_MODERATOR_LEVEL = 50 const val DEFAULT_ROOM_USER_LEVEL = 0 + + val DEFAULT_DEFINED_LEVELS = listOf(DEFAULT_ROOM_ADMIN_LEVEL, DEFAULT_ROOM_MODERATOR_LEVEL, DEFAULT_ROOM_USER_LEVEL) } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt index 1dc2459538..9f775d3fb9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt @@ -24,4 +24,5 @@ sealed class RoomMemberProfileAction : VectorViewModelAction { object IgnoreUser : RoomMemberProfileAction() object VerifyUser : RoomMemberProfileAction() object ShareRoomMemberProfile : RoomMemberProfileAction() + data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt index 70c3e8add2..4029d7bc37 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt @@ -18,6 +18,9 @@ package im.vector.riotx.features.roommemberprofile import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.riotx.R import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileSection @@ -28,7 +31,8 @@ import javax.inject.Inject class RoomMemberProfileController @Inject constructor( private val stringProvider: StringProvider, - colorProvider: ColorProvider + colorProvider: ColorProvider, + private val session: Session ) : TypedEpoxyController() { private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -42,6 +46,7 @@ class RoomMemberProfileController @Inject constructor( fun onShowDeviceListNoCrossSigning() fun onJumpToReadReceiptClicked() fun onMentionClicked() + fun onSetPowerLevel(userPowerLevel: Int) } override fun buildModels(data: RoomMemberProfileViewState?) { @@ -71,6 +76,68 @@ class RoomMemberProfileController @Inject constructor( } private fun buildRoomMemberActions(state: RoomMemberProfileViewState) { + buildSecuritySection(state) + buildMoreSection(state) + buildAdminSection(state) + } + + private fun buildAdminSection(state: RoomMemberProfileViewState) { + val powerLevelsContent = state.powerLevelsContent() ?: return + val powerLevelsStr = state.userPowerLevelString() ?: return + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + val userPowerLevel = powerLevelsHelper.getUserPowerLevel(state.userId) + val myPowerLevel = powerLevelsHelper.getUserPowerLevel(session.myUserId) + if ((!state.isMine && myPowerLevel <= userPowerLevel) + || myPowerLevel != PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL) { + return + } + buildProfileSection("Admin Actions") + buildProfileAction( + id = "set_power_level", + editable = false, + title = powerLevelsStr, + dividerColor = dividerColor, + action = { callback?.onSetPowerLevel(userPowerLevel) } + ) + } + + private fun buildMoreSection(state: RoomMemberProfileViewState) { + // More + if (!state.isMine) { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "read_receipt", + editable = false, + title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), + dividerColor = dividerColor, + action = { callback?.onJumpToReadReceiptClicked() } + ) + + val ignoreActionTitle = state.buildIgnoreActionTitle() + + buildProfileAction( + id = "mention", + title = stringProvider.getString(R.string.room_participants_action_mention), + dividerColor = dividerColor, + editable = false, + divider = ignoreActionTitle != null, + action = { callback?.onMentionClicked() } + ) + if (ignoreActionTitle != null) { + buildProfileAction( + id = "ignore", + title = ignoreActionTitle, + dividerColor = dividerColor, + destructive = true, + editable = false, + divider = false, + action = { callback?.onIgnoreClicked() } + ) + } + } + } + + private fun buildSecuritySection(state: RoomMemberProfileViewState) { // Security buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) @@ -148,40 +215,6 @@ class RoomMemberProfileController @Inject constructor( centered(false) } } - - // More - if (!state.isMine) { - buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) - buildProfileAction( - id = "read_receipt", - editable = false, - title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), - dividerColor = dividerColor, - action = { callback?.onJumpToReadReceiptClicked() } - ) - - val ignoreActionTitle = state.buildIgnoreActionTitle() - - buildProfileAction( - id = "mention", - title = stringProvider.getString(R.string.room_participants_action_mention), - dividerColor = dividerColor, - editable = false, - divider = ignoreActionTitle != null, - action = { callback?.onMentionClicked() } - ) - if (ignoreActionTitle != null) { - buildProfileAction( - id = "ignore", - title = ignoreActionTitle, - dividerColor = dividerColor, - destructive = true, - editable = false, - divider = false, - action = { callback?.onIgnoreClicked() } - ) - } - } } private fun RoomMemberProfileViewState.buildIgnoreActionTitle(): String? { diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt index 28734af0ad..7dc43144ef 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -43,6 +43,7 @@ import im.vector.riotx.core.utils.startSharePlainTextIntent import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet +import im.vector.riotx.features.roommemberprofile.powerlevel.SetPowerLevelDialogs import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_matrix_profile.* import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.* @@ -94,15 +95,23 @@ class RoomMemberProfileFragment @Inject constructor( matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) viewModel.observeViewEvents { when (it) { - is RoomMemberProfileViewEvents.Loading -> showLoading(it.message) + is RoomMemberProfileViewEvents.Loading -> showLoading(it.message) is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable) is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it) is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink) + is RoomMemberProfileViewEvents.OnSetPowerLevelSuccess -> Unit + is RoomMemberProfileViewEvents.ShowPowerLevelValidation -> handleShowPowerLevelAdminWarning(it) }.exhaustive } } + private fun handleShowPowerLevelAdminWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelValidation) { + SetPowerLevelDialogs.showValidation(requireActivity()){ + viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false)) + } + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.roomMemberProfileShareAction -> { @@ -238,4 +247,10 @@ class RoomMemberProfileFragment @Inject constructor( private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) { navigator.openBigImageViewer(requireActivity(), view, userMatrixItem) } + + override fun onSetPowerLevel(userPowerLevel: Int) { + SetPowerLevelDialogs.showChoice(requireActivity(), userPowerLevel) { newPowerLevel -> + viewModel.handle(RoomMemberProfileAction.SetPowerLevel(userPowerLevel, newPowerLevel, true)) + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt index 5d8757a337..dc0e622b7a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt @@ -26,6 +26,8 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents() object OnIgnoreActionSuccess : RoomMemberProfileViewEvents() + object OnSetPowerLevelSuccess : RoomMemberProfileViewEvents() + data class ShowPowerLevelValidation(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents() data class StartVerification( val userId: String, diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt index b32d2adaa6..ddc1705bd8 100644 --- a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.room.Room @@ -41,6 +42,7 @@ import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.mapOptional import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap @@ -140,6 +142,31 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction() is RoomMemberProfileAction.VerifyUser -> prepareVerification() is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile() + is RoomMemberProfileAction.SetPowerLevel -> handleSetPowerLevel(action) + } + } + + private fun handleSetPowerLevel(action: RoomMemberProfileAction.SetPowerLevel) = withState { state -> + if (room == null || action.previousValue == action.newValue) { + return@withState + } + val currentPowerLevelsContent = state.powerLevelsContent() ?: return@withState + val myPowerLevel = PowerLevelsHelper(currentPowerLevelsContent).getUserPowerLevel(session.myUserId) + if (action.askForValidation && action.newValue >= myPowerLevel) { + _viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelValidation(action.previousValue, action.newValue)) + } else { + currentPowerLevelsContent.users[state.userId] = action.newValue + viewModelScope.launch { + _viewEvents.post(RoomMemberProfileViewEvents.Loading()) + try { + awaitCallback { + room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it) + } + _viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess) + } catch (failure: Throwable) { + _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) + } + } } } @@ -208,7 +235,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL) { stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName) } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL) { - "" + stringProvider.getString(R.string.room_member_power_level_user_in, roomName) } else { stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel, roomName) } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/powerlevel/SetPowerLevelDialogs.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/powerlevel/SetPowerLevelDialogs.kt new file mode 100644 index 0000000000..5446d5c45e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/powerlevel/SetPowerLevelDialogs.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 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.riotx.features.roommemberprofile.powerlevel + +import android.app.Activity +import android.content.DialogInterface +import android.view.KeyEvent +import android.widget.SeekBar +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants +import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard +import kotlinx.android.synthetic.main.dialog_set_power_level.view.* + +object SetPowerLevelDialogs { + + fun showChoice(activity: Activity, currentValue: Int, listener: (Int) -> Unit) { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_set_power_level, null) + dialogLayout.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId -> + dialogLayout.powerLevelCustomLayout.isVisible = checkedId == R.id.powerLevelCustomRadio + } + dialogLayout.powerLevelCustomSlider.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + dialogLayout.powerLevelCustomValue.text = progress.toString() + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + //NOOP + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + //NOOP + } + }) + dialogLayout.powerLevelCustomSlider.progress = currentValue + when (currentValue) { + PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL -> dialogLayout.powerLevelAdminRadio.isChecked = true + PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL -> dialogLayout.powerLevelModeratorRadio.isChecked = true + PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL -> dialogLayout.powerLevelDefaultRadio.isChecked = true + else -> dialogLayout.powerLevelCustomRadio.isChecked = true + } + + AlertDialog.Builder(activity) + .setTitle("Change power level") + .setView(dialogLayout) + .setPositiveButton(R.string.action_change) + { _, _ -> + val newValue = when (dialogLayout.powerLevelRadioGroup.checkedRadioButtonId) { + R.id.powerLevelAdminRadio -> PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL + R.id.powerLevelModeratorRadio -> PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL + R.id.powerLevelDefaultRadio -> PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL + else -> dialogLayout.powerLevelCustomSlider.progress + } + listener(newValue) + } + .setNegativeButton(R.string.cancel, null) + .setOnKeyListener(DialogInterface.OnKeyListener + { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .setOnDismissListener { + dialogLayout.hideKeyboard() + } + .create() + .show() + } + + fun showValidation(activity: Activity, onValidate: () -> Unit) { + // ask to the user to confirmation thu upgrade. + AlertDialog.Builder(activity) + .setMessage(R.string.room_participants_power_level_prompt) + .setPositiveButton(R.string.yes) { _, _ -> + onValidate() + } + .setNegativeButton(R.string.no, null) + .show() + } +} diff --git a/vector/src/main/res/layout/dialog_set_power_level.xml b/vector/src/main/res/layout/dialog_set_power_level.xml new file mode 100644 index 0000000000..276def5342 --- /dev/null +++ b/vector/src/main/res/layout/dialog_set_power_level.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index de54771187..326514a6c2 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2086,6 +2086,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Admin in %1$s Moderator in %1$s + Default in %1$s Custom (%1$d) in %2$s Jump to read receipt