mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-13 12:38:58 +01:00
Bump Compose M3 to 1.0.0-beta01 (#7867)
This commit is contained in:
@@ -2,7 +2,8 @@ package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
@@ -15,6 +16,7 @@ import androidx.compose.material3.SmallTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -22,12 +24,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
@@ -97,14 +98,10 @@ fun AppBar(
|
||||
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
) {
|
||||
val scrollFraction = if (isActionMode) 1f else scrollBehavior?.state?.overlappedFraction ?: 0f
|
||||
val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(scrollFraction)
|
||||
|
||||
Column(
|
||||
modifier = modifier.drawBehind { drawRect(backgroundColor) },
|
||||
modifier = modifier,
|
||||
) {
|
||||
SmallTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
navigationIcon = {
|
||||
if (isActionMode) {
|
||||
IconButton(onClick = onCancelActionMode) {
|
||||
@@ -126,10 +123,11 @@ fun AppBar(
|
||||
},
|
||||
title = titleContent,
|
||||
actions = actions,
|
||||
// Background handled by parent
|
||||
windowInsets = WindowInsets.statusBars,
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
elevation = if (isActionMode) 3.dp else 0.dp,
|
||||
),
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.VectorConverter
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.interaction.FocusInteraction
|
||||
import androidx.compose.foundation.interaction.HoverInteraction
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ButtonElevation
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.animateElevation
|
||||
import androidx.compose.material3.ButtonDefaults as M3ButtonDefaults
|
||||
|
||||
@Composable
|
||||
fun TextButton(
|
||||
@@ -30,10 +49,15 @@ fun TextButton(
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
elevation: ButtonElevation? = null,
|
||||
shape: Shape = ButtonDefaults.textShape,
|
||||
shape: Shape = M3ButtonDefaults.textShape,
|
||||
border: BorderStroke? = null,
|
||||
colors: ButtonColors = ButtonDefaults.textButtonColors(),
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
|
||||
colors: ButtonColors = ButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.primary,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
),
|
||||
contentPadding: PaddingValues = M3ButtonDefaults.TextButtonContentPadding,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) =
|
||||
Button(
|
||||
@@ -58,10 +82,10 @@ fun Button(
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
|
||||
shape: Shape = ButtonDefaults.textShape,
|
||||
shape: Shape = M3ButtonDefaults.textShape,
|
||||
border: BorderStroke? = null,
|
||||
colors: ButtonColors = ButtonDefaults.buttonColors(),
|
||||
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
|
||||
contentPadding: PaddingValues = M3ButtonDefaults.ContentPadding,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val containerColor = colors.containerColor(enabled).value
|
||||
@@ -86,8 +110,8 @@ fun Button(
|
||||
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
|
||||
Row(
|
||||
Modifier.defaultMinSize(
|
||||
minWidth = ButtonDefaults.MinWidth,
|
||||
minHeight = ButtonDefaults.MinHeight,
|
||||
minWidth = M3ButtonDefaults.MinWidth,
|
||||
minHeight = M3ButtonDefaults.MinHeight,
|
||||
)
|
||||
.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -98,3 +122,255 @@ fun Button(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ButtonDefaults {
|
||||
/**
|
||||
* Creates a [ButtonColors] that represents the default container and content colors used in a
|
||||
* [Button].
|
||||
*
|
||||
* @param containerColor the container color of this [Button] when enabled.
|
||||
* @param contentColor the content color of this [Button] when enabled.
|
||||
* @param disabledContainerColor the container color of this [Button] when not enabled.
|
||||
* @param disabledContentColor the content color of this [Button] when not enabled.
|
||||
*/
|
||||
@Composable
|
||||
fun buttonColors(
|
||||
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||
contentColor: Color = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
): ButtonColors = ButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
disabledContainerColor = disabledContainerColor,
|
||||
disabledContentColor = disabledContentColor,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a [ButtonElevation] that will animate between the provided values according to the
|
||||
* Material specification for a [Button].
|
||||
*
|
||||
* @param defaultElevation the elevation used when the [Button] is enabled, and has no other
|
||||
* [Interaction]s.
|
||||
* @param pressedElevation the elevation used when this [Button] is enabled and pressed.
|
||||
* @param focusedElevation the elevation used when the [Button] is enabled and focused.
|
||||
* @param hoveredElevation the elevation used when the [Button] is enabled and hovered.
|
||||
* @param disabledElevation the elevation used when the [Button] is not enabled.
|
||||
*/
|
||||
@Composable
|
||||
fun buttonElevation(
|
||||
defaultElevation: Dp = 0.dp,
|
||||
pressedElevation: Dp = 0.dp,
|
||||
focusedElevation: Dp = 0.dp,
|
||||
hoveredElevation: Dp = 1.dp,
|
||||
disabledElevation: Dp = 0.dp,
|
||||
): ButtonElevation = ButtonElevation(
|
||||
defaultElevation = defaultElevation,
|
||||
pressedElevation = pressedElevation,
|
||||
focusedElevation = focusedElevation,
|
||||
hoveredElevation = hoveredElevation,
|
||||
disabledElevation = disabledElevation,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the elevation for a button in different states.
|
||||
*
|
||||
* - See [M3ButtonDefaults.buttonElevation] for the default elevation used in a [Button].
|
||||
* - See [M3ButtonDefaults.elevatedButtonElevation] for the default elevation used in a
|
||||
* [ElevatedButton].
|
||||
*/
|
||||
@Stable
|
||||
class ButtonElevation internal constructor(
|
||||
private val defaultElevation: Dp,
|
||||
private val pressedElevation: Dp,
|
||||
private val focusedElevation: Dp,
|
||||
private val hoveredElevation: Dp,
|
||||
private val disabledElevation: Dp,
|
||||
) {
|
||||
/**
|
||||
* Represents the tonal elevation used in a button, depending on its [enabled] state and
|
||||
* [interactionSource]. This should typically be the same value as the [shadowElevation].
|
||||
*
|
||||
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
|
||||
* When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
|
||||
* color in light theme and lighter color in dark theme.
|
||||
*
|
||||
* See [shadowElevation] which controls the elevation of the shadow drawn around the button.
|
||||
*
|
||||
* @param enabled whether the button is enabled
|
||||
* @param interactionSource the [InteractionSource] for this button
|
||||
*/
|
||||
@Composable
|
||||
internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
|
||||
return animateElevation(enabled = enabled, interactionSource = interactionSource)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the shadow elevation used in a button, depending on its [enabled] state and
|
||||
* [interactionSource]. This should typically be the same value as the [tonalElevation].
|
||||
*
|
||||
* Shadow elevation is used to apply a shadow around the button to give it higher emphasis.
|
||||
*
|
||||
* See [tonalElevation] which controls the elevation with a color shift to the surface.
|
||||
*
|
||||
* @param enabled whether the button is enabled
|
||||
* @param interactionSource the [InteractionSource] for this button
|
||||
*/
|
||||
@Composable
|
||||
internal fun shadowElevation(
|
||||
enabled: Boolean,
|
||||
interactionSource: InteractionSource,
|
||||
): State<Dp> {
|
||||
return animateElevation(enabled = enabled, interactionSource = interactionSource)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun animateElevation(
|
||||
enabled: Boolean,
|
||||
interactionSource: InteractionSource,
|
||||
): State<Dp> {
|
||||
val interactions = remember { mutableStateListOf<Interaction>() }
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is HoverInteraction.Enter -> {
|
||||
interactions.add(interaction)
|
||||
}
|
||||
is HoverInteraction.Exit -> {
|
||||
interactions.remove(interaction.enter)
|
||||
}
|
||||
is FocusInteraction.Focus -> {
|
||||
interactions.add(interaction)
|
||||
}
|
||||
is FocusInteraction.Unfocus -> {
|
||||
interactions.remove(interaction.focus)
|
||||
}
|
||||
is PressInteraction.Press -> {
|
||||
interactions.add(interaction)
|
||||
}
|
||||
is PressInteraction.Release -> {
|
||||
interactions.remove(interaction.press)
|
||||
}
|
||||
is PressInteraction.Cancel -> {
|
||||
interactions.remove(interaction.press)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val interaction = interactions.lastOrNull()
|
||||
|
||||
val target =
|
||||
if (!enabled) {
|
||||
disabledElevation
|
||||
} else {
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> pressedElevation
|
||||
is HoverInteraction.Enter -> hoveredElevation
|
||||
is FocusInteraction.Focus -> focusedElevation
|
||||
else -> defaultElevation
|
||||
}
|
||||
}
|
||||
|
||||
val animatable = remember { Animatable(target, Dp.VectorConverter) }
|
||||
|
||||
if (!enabled) {
|
||||
// No transition when moving to a disabled state
|
||||
LaunchedEffect(target) { animatable.snapTo(target) }
|
||||
} else {
|
||||
LaunchedEffect(target) {
|
||||
val lastInteraction = when (animatable.targetValue) {
|
||||
pressedElevation -> PressInteraction.Press(Offset.Zero)
|
||||
hoveredElevation -> HoverInteraction.Enter()
|
||||
focusedElevation -> FocusInteraction.Focus()
|
||||
else -> null
|
||||
}
|
||||
animatable.animateElevation(
|
||||
from = lastInteraction,
|
||||
to = interaction,
|
||||
target = target,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return animatable.asState()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other !is ButtonElevation) return false
|
||||
|
||||
if (defaultElevation != other.defaultElevation) return false
|
||||
if (pressedElevation != other.pressedElevation) return false
|
||||
if (focusedElevation != other.focusedElevation) return false
|
||||
if (hoveredElevation != other.hoveredElevation) return false
|
||||
if (disabledElevation != other.disabledElevation) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = defaultElevation.hashCode()
|
||||
result = 31 * result + pressedElevation.hashCode()
|
||||
result = 31 * result + focusedElevation.hashCode()
|
||||
result = 31 * result + hoveredElevation.hashCode()
|
||||
result = 31 * result + disabledElevation.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the container and content colors used in a button in different states.
|
||||
*
|
||||
* - See [M3ButtonDefaults.buttonColors] for the default colors used in a [Button].
|
||||
* - See [M3ButtonDefaults.elevatedButtonColors] for the default colors used in a [ElevatedButton].
|
||||
* - See [M3ButtonDefaults.textButtonColors] for the default colors used in a [TextButton].
|
||||
*/
|
||||
@Immutable
|
||||
class ButtonColors internal constructor(
|
||||
private val containerColor: Color,
|
||||
private val contentColor: Color,
|
||||
private val disabledContainerColor: Color,
|
||||
private val disabledContentColor: Color,
|
||||
) {
|
||||
/**
|
||||
* Represents the container color for this button, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the button is enabled
|
||||
*/
|
||||
@Composable
|
||||
internal fun containerColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the content color for this button, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the button is enabled
|
||||
*/
|
||||
@Composable
|
||||
internal fun contentColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other !is ButtonColors) return false
|
||||
|
||||
if (containerColor != other.containerColor) return false
|
||||
if (contentColor != other.contentColor) return false
|
||||
if (disabledContainerColor != other.disabledContainerColor) return false
|
||||
if (disabledContentColor != other.disabledContentColor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = containerColor.hashCode()
|
||||
result = 31 * result + contentColor.hashCode()
|
||||
result = 31 * result + disabledContainerColor.hashCode()
|
||||
result = 31 * result + disabledContentColor.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,20 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonColors
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.OutlinedIconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.minimumTouchTargetSize
|
||||
@@ -100,6 +105,88 @@ fun IconButton(
|
||||
}
|
||||
}
|
||||
|
||||
object IconButtonDefaults {
|
||||
/**
|
||||
* Creates a [IconButtonColors] that represents the default colors used in a [IconButton].
|
||||
*
|
||||
* @param containerColor the container color of this icon button when enabled.
|
||||
* @param contentColor the content color of this icon button when enabled.
|
||||
* @param disabledContainerColor the container color of this icon button when not enabled.
|
||||
* @param disabledContentColor the content color of this icon button when not enabled.
|
||||
*/
|
||||
@Composable
|
||||
fun iconButtonColors(
|
||||
containerColor: Color = Color.Transparent,
|
||||
contentColor: Color = LocalContentColor.current,
|
||||
disabledContainerColor: Color = Color.Transparent,
|
||||
disabledContentColor: Color = contentColor.copy(alpha = 0.38f),
|
||||
): IconButtonColors =
|
||||
IconButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
disabledContainerColor = disabledContainerColor,
|
||||
disabledContentColor = disabledContentColor,
|
||||
)
|
||||
}
|
||||
|
||||
object IconButtonTokens {
|
||||
val StateLayerSize = 40.0.dp
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the container and content colors used in an icon button in different states.
|
||||
*
|
||||
* - See [IconButtonDefaults.filledIconButtonColors] and
|
||||
* [IconButtonDefaults.filledTonalIconButtonColors] for the default colors used in a
|
||||
* [FilledIconButton].
|
||||
* - See [IconButtonDefaults.outlinedIconButtonColors] for the default colors used in an
|
||||
* [OutlinedIconButton].
|
||||
*/
|
||||
@Immutable
|
||||
class IconButtonColors internal constructor(
|
||||
private val containerColor: Color,
|
||||
private val contentColor: Color,
|
||||
private val disabledContainerColor: Color,
|
||||
private val disabledContentColor: Color,
|
||||
) {
|
||||
/**
|
||||
* Represents the container color for this icon button, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the icon button is enabled
|
||||
*/
|
||||
@Composable
|
||||
internal fun containerColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the content color for this icon button, depending on [enabled].
|
||||
*
|
||||
* @param enabled whether the icon button is enabled
|
||||
*/
|
||||
@Composable
|
||||
internal fun contentColor(enabled: Boolean): State<Color> {
|
||||
return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || other !is IconButtonColors) return false
|
||||
|
||||
if (containerColor != other.containerColor) return false
|
||||
if (contentColor != other.contentColor) return false
|
||||
if (disabledContainerColor != other.disabledContainerColor) return false
|
||||
if (disabledContentColor != other.disabledContentColor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = containerColor.hashCode()
|
||||
result = 31 * result + contentColor.hashCode()
|
||||
result = 31 * result + disabledContainerColor.hashCode()
|
||||
result = 31 * result + disabledContentColor.hashCode()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user