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 b119cff25d..72fdf2e344 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 @@ -52,10 +52,10 @@ import org.signal.core.ui.compose.DropdownMenus import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons -import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent -import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem -import org.signal.core.ui.compose.copied.androidx.compose.dragContainer -import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState +import org.signal.core.ui.compose.list.ReorderListEvent +import org.signal.core.ui.compose.list.ReorderableItem +import org.signal.core.ui.compose.list.rememberReorderableListState +import org.signal.core.ui.compose.list.reorderableList import org.signal.core.util.toInt import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -101,11 +101,11 @@ class ChatFoldersFragment : ComposeFragment() { onDeleteDismissed = { viewModel.showDeleteDialog(false) }, - onDragAndDropEvent = { event -> + onReorderListEvent = { event -> when (event) { - is DragAndDropEvent.OnItemMove -> viewModel.updateItemPosition(event.fromIndex, event.toIndex) - is DragAndDropEvent.OnItemDrop -> viewModel.saveItemPositions() - is DragAndDropEvent.OnDragCancel -> {} + is ReorderListEvent.ItemMoved -> viewModel.updateItemPosition(event.fromIndex, event.toIndex) + is ReorderListEvent.ItemDropped -> viewModel.saveItemPositions() + is ReorderListEvent.DragCanceled -> {} } } ) @@ -123,10 +123,10 @@ fun FoldersScreen( onDeleteClicked: (ChatFolderRecord) -> Unit = {}, onDeleteConfirmed: () -> Unit = {}, onDeleteDismissed: () -> Unit = {}, - onDragAndDropEvent: (DragAndDropEvent) -> Unit = {} + onReorderListEvent: (ReorderListEvent) -> Unit = {} ) { val listState = rememberLazyListState() - val dragDropState = rememberDragDropState(listState, includeHeader = true, includeFooter = true, onEvent = onDragAndDropEvent) + val reorderableListState = rememberReorderableListState(listState, includeHeader = true, includeFooter = true, onEvent = onReorderListEvent) LaunchedEffect(Unit) { if (!SignalStore.uiHints.hasSeenChatFoldersEducationSheet) { @@ -147,14 +147,14 @@ fun FoldersScreen( } LazyColumn( - modifier = Modifier.dragContainer( - dragDropState = dragDropState, + modifier = Modifier.reorderableList( + reorderableListState = reorderableListState, dragHandleWidth = 56.dp ), state = listState ) { item { - DraggableItem(dragDropState, 0) { + ReorderableItem(reorderableListState, 0) { Text( text = stringResource(id = R.string.ChatFoldersFragment__organize_your_chats), style = MaterialTheme.typography.bodyMedium, @@ -175,7 +175,7 @@ fun FoldersScreen( } itemsIndexed(state.folders) { index, folder -> - DraggableItem(dragDropState, 1 + index) { isDragging -> + ReorderableItem(reorderableListState, 1 + index) { isDragging -> val elevation = if (isDragging) 1.dp else 0.dp val isAllChats = folder.folderType == ChatFolderRecord.FolderType.ALL FolderRow( @@ -193,7 +193,7 @@ fun FoldersScreen( } item { - DraggableItem(dragDropState, 1 + state.folders.size) { + ReorderableItem(reorderableListState, 1 + state.folders.size) { if (state.suggestedFolders.isNotEmpty()) { Dividers.Default() 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 8f70ea2b4b..e753ec334c 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 @@ -59,10 +59,10 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Rows import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons -import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent -import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem -import org.signal.core.ui.compose.copied.androidx.compose.dragContainer -import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState +import org.signal.core.ui.compose.list.ReorderListEvent +import org.signal.core.ui.compose.list.ReorderableItem +import org.signal.core.ui.compose.list.rememberReorderableListState +import org.signal.core.ui.compose.list.reorderableList import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.polls.Poll @@ -154,15 +154,15 @@ private fun CreatePollScreen( // Drag and drop val listState = rememberLazyListState() - val dragDropState = rememberDragDropState(listState, includeHeader = true, includeFooter = true, onEvent = { event -> + val reorderableListState = rememberReorderableListState(listState, includeHeader = true, includeFooter = true, onEvent = { event -> when (event) { - is DragAndDropEvent.OnItemMove -> { + is ReorderListEvent.ItemMoved -> { val oldIndex = options[event.fromIndex] options[event.fromIndex] = options[event.toIndex] options[event.toIndex] = oldIndex } - is DragAndDropEvent.OnItemDrop, is DragAndDropEvent.OnDragCancel -> Unit + is ReorderListEvent.ItemDropped, is ReorderListEvent.DragCanceled -> Unit } }) @@ -204,14 +204,14 @@ private fun CreatePollScreen( modifier = Modifier .fillMaxHeight() .imePadding() - .dragContainer( - dragDropState = dragDropState, + .reorderableList( + reorderableListState = reorderableListState, dragHandleWidth = 56.dp ), state = listState ) { item { - DraggableItem(dragDropState, 0) { + ReorderableItem(reorderableListState, 0) { Text( text = stringResource(R.string.CreatePollFragment__question), modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), @@ -246,7 +246,7 @@ private fun CreatePollScreen( } itemsIndexed(options) { index, option -> - DraggableItem(dragDropState, 1 + index) { + ReorderableItem(reorderableListState, 1 + index) { TextFieldWithCountdown( value = option, label = { Text(text = stringResource(R.string.CreatePollFragment__option_n, index + 1)) }, @@ -273,7 +273,7 @@ private fun CreatePollScreen( } item { - DraggableItem(dragDropState, 1 + options.size) { + ReorderableItem(reorderableListState, 1 + options.size) { Dividers.Default() Rows.ToggleRow(checked = allowMultiple, text = stringResource(R.string.CreatePollFragment__allow_multiple_votes), onCheckChanged = { allowMultiple = it }) Spacer(modifier = Modifier.size(60.dp)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementActivity.kt index 417a1f8e0f..6458b9ec60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementActivity.kt @@ -16,7 +16,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.launch -import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent +import org.signal.core.ui.compose.list.ReorderListEvent import org.signal.core.ui.compose.theme.SignalTheme import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R @@ -73,11 +73,11 @@ class StickerManagementActivity : PassphraseRequiredActivity() { override fun onRemoveStickerPacksCanceled() = viewModel.onUninstallStickerPacksCanceled() override fun onSelectionToggle(pack: InstalledStickerPack) = viewModel.toggleSelection(pack) override fun onSelectAllToggle() = viewModel.toggleSelectAll() - override fun onDragAndDropEvent(event: DragAndDropEvent) { + override fun onReorderableEvent(event: ReorderListEvent) { when (event) { - is DragAndDropEvent.OnItemMove -> viewModel.updatePosition(event.fromIndex, event.toIndex) - is DragAndDropEvent.OnItemDrop -> viewModel.saveInstalledPacksSortOrder() - is DragAndDropEvent.OnDragCancel -> {} + is ReorderListEvent.ItemMoved -> viewModel.updatePosition(event.fromIndex, event.toIndex) + is ReorderListEvent.ItemDropped -> viewModel.saveInstalledPacksSortOrder() + is ReorderListEvent.DragCanceled -> {} } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementBottomSheet.kt index 0ba856d18f..a725739419 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementBottomSheet.kt @@ -29,7 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.coroutines.launch import org.signal.core.ui.compose.BottomSheets import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment -import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent +import org.signal.core.ui.compose.list.ReorderListEvent import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs @@ -120,11 +120,11 @@ class StickerManagementBottomSheet : ComposeBottomSheetDialogFragment() { override fun onRemoveStickerPacksCanceled() = viewModel.onUninstallStickerPacksCanceled() override fun onSelectionToggle(pack: InstalledStickerPack) = viewModel.toggleSelection(pack) override fun onSelectAllToggle() = viewModel.toggleSelectAll() - override fun onDragAndDropEvent(event: DragAndDropEvent) { + override fun onReorderableEvent(event: ReorderListEvent) { when (event) { - is DragAndDropEvent.OnItemMove -> viewModel.updatePosition(event.fromIndex, event.toIndex) - is DragAndDropEvent.OnItemDrop -> viewModel.saveInstalledPacksSortOrder() - is DragAndDropEvent.OnDragCancel -> {} + is ReorderListEvent.ItemMoved -> viewModel.updatePosition(event.fromIndex, event.toIndex) + is ReorderListEvent.ItemDropped -> viewModel.saveInstalledPacksSortOrder() + is ReorderListEvent.DragCanceled -> {} } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementScreen.kt index 48d8d79746..5e1f66c18b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/manage/StickerManagementScreen.kt @@ -69,10 +69,10 @@ import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons import org.signal.core.ui.compose.Snackbars -import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent -import org.signal.core.ui.compose.copied.androidx.compose.DraggableItem -import org.signal.core.ui.compose.copied.androidx.compose.dragContainer -import org.signal.core.ui.compose.copied.androidx.compose.rememberDragDropState +import org.signal.core.ui.compose.list.ReorderListEvent +import org.signal.core.ui.compose.list.ReorderableItem +import org.signal.core.ui.compose.list.rememberReorderableListState +import org.signal.core.ui.compose.list.reorderableList import org.signal.core.ui.compose.showSnackbar import org.signal.core.ui.getWindowSizeClass import org.thoughtcrime.securesms.R @@ -140,7 +140,7 @@ interface InstalledStickersContentCallbacks { fun onRemoveStickerPacksCanceled() fun onSelectionToggle(pack: InstalledStickerPack) fun onSelectAllToggle() - fun onDragAndDropEvent(event: DragAndDropEvent) + fun onReorderableEvent(event: ReorderListEvent) fun onShowPreviewClick(pack: InstalledStickerPack) object Empty : InstalledStickersContentCallbacks { @@ -150,7 +150,7 @@ interface InstalledStickersContentCallbacks { override fun onRemoveStickerPacksCanceled() = Unit override fun onSelectionToggle(pack: InstalledStickerPack) = Unit override fun onSelectAllToggle() = Unit - override fun onDragAndDropEvent(event: DragAndDropEvent) = Unit + override fun onReorderableEvent(event: ReorderListEvent) = Unit override fun onShowPreviewClick(pack: InstalledStickerPack) = Unit } } @@ -456,7 +456,7 @@ private fun InstalledStickersContent( EmptyView(text = stringResource(R.string.StickerManagement_installed_tab_empty_text)) } else { val listState = rememberLazyListState() - val dragDropState = rememberDragDropState(lazyListState = listState, includeHeader = true, includeFooter = false, onEvent = callbacks::onDragAndDropEvent) + val reorderableListState = rememberReorderableListState(lazyListState = listState, includeHeader = true, includeFooter = false, onEvent = callbacks::onReorderableEvent) val density = LocalDensity.current val haptics = LocalHapticFeedback.current @@ -473,13 +473,13 @@ private fun InstalledStickersContent( state = listState, modifier = modifier .fillMaxHeight() - .dragContainer( - dragDropState = dragDropState, + .reorderableList( + reorderableListState = reorderableListState, dragHandleWidth = 56.dp ) ) { item(key = "installed_section_header") { - DraggableItem(dragDropState, 0) { + ReorderableItem(reorderableListState, 0) { StickerPackSectionHeader( text = stringResource(R.string.StickerManagement_installed_stickers_header), modifier = Modifier.animateItem() @@ -493,9 +493,9 @@ private fun InstalledStickersContent( ) { index, pack -> val menuController = remember { DropdownMenus.MenuController() } - DraggableItem( + ReorderableItem( index = index + 1, - dragDropState = dragDropState + reorderableListState = reorderableListState ) { isDragging -> InstalledStickerPackRow( pack = pack, 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/list/DragGestureDetector.kt similarity index 98% rename from core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragGestureDetector.kt rename to core/ui/src/main/java/org/signal/core/ui/compose/list/DragGestureDetector.kt index d887e90606..cdb561a25e 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/list/DragGestureDetector.kt @@ -1,4 +1,4 @@ -package org.signal.core.ui.compose.copied.androidx.compose +package org.signal.core.ui.compose.list import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown 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/list/DragToReorder.kt similarity index 81% rename from core/ui/src/main/java/org/signal/core/ui/compose/copied/androidx/compose/DragAndDrop.kt rename to core/ui/src/main/java/org/signal/core/ui/compose/list/DragToReorder.kt index 4c87feb7c0..b05faf0a49 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/list/DragToReorder.kt @@ -1,4 +1,9 @@ -package org.signal.core.ui.compose.copied.androidx.compose +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.ui.compose.list import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring @@ -32,29 +37,30 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.signal.core.ui.compose.copied.androidx.compose.DragAndDropEvent.OnItemMove +import org.signal.core.ui.compose.list.ReorderListEvent.ItemMoved /** - * From AndroidX Compose demo + * Adapted from the AndroidX Compose demo * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt * - * Allows for dragging and dropping to reorder within lazy columns - * Supports adding non-draggable headers and footers. + * - Allows for dragging and dropping to reorder within lazy columns. + * - Supports adding non-draggable headers and footers. */ @Composable -fun rememberDragDropState( +fun rememberReorderableListState( lazyListState: LazyListState, includeHeader: Boolean, includeFooter: Boolean, - onEvent: (DragAndDropEvent) -> Unit = {} -): DragDropState { + onEvent: (ReorderListEvent) -> Unit = {} +): ReorderableListState { val scope = rememberCoroutineScope() val state = remember(lazyListState) { - DragDropState(state = lazyListState, onEvent = onEvent, includeHeader = includeHeader, includeFooter = includeFooter, scope = scope) + ReorderableListState(state = lazyListState, onEvent = onEvent, includeHeader = includeHeader, includeFooter = includeFooter, scope = scope) } val maxAutoScrollSpeed = with(LocalDensity.current) { 30.dp.toPx() } val baseAutoScrollSpeed = with(LocalDensity.current) { 10.dp.toPx() } val scrollAcceleration = 2f + LaunchedEffect(state) { while (true) { withFrameNanos { } @@ -73,13 +79,12 @@ fun rememberDragDropState( return state } -class DragDropState -internal constructor( +class ReorderableListState internal constructor( private val state: LazyListState, private val scope: CoroutineScope, private val includeHeader: Boolean, private val includeFooter: Boolean, - private val onEvent: (DragAndDropEvent) -> Unit + private val onEvent: (ReorderListEvent) -> Unit ) { var draggingItemIndex by mutableStateOf(null) private set @@ -119,12 +124,12 @@ internal constructor( internal fun onDragEnd() { onDragInterrupted() - onEvent(DragAndDropEvent.OnItemDrop) + onEvent(ReorderListEvent.ItemDropped) } internal fun onDragCancel() { onDragInterrupted() - onEvent(DragAndDropEvent.OnDragCancel) + onEvent(ReorderListEvent.DragCanceled) } private fun onDragInterrupted() { @@ -221,9 +226,9 @@ internal constructor( private fun performSwap(draggingItem: LazyListItemInfo, targetItem: LazyListItemInfo) { if (includeHeader) { - onEvent.invoke(OnItemMove(fromIndex = draggingItem.index - 1, toIndex = targetItem.index - 1)) + onEvent.invoke(ItemMoved(fromIndex = draggingItem.index - 1, toIndex = targetItem.index - 1)) } else { - onEvent.invoke(OnItemMove(fromIndex = draggingItem.index, toIndex = targetItem.index)) + onEvent.invoke(ItemMoved(fromIndex = draggingItem.index, toIndex = targetItem.index)) } draggingItemIndex = targetItem.index } @@ -232,38 +237,38 @@ internal constructor( get() = this.offset + this.size } -sealed interface DragAndDropEvent { +sealed interface ReorderListEvent { /** * Triggered when an item is moving from one position to another. * * The ordering of the corresponding UI state should be updated when this event is received. */ - data class OnItemMove(val fromIndex: Int, val toIndex: Int) : DragAndDropEvent + data class ItemMoved(val fromIndex: Int, val toIndex: Int) : ReorderListEvent /** * Triggered when a dragged item is dropped into its final position. */ - data object OnItemDrop : DragAndDropEvent + data object ItemDropped : ReorderListEvent /** * Triggered when a drag gesture is canceled. */ - data object OnDragCancel : DragAndDropEvent + data object DragCanceled : ReorderListEvent } /** * Enables drag-to-reorder functionality within a container. * - * @param dragDropState The state managing the drag operation. + * @param reorderableListState 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, +fun Modifier.reorderableList( + reorderableListState: ReorderableListState, dragHandleWidth: Dp ): Modifier { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - return pointerInput(dragDropState, dragHandleWidth, isRtl) { + return pointerInput(reorderableListState, dragHandleWidth, isRtl) { val containerWidthPx = size.width.toFloat() val handleWidthPx = dragHandleWidth.toPx() @@ -275,31 +280,31 @@ fun Modifier.dragContainer( detectDragGestures( dragHandleXRange = dragHandleXRange, - onDrag = { change, offset -> dragDropState.onDrag(offset = offset, change = change) }, - onDragStart = { offset -> dragDropState.onDragStart(offset) }, - onDragEnd = { dragDropState.onDragEnd() }, - onDragCancel = { dragDropState.onDragCancel() } + onDrag = { change, offset -> reorderableListState.onDrag(offset = offset, change = change) }, + onDragStart = { offset -> reorderableListState.onDragStart(offset) }, + onDragEnd = { reorderableListState.onDragEnd() }, + onDragCancel = { reorderableListState.onDragCancel() } ) } } @Composable -fun LazyItemScope.DraggableItem( - dragDropState: DragDropState, +fun LazyItemScope.ReorderableItem( + reorderableListState: ReorderableListState, index: Int, modifier: Modifier = Modifier, content: @Composable ColumnScope.(isDragging: Boolean) -> Unit ) { - val dragging = index == dragDropState.draggingItemIndex + val dragging = index == reorderableListState.draggingItemIndex val draggingModifier = if (dragging) { Modifier .zIndex(1f) - .graphicsLayer { translationY = dragDropState.draggingItemOffset } - } else if (index == dragDropState.previousIndexOfDraggedItem) { + .graphicsLayer { translationY = reorderableListState.draggingItemOffset } + } else if (index == reorderableListState.previousIndexOfDraggedItem) { Modifier .zIndex(1f) - .graphicsLayer { translationY = dragDropState.previousItemOffset.value } + .graphicsLayer { translationY = reorderableListState.previousItemOffset.value } } else { Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) }