mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-18 16:25:27 +02:00
Flex chips
This commit is contained in:
parent
3d2a63fe95
commit
9d76c168ec
@ -18,7 +18,8 @@
|
|||||||
|
|
||||||
package com.sadellie.unitto.core.ui.common
|
package com.sadellie.unitto.core.ui.common
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AssistChipDefaults
|
import androidx.compose.material3.AssistChipDefaults
|
||||||
import androidx.compose.material3.FilterChipDefaults
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
@ -40,6 +40,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@ -48,41 +49,37 @@ import androidx.compose.ui.unit.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun FilterChip(
|
fun FilterChip(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
selected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
label: String,
|
label: String,
|
||||||
imageVector: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
) {
|
) {
|
||||||
|
val transition = updateTransition(targetState = isSelected, label = "Selected transition")
|
||||||
|
val backgroundColor = transition.animateColor(label = "Background color") {
|
||||||
|
if (it) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
val borderColor = transition.animateColor(label = "Border color") {
|
||||||
|
if (it) Color.Transparent else MaterialTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(
|
.padding(vertical = 8.dp)
|
||||||
color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
|
.clip(FilterChipDefaults.shape)
|
||||||
shape = FilterChipDefaults.shape
|
.clickable { onClick() }
|
||||||
)
|
.background(backgroundColor.value)
|
||||||
.border(
|
.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = if (selected) Color.Transparent else MaterialTheme.colorScheme.outline,
|
color = borderColor.value,
|
||||||
shape = FilterChipDefaults.shape
|
shape = FilterChipDefaults.shape
|
||||||
)
|
)
|
||||||
.height(FilterChipDefaults.Height)
|
.height(FilterChipDefaults.Height)
|
||||||
.clickable { onClick() }
|
.padding(horizontal = 16.dp),
|
||||||
.padding(start = 8.dp, end = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(visible = selected) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.height(FilterChipDefaults.IconSize),
|
|
||||||
imageVector = imageVector,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,17 +93,16 @@ fun AssistChip(
|
|||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(
|
.padding(vertical = 8.dp)
|
||||||
color = MaterialTheme.colorScheme.surface,
|
.clip(FilterChipDefaults.shape)
|
||||||
shape = AssistChipDefaults.shape
|
.clickable { onClick() }
|
||||||
)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.border(
|
.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = MaterialTheme.colorScheme.outline,
|
||||||
shape = AssistChipDefaults.shape
|
shape = AssistChipDefaults.shape
|
||||||
)
|
)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.clickable { onClick() }
|
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@ -135,10 +131,8 @@ fun PreviewFilterChip() {
|
|||||||
var isSelected by remember { mutableStateOf(true) }
|
var isSelected by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = isSelected,
|
isSelected = isSelected,
|
||||||
onClick = { isSelected = !isSelected },
|
onClick = { isSelected = !isSelected },
|
||||||
label = "Label",
|
label = "Label",
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = ""
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ package com.sadellie.unitto.feature.converter
|
|||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -32,6 +33,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.sadellie.unitto.core.base.R
|
import com.sadellie.unitto.core.base.R
|
||||||
import com.sadellie.unitto.core.ui.common.EmptyScreen
|
import com.sadellie.unitto.core.ui.common.EmptyScreen
|
||||||
@ -114,10 +116,12 @@ private fun LeftSideScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ChipsRow(
|
ChipsRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
chosenUnitGroup = uiState.unitGroup,
|
chosenUnitGroup = uiState.unitGroup,
|
||||||
items = uiState.shownUnitGroups,
|
items = uiState.shownUnitGroups,
|
||||||
selectAction = updateUnitGroup,
|
selectAction = updateUnitGroup,
|
||||||
lazyListState = chipsRowLazyListState,
|
|
||||||
navigateToSettingsAction = navigateToUnitGroups
|
navigateToSettingsAction = navigateToUnitGroups
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -161,7 +165,7 @@ private fun LeftSideScreenPreview() {
|
|||||||
units = units,
|
units = units,
|
||||||
query = TextFieldValue("test"),
|
query = TextFieldValue("test"),
|
||||||
favorites = false,
|
favorites = false,
|
||||||
shownUnitGroups = listOf(UnitGroup.LENGTH, UnitGroup.TEMPERATURE, UnitGroup.CURRENCY),
|
shownUnitGroups = UnitGroup.entries,
|
||||||
unitGroup = units.keys.toList().first(),
|
unitGroup = units.keys.toList().first(),
|
||||||
sorting = UnitsListSorting.USAGE,
|
sorting = UnitsListSorting.USAGE,
|
||||||
),
|
),
|
||||||
|
@ -18,23 +18,30 @@
|
|||||||
|
|
||||||
package com.sadellie.unitto.feature.converter.components
|
package com.sadellie.unitto.feature.converter.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -50,51 +57,156 @@ import com.sadellie.unitto.data.model.UnitGroup
|
|||||||
* @param chosenUnitGroup Currently selected [UnitGroup]
|
* @param chosenUnitGroup Currently selected [UnitGroup]
|
||||||
* @param selectAction Action to perform when a chip is clicked
|
* @param selectAction Action to perform when a chip is clicked
|
||||||
* @param navigateToSettingsAction Action to perform when clicking settings chip at the end
|
* @param navigateToSettingsAction Action to perform when clicking settings chip at the end
|
||||||
* @param lazyListState Used for animated scroll when entering unit selection screen
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ChipsRow(
|
internal fun ChipsRow(
|
||||||
items: List<UnitGroup> = UnitGroup.entries,
|
modifier: Modifier,
|
||||||
|
items: List<UnitGroup>,
|
||||||
chosenUnitGroup: UnitGroup?,
|
chosenUnitGroup: UnitGroup?,
|
||||||
selectAction: (UnitGroup?) -> Unit,
|
selectAction: (UnitGroup?) -> Unit,
|
||||||
navigateToSettingsAction: () -> Unit,
|
navigateToSettingsAction: () -> Unit,
|
||||||
lazyListState: LazyListState
|
|
||||||
) {
|
) {
|
||||||
LazyRow(
|
var expanded by remember { mutableStateOf(false) }
|
||||||
modifier = Modifier
|
val chipModifier = Modifier.padding(horizontal = 4.dp)
|
||||||
.padding(bottom = 4.dp)
|
|
||||||
.fillMaxWidth(),
|
AnimatedContent(
|
||||||
state = lazyListState,
|
targetState = expanded,
|
||||||
contentPadding = PaddingValues(8.dp),
|
transitionSpec = {
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
expandVertically(expandFrom = Alignment.Top) { it } + fadeIn() togetherWith
|
||||||
) {
|
shrinkVertically(shrinkTowards = Alignment.Top) { it } + fadeOut()
|
||||||
items(items, key = { it.name }) { item ->
|
}
|
||||||
val selected: Boolean = item == chosenUnitGroup
|
) { isExpanded ->
|
||||||
FilterChip(
|
FlexRow(
|
||||||
selected = selected,
|
modifier = modifier,
|
||||||
onClick = { selectAction(if (selected) null else item) },
|
maxRows = if (isExpanded) Int.MAX_VALUE else 2,
|
||||||
label = stringResource(item.res),
|
mainContent = {
|
||||||
imageVector = Icons.Default.Check,
|
items.forEach { item ->
|
||||||
contentDescription = stringResource(R.string.checked_filter_description)
|
val selected: Boolean = item == chosenUnitGroup
|
||||||
)
|
FilterChip(
|
||||||
|
modifier = chipModifier,
|
||||||
|
isSelected = selected,
|
||||||
|
onClick = {
|
||||||
|
selectAction(if (selected) null else item)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
label = stringResource(item.res),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AssistChip(
|
||||||
|
modifier = chipModifier,
|
||||||
|
onClick = navigateToSettingsAction,
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = stringResource(R.string.open_settings_label)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
AssistChip(
|
||||||
|
modifier = chipModifier,
|
||||||
|
onClick = { expanded = false },
|
||||||
|
imageVector = Icons.Default.ExpandLess,
|
||||||
|
contentDescription = "" // TODO
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expandContent = {
|
||||||
|
AssistChip(
|
||||||
|
modifier = chipModifier,
|
||||||
|
onClick = { expanded = true },
|
||||||
|
imageVector = Icons.Default.ExpandMore,
|
||||||
|
contentDescription = "" // TODO
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foldable row that places a specified element if overflown.
|
||||||
|
*
|
||||||
|
* @param modifier [Modifier] to be applied to this layout.
|
||||||
|
* @param maxRows Max amount of rows including with [expandContent].
|
||||||
|
* @param mainContent Main content (list of items) that will be folded.
|
||||||
|
* @param expandContent Item that will be placed at the end if given [maxRows] wasn't high enough to
|
||||||
|
* place all [mainContent] items.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun FlexRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
maxRows: Int = Int.MAX_VALUE,
|
||||||
|
mainContent: @Composable () -> Unit,
|
||||||
|
expandContent: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(
|
||||||
|
modifier = modifier
|
||||||
|
) { constraints ->
|
||||||
|
val localConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
val layoutWidth = localConstraints.maxWidth
|
||||||
|
|
||||||
|
val mainMeasurables = subcompose(FlexRowSlots.Main, mainContent)
|
||||||
|
val expandMeasurables = subcompose(
|
||||||
|
slotId = FlexRowSlots.Expand,
|
||||||
|
content = { expandContent() }
|
||||||
|
).map {
|
||||||
|
it.measure(localConstraints)
|
||||||
|
}
|
||||||
|
val expandContentWidth = expandMeasurables.sumOf { it.measuredWidth }
|
||||||
|
|
||||||
|
val placeables = mutableListOf<MutableList<Placeable>>(mutableListOf())
|
||||||
|
|
||||||
|
var widthLeft = layoutWidth
|
||||||
|
var index = 0
|
||||||
|
for (measurable in mainMeasurables) {
|
||||||
|
val mainPlaceable = measurable.measure(localConstraints)
|
||||||
|
|
||||||
|
val lastAvailableRow = placeables.size >= maxRows
|
||||||
|
val notLastItem = index < mainMeasurables.lastIndex
|
||||||
|
|
||||||
|
// count expandContent width only for last row and not last main placeable
|
||||||
|
val measuredWidth = if (lastAvailableRow and notLastItem) {
|
||||||
|
mainPlaceable.measuredWidth + expandContentWidth
|
||||||
|
} else {
|
||||||
|
mainPlaceable.measuredWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// need new row
|
||||||
|
if (widthLeft <= measuredWidth) {
|
||||||
|
// Can't add more rows, add expandContent
|
||||||
|
if (lastAvailableRow) {
|
||||||
|
expandMeasurables.forEach {
|
||||||
|
placeables.last().add(it)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
placeables.add(mutableListOf())
|
||||||
|
widthLeft = layoutWidth
|
||||||
|
}
|
||||||
|
placeables.last().add(mainPlaceable)
|
||||||
|
index++
|
||||||
|
widthLeft -= mainPlaceable.measuredWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
val flattenPlaceables = placeables.flatten()
|
||||||
* Usually this chip is placed at the start, but since we scroll to currently selected
|
val layoutHeight = placeables.size * (flattenPlaceables.maxByOrNull { it.height }?.height ?: 0)
|
||||||
* chip, user may never find it. There is higher chance that user will notice this chip when
|
|
||||||
* scrolling right (to the last one).
|
layout(layoutWidth, layoutHeight) {
|
||||||
*/
|
var yPos = 0
|
||||||
item("settings") {
|
placeables.forEach { row ->
|
||||||
AssistChip(
|
var xPos = 0
|
||||||
onClick = navigateToSettingsAction,
|
row.forEach { placeable ->
|
||||||
imageVector = Icons.Default.Settings,
|
placeable.place(x = xPos, y = yPos)
|
||||||
contentDescription = stringResource(R.string.open_settings_label)
|
xPos += placeable.width
|
||||||
)
|
}
|
||||||
|
yPos += row.maxByOrNull { it.height }?.height ?: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
private enum class FlexRowSlots { Main, Expand }
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=380dp,height=850.9dp,dpi=440")
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewUnittoChips() {
|
fun PreviewUnittoChips() {
|
||||||
var selected by remember { mutableStateOf<UnitGroup?>(UnitGroup.LENGTH) }
|
var selected by remember { mutableStateOf<UnitGroup?>(UnitGroup.LENGTH) }
|
||||||
@ -103,11 +215,25 @@ fun PreviewUnittoChips() {
|
|||||||
selected = unitGroup
|
selected = unitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
ChipsRow(
|
Column {
|
||||||
items = UnitGroup.entries,
|
ChipsRow(
|
||||||
chosenUnitGroup = selected,
|
modifier = Modifier
|
||||||
selectAction = { selectAction(it) },
|
.background(MaterialTheme.colorScheme.background)
|
||||||
navigateToSettingsAction = {},
|
.fillMaxWidth(),
|
||||||
lazyListState = rememberLazyListState()
|
items = UnitGroup.entries.take(7),
|
||||||
)
|
chosenUnitGroup = selected,
|
||||||
|
selectAction = { selectAction(it) },
|
||||||
|
navigateToSettingsAction = {},
|
||||||
|
)
|
||||||
|
|
||||||
|
ChipsRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
items = UnitGroup.entries.take(10),
|
||||||
|
chosenUnitGroup = selected,
|
||||||
|
selectAction = { selectAction(it) },
|
||||||
|
navigateToSettingsAction = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user