mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Power level: start to handle updating
This commit is contained in:
parent
3dc2cd4d7a
commit
9075371145
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<RoomMemberProfileViewState>() {
|
||||
|
||||
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? {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<Unit> {
|
||||
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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
85
vector/src/main/res/layout/dialog_set_power_level.xml
Normal file
85
vector/src/main/res/layout/dialog_set_power_level.xml
Normal file
@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingLeft="?dialogPreferredPadding"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingRight="?dialogPreferredPadding">
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/powerLevelRadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/powerLevelAdminRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Admin"
|
||||
android:textColor="?riotx_text_primary" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/powerLevelModeratorRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Moderator"
|
||||
android:textColor="?riotx_text_primary" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/powerLevelDefaultRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Default"
|
||||
android:textColor="?riotx_text_primary" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/powerLevelCustomRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Custom"
|
||||
android:textColor="?riotx_text_primary" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/powerLevelCustomLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/powerLevelCustomTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:text="Custom power level"
|
||||
android:layout_marginStart="16dp"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/powerLevelCustomValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="20"
|
||||
android:textColor="?riotx_text_secondary" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/powerLevelCustomSlider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_below="@id/powerLevelCustomTitle"
|
||||
android:max="100"
|
||||
android:progress="1" />
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
@ -2086,6 +2086,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||
|
||||
<string name="room_member_power_level_admin_in">Admin in %1$s</string>
|
||||
<string name="room_member_power_level_moderator_in">Moderator in %1$s</string>
|
||||
<string name="room_member_power_level_user_in">Default in %1$s</string>
|
||||
<string name="room_member_power_level_custom_in">Custom (%1$d) in %2$s</string>
|
||||
|
||||
<string name="room_member_jump_to_read_receipt">Jump to read receipt</string>
|
||||
|
Loading…
Reference in New Issue
Block a user