Merge pull request #1768 from vitorpamplona/claude/add-drag-drop-reactions-43YJ2

Replace move buttons with drag-to-reorder in reactions settings
This commit is contained in:
Vitor Pamplona
2026-03-04 17:08:14 -05:00
committed by GitHub
2 changed files with 129 additions and 53 deletions
@@ -20,6 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -32,25 +34,30 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ReactionRowAction
@@ -87,6 +94,10 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) {
accountViewModel.changeReactionRowItems(newItems)
}
var draggedItemIndex by remember { mutableIntStateOf(-1) }
var dragOffset by remember { mutableFloatStateOf(0f) }
val itemHeights = remember { mutableStateMapOf<Int, Float>() }
Column(
modifier =
Modifier
@@ -104,10 +115,18 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) {
)
items.forEachIndexed { index, item ->
val isDragging = draggedItemIndex == index
val targetElevation = if (isDragging) 8f else 0f
val animatedElevation by animateFloatAsState(
targetValue = targetElevation,
label = "dragElevation",
)
ReactionRowItemCard(
item = item,
canMoveUp = index > 0,
canMoveDown = index < items.lastIndex,
isDragging = isDragging,
dragOffsetY = if (isDragging) dragOffset else 0f,
elevation = animatedElevation,
onToggleEnabled = {
val newItems = items.toMutableList()
newItems[index] = item.copy(enabled = !item.enabled)
@@ -118,24 +137,73 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) {
newItems[index] = item.copy(showCounter = !item.showCounter)
save(newItems)
},
onMoveUp = {
if (index > 0) {
val newItems = items.toMutableList()
val temp = newItems[index - 1]
newItems[index - 1] = newItems[index]
newItems[index] = temp
save(newItems)
onMeasured = { height ->
itemHeights[index] = height
},
onDragStart = {
draggedItemIndex = index
dragOffset = 0f
},
onDrag = { dragAmount ->
dragOffset += dragAmount
val currentIndex = draggedItemIndex
if (currentIndex < 0) return@ReactionRowItemCard
// Check if we should swap with the item above
if (dragOffset < 0 && currentIndex > 0) {
val aboveHeight = itemHeights[currentIndex - 1] ?: 0f
if (-dragOffset > aboveHeight / 2f) {
val newItems = items.toMutableList()
val temp = newItems[currentIndex - 1]
newItems[currentIndex - 1] = newItems[currentIndex]
newItems[currentIndex] = temp
items = newItems
// Transfer heights
val h1 = itemHeights[currentIndex]
val h2 = itemHeights[currentIndex - 1]
if (h1 != null) itemHeights[currentIndex - 1] = h1
if (h2 != null) itemHeights[currentIndex] = h2
dragOffset += aboveHeight
draggedItemIndex = currentIndex - 1
}
}
// Check if we should swap with the item below
if (dragOffset > 0 && currentIndex < items.lastIndex) {
val belowHeight = itemHeights[currentIndex + 1] ?: 0f
if (dragOffset > belowHeight / 2f) {
val newItems = items.toMutableList()
val temp = newItems[currentIndex + 1]
newItems[currentIndex + 1] = newItems[currentIndex]
newItems[currentIndex] = temp
items = newItems
// Transfer heights
val h1 = itemHeights[currentIndex]
val h2 = itemHeights[currentIndex + 1]
if (h1 != null) itemHeights[currentIndex + 1] = h1
if (h2 != null) itemHeights[currentIndex] = h2
dragOffset -= belowHeight
draggedItemIndex = currentIndex + 1
}
}
},
onMoveDown = {
if (index < items.lastIndex) {
val newItems = items.toMutableList()
val temp = newItems[index + 1]
newItems[index + 1] = newItems[index]
newItems[index] = temp
save(newItems)
}
onDragEnd = {
draggedItemIndex = -1
dragOffset = 0f
save(items)
},
onDragCancel = {
draggedItemIndex = -1
dragOffset = 0f
},
modifier =
Modifier
.zIndex(if (isDragging) 1f else 0f),
)
if (index < items.lastIndex) {
HorizontalDivider()
@@ -149,20 +217,36 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) {
@Composable
private fun ReactionRowItemCard(
item: ReactionRowItem,
canMoveUp: Boolean,
canMoveDown: Boolean,
isDragging: Boolean,
dragOffsetY: Float,
elevation: Float,
onToggleEnabled: () -> Unit,
onToggleCounter: () -> Unit,
onMoveUp: () -> Unit,
onMoveDown: () -> Unit,
onMeasured: (Float) -> Unit,
onDragStart: () -> Unit,
onDrag: (Float) -> Unit,
onDragEnd: () -> Unit,
onDragCancel: () -> Unit,
modifier: Modifier = Modifier,
) {
val actionName = reactionActionName(item.action)
val actionDescription = reactionActionDescription(item.action)
Column(
modifier =
Modifier
modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
onMeasured(coordinates.size.height.toFloat())
}
.graphicsLayer {
translationY = dragOffsetY
shadowElevation = elevation
if (isDragging) {
scaleX = 1.02f
scaleY = 1.02f
}
}
.padding(vertical = 8.dp),
) {
Row(
@@ -186,32 +270,25 @@ private fun ReactionRowItemCard(
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
IconButton(
onClick = onMoveUp,
enabled = canMoveUp,
modifier = Modifier.size(32.dp),
) {
Icon(
Icons.Default.KeyboardArrowUp,
contentDescription = stringRes(R.string.reactions_settings_move_up),
modifier = Modifier.size(20.dp),
tint = if (canMoveUp) MaterialTheme.colorScheme.onSurface else Color.Gray,
)
}
IconButton(
onClick = onMoveDown,
enabled = canMoveDown,
modifier = Modifier.size(32.dp),
) {
Icon(
Icons.Default.KeyboardArrowDown,
contentDescription = stringRes(R.string.reactions_settings_move_down),
modifier = Modifier.size(20.dp),
tint = if (canMoveDown) MaterialTheme.colorScheme.onSurface else Color.Gray,
)
}
}
Icon(
Icons.Default.DragHandle,
contentDescription = stringRes(R.string.reactions_settings_reorder),
modifier =
Modifier
.size(32.dp)
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = { onDragStart() },
onDrag = { change, dragAmount ->
change.consume()
onDrag(dragAmount.y)
},
onDragEnd = { onDragEnd() },
onDragCancel = { onDragCancel() },
)
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.height(8.dp))
+1 -2
View File
@@ -1238,8 +1238,7 @@
<string name="reactions_settings_description">Configure which reaction buttons are shown, their order, and whether to display counters.</string>
<string name="reactions_settings_enabled">Enabled</string>
<string name="reactions_settings_show_counter">Show Count</string>
<string name="reactions_settings_move_up">Move up</string>
<string name="reactions_settings_move_down">Move down</string>
<string name="reactions_settings_reorder">Reorder</string>
<string name="reactions_settings_reply">Reply</string>
<string name="reactions_settings_reply_description">Reply to this note</string>
<string name="reactions_settings_boost">Boost</string>