From e3b569ca5baf97875d5ff92c6cfbba51c5fbe1f2 Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Wed, 7 Jan 2026 10:55:09 -0500 Subject: [PATCH] Calculate drag handle bounds using the container width instead of the screen width. --- .../app/chats/folders/ChatFoldersFragment.kt | 12 ++-- .../conversation/v2/CreatePollFragment.kt | 8 +-- .../stickers/StickerManagementActivity.kt | 8 +-- .../copied/androidx/compose/DragAndDrop.kt | 56 ++++++++++++------- .../androidx/compose/DragGestureDetector.kt | 13 ++--- 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt index 04697ef75d..5c90216188 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/folders/ChatFoldersFragment.kt @@ -31,8 +31,6 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -60,7 +58,6 @@ import org.signal.core.util.toInt import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate /** @@ -127,8 +124,6 @@ fun FoldersScreen( onDeleteDismissed: () -> Unit = {}, onDragAndDropEvent: (DragAndDropEvent) -> Unit = {} ) { - val screenWidth = LocalConfiguration.current.screenWidthDp.dp - val isRtl = ViewUtil.isRtl(LocalContext.current) val listState = rememberLazyListState() val dragDropState = rememberDragDropState(listState, includeHeader = true, includeFooter = true, onEvent = onDragAndDropEvent) @@ -153,8 +148,7 @@ fun FoldersScreen( LazyColumn( modifier = Modifier.dragContainer( dragDropState = dragDropState, - leftDpOffset = if (isRtl) 0.dp else screenWidth - 48.dp, - rightDpOffset = if (isRtl) 48.dp else screenWidth + dragHandleWidth = 56.dp ), state = listState ) { @@ -220,6 +214,7 @@ fun FoldersScreen( onAdd = { onAdd(chatFolder) } ) } + ChatFolderRecord.FolderType.INDIVIDUAL -> { val title: String = stringResource(R.string.ChatFoldersFragment__one_on_one_chats) FolderRow( @@ -229,6 +224,7 @@ fun FoldersScreen( onAdd = { onAdd(chatFolder) } ) } + ChatFolderRecord.FolderType.GROUP -> { val title: String = stringResource(R.string.ChatFoldersFragment__groups) FolderRow( @@ -238,9 +234,11 @@ fun FoldersScreen( onAdd = { onAdd(chatFolder) } ) } + ChatFolderRecord.FolderType.ALL -> { error("All chats should not be suggested") } + ChatFolderRecord.FolderType.CUSTOM -> { error("Custom folders should not be suggested") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt index f8815716a5..1923b686bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/CreatePollFragment.kt @@ -40,8 +40,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -154,8 +152,6 @@ private fun CreatePollScreen( var focusedOption by remember { mutableStateOf(-1) } // Drag and drop - val screenWidth = LocalConfiguration.current.screenWidthDp.dp - val isRtl = ViewUtil.isRtl(LocalContext.current) val listState = rememberLazyListState() val dragDropState = rememberDragDropState(listState, includeHeader = true, includeFooter = true, onEvent = { event -> when (event) { @@ -164,6 +160,7 @@ private fun CreatePollScreen( options[event.fromIndex] = options[event.toIndex] options[event.toIndex] = oldIndex } + is DragAndDropEvent.OnItemDrop, is DragAndDropEvent.OnDragCancel -> Unit } }) @@ -208,8 +205,7 @@ private fun CreatePollScreen( .imePadding() .dragContainer( dragDropState = dragDropState, - leftDpOffset = if (isRtl) 0.dp else screenWidth - 56.dp, - rightDpOffset = if (isRtl) 56.dp else screenWidth + dragHandleWidth = 56.dp ), state = listState ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.kt index dd8a1db813..f093bb2007 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManagementActivity.kt @@ -49,17 +49,14 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -484,8 +481,6 @@ private fun InstalledStickersContent( val listState = rememberLazyListState() val dragDropState = rememberDragDropState(lazyListState = listState, includeHeader = true, includeFooter = false, onEvent = callbacks::onDragAndDropEvent) - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val screenWidth = LocalConfiguration.current.screenWidthDp.dp val density = LocalDensity.current val haptics = LocalHapticFeedback.current @@ -503,8 +498,7 @@ private fun InstalledStickersContent( .fillMaxHeight() .dragContainer( dragDropState = dragDropState, - leftDpOffset = if (isRtl) 0.dp else screenWidth - 56.dp, - rightDpOffset = if (isRtl) 56.dp else screenWidth + dragHandleWidth = 56.dp ) ) { item(key = "installed_section_header") { diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragAndDrop.kt b/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragAndDrop.kt index 25913c55a9..a8a0d846ca 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragAndDrop.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragAndDrop.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.zIndex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -161,14 +163,11 @@ internal constructor( } draggingItemIndex = targetItem.index } else { - val overscroll = - when { - draggingItemDraggedDelta > 0 -> - (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) - draggingItemDraggedDelta < 0 -> - (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) - else -> 0f - } + val overscroll = when { + draggingItemDraggedDelta > 0 -> (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + draggingItemDraggedDelta < 0 -> (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + else -> 0f + } if (overscroll != 0f) { scrollChannel.trySend(overscroll) } @@ -198,21 +197,34 @@ sealed interface DragAndDropEvent { data object OnDragCancel : DragAndDropEvent } +/** + * Enables drag-to-reorder functionality within a container. + * + * @param dragDropState The state managing the drag operation. + * @param dragHandleWidth Width of the draggable area (positioned at the end of the container). + */ +@Composable fun Modifier.dragContainer( dragDropState: DragDropState, - leftDpOffset: Dp, - rightDpOffset: Dp + dragHandleWidth: Dp ): Modifier { - return pointerInput(dragDropState) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + return pointerInput(dragDropState, dragHandleWidth, isRtl) { + val containerWidthPx = size.width.toFloat() + val handleWidthPx = dragHandleWidth.toPx() + + val dragHandleXRange = if (isRtl) { + 0f..handleWidthPx + } else { + (containerWidthPx - handleWidthPx)..containerWidthPx + } + detectDragGestures( - onDrag = { change, offset -> - dragDropState.onDrag(offset = offset, change = change) - }, + dragHandleXRange = dragHandleXRange, + onDrag = { change, offset -> dragDropState.onDrag(offset = offset, change = change) }, onDragStart = { offset -> dragDropState.onDragStart(offset) }, onDragEnd = { dragDropState.onDragEnd() }, - onDragCancel = { dragDropState.onDragCancel() }, - leftDpOffset = leftDpOffset, - rightDpOffset = rightDpOffset + onDragCancel = { dragDropState.onDragCancel() } ) } } @@ -227,11 +239,13 @@ fun LazyItemScope.DraggableItem( val dragging = index == dragDropState.draggingItemIndex val draggingModifier = if (dragging) { - Modifier.zIndex(1f).graphicsLayer { translationY = dragDropState.draggingItemOffset } + Modifier + .zIndex(1f) + .graphicsLayer { translationY = dragDropState.draggingItemOffset } } else if (index == dragDropState.previousIndexOfDraggedItem) { - Modifier.zIndex(1f).graphicsLayer { - translationY = dragDropState.previousItemOffset.value - } + Modifier + .zIndex(1f) + .graphicsLayer { translationY = dragDropState.previousItemOffset.value } } else { Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) } diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragGestureDetector.kt b/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragGestureDetector.kt index 8a37c8c8d5..062fc1d834 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragGestureDetector.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragGestureDetector.kt @@ -15,8 +15,6 @@ import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed import androidx.compose.ui.input.pointer.isOutOfBounds import androidx.compose.ui.input.pointer.positionChange -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFirstOrNull @@ -25,21 +23,22 @@ import kotlinx.coroutines.CancellationException /** * Modified version of detectDragGesturesAfterLongPress from [androidx.compose.foundation.gestures.DragGestureDetector] - * that allows you to optionally offset the starting and ending position of the draggable area + * that initiates drags when the touch starts within a specified x coordinate range. + * + * @param dragHandleXRange The x coordinate range (in pixels) where drags can be initiated. */ suspend fun PointerInputScope.detectDragGestures( + dragHandleXRange: ClosedFloatingPointRange, onDragStart: (Offset) -> Unit = { }, onDragEnd: () -> Unit = { }, onDragCancel: () -> Unit = { }, - onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, - leftDpOffset: Dp = 0.dp, - rightDpOffset: Dp + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit ) { awaitEachGesture { try { val down = awaitFirstDown(requireUnconsumed = false) val drag = awaitLongPressOrCancellation(down.id) - if (drag != null && (drag.position.x > leftDpOffset.toPx()) && (drag.position.x < rightDpOffset.toPx())) { + if (drag != null && down.position.x in dragHandleXRange) { onDragStart.invoke(drag.position) if (