From 5b95a47e0e83a0b449a29901d4ea3dc3e21fab36 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 15:29:19 +0000 Subject: [PATCH] refactor: keep lazy itemsIndexed for relay drag-and-drop Replace the single-item Column approach with RelayDragState that works within itemsIndexed. Each relay item remains a separate lazy item for better performance with large lists. The drag handle icon on each row captures drag gestures while the item modifier applies visual feedback (elevation, scale, translation). https://claude.ai/code/session_01RVM5kEJGrCaJTaP3GmVHDd --- .../loggedIn/relays/AllRelayListScreen.kt | 86 ++++++-- .../relays/blocked/BlockedRelayListView.kt | 37 ++-- .../broadcast/BroadcastRelayListView.kt | 37 ++-- .../common/BasicRelaySetupInfoClickableRow.kt | 50 +++-- .../common/BasicRelaySetupInfoDialog.kt | 7 +- .../relays/common/DraggableRelayList.kt | 188 +++++++++--------- .../loggedIn/relays/dm/DMRelayListView.kt | 38 ++-- .../relays/feeds/RelayFeedsListView.kt | 37 ++-- .../relays/indexer/IndexerRelayListView.kt | 39 ++-- .../relays/local/LocalRelayListView.kt | 36 ++-- .../nip37/PrivateOutboxRelayListView.kt | 38 ++-- .../relays/nip65/Nip65RelayListView.kt | 89 +++++---- .../relays/proxy/ProxyRelayListView.kt | 39 ++-- .../relays/search/SearchRelayListView.kt | 39 ++-- .../relays/trusted/TrustedRelayListView.kt | 37 ++-- 15 files changed, 465 insertions(+), 332 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 9a444574f..0f47001b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel @@ -201,6 +202,67 @@ fun MappedAllRelayListView( val indexerCounts by indexerViewModel.countResults.collectAsStateWithLifecycle() val searchCounts by searchViewModel.countResults.collectAsStateWithLifecycle() + val homeDragState = + rememberRelayDragState( + onMove = { from, to -> nip65ViewModel.moveHomeRelay(from, to) }, + itemCount = { homeFeedState.size }, + ) + val notifDragState = + rememberRelayDragState( + onMove = { from, to -> nip65ViewModel.moveNotifRelay(from, to) }, + itemCount = { notifFeedState.size }, + ) + val dmDragState = + rememberRelayDragState( + onMove = { from, to -> dmViewModel.moveRelay(from, to) }, + itemCount = { dmFeedState.size }, + ) + val privateOutboxDragState = + rememberRelayDragState( + onMove = { from, to -> privateOutboxViewModel.moveRelay(from, to) }, + itemCount = { privateOutboxFeedState.size }, + ) + val proxyDragState = + rememberRelayDragState( + onMove = { from, to -> proxyViewModel.moveRelay(from, to) }, + itemCount = { proxyRelays.size }, + ) + val broadcastDragState = + rememberRelayDragState( + onMove = { from, to -> broadcastViewModel.moveRelay(from, to) }, + itemCount = { broadcastRelays.size }, + ) + val indexerDragState = + rememberRelayDragState( + onMove = { from, to -> indexerViewModel.moveRelay(from, to) }, + itemCount = { indexerRelays.size }, + ) + val searchDragState = + rememberRelayDragState( + onMove = { from, to -> searchViewModel.moveRelay(from, to) }, + itemCount = { searchFeedState.size }, + ) + val localDragState = + rememberRelayDragState( + onMove = { from, to -> localViewModel.moveRelay(from, to) }, + itemCount = { localFeedState.size }, + ) + val trustedDragState = + rememberRelayDragState( + onMove = { from, to -> trustedViewModel.moveRelay(from, to) }, + itemCount = { trustedFeedState.size }, + ) + val feedsDragState = + rememberRelayDragState( + onMove = { from, to -> relayFeedsViewModel.moveRelay(from, to) }, + itemCount = { relayFeedsFeedState.size }, + ) + val blockedDragState = + rememberRelayDragState( + onMove = { from, to -> blockedViewModel.moveRelay(from, to) }, + itemCount = { blockedFeedState.size }, + ) + Scaffold( topBar = { SavingTopBar( @@ -274,7 +336,7 @@ fun MappedAllRelayListView( SettingsCategoryFirstModifier, ) } - renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav, outboxCounts) + renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav, outboxCounts, homeDragState) item { SettingsCategory( @@ -283,7 +345,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav, inboxCounts) + renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav, inboxCounts, notifDragState) item { SettingsCategoryWithButton( @@ -295,7 +357,7 @@ fun MappedAllRelayListView( }, ) } - renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav, dmCounts) + renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav, dmCounts, dmDragState) item { SettingsCategory( @@ -304,7 +366,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav, privateHomeCounts) + renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav, privateHomeCounts, privateOutboxDragState) item { SettingsCategory( @@ -313,7 +375,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav, proxyCounts) + renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav, proxyCounts, proxyDragState) item { SettingsCategory( @@ -322,7 +384,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, nav) + renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, nav, broadcastDragState) item { SettingsCategoryWithButton( @@ -333,7 +395,7 @@ fun MappedAllRelayListView( ResetIndexerRelays(indexerViewModel) } } - renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav, indexerCounts) + renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav, indexerCounts, indexerDragState) item { SettingsCategoryWithButton( @@ -344,7 +406,7 @@ fun MappedAllRelayListView( ResetSearchRelays(searchViewModel) } } - renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav, searchCounts) + renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav, searchCounts, searchDragState) item { SettingsCategory( @@ -371,7 +433,7 @@ fun MappedAllRelayListView( ) } } - renderLocalItems(localFeedState, localViewModel, accountViewModel, nav) + renderLocalItems(localFeedState, localViewModel, accountViewModel, nav, localDragState) item { SettingsCategory( @@ -380,7 +442,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, nav) + renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, nav, trustedDragState) item { SettingsCategory( @@ -389,7 +451,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderRelayFeedsItems(relayFeedsFeedState, relayFeedsViewModel, accountViewModel, nav) + renderRelayFeedsItems(relayFeedsFeedState, relayFeedsViewModel, accountViewModel, nav, feedsDragState) item { SettingsCategory( @@ -398,7 +460,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, nav) + renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, nav, blockedDragState) item { SettingsCategory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt index 95b42d6d7..f9d79a24d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,9 +35,10 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -48,14 +50,18 @@ fun BlockedRelayList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderBlockedItems(feedState, postViewModel, accountViewModel, newNav) + renderBlockedItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -65,21 +71,18 @@ fun LazyListScope.renderBlockedItems( postViewModel: BlockedRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + dragState: RelayDragState? = null, ) { - item(key = "Blocked_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Blocked" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt index 770adee79..4ca0a70ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,9 +35,10 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -48,14 +50,18 @@ fun BroadcastRelayList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderBroadcastItems(feedState, postViewModel, accountViewModel, newNav) + renderBroadcastItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -65,21 +71,18 @@ fun LazyListScope.renderBroadcastItems( postViewModel: BroadcastRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + dragState: RelayDragState? = null, ) { - item(key = "Broadcast_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Broadcast" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 2446eb56e..449cedf28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -71,36 +71,54 @@ fun BasicRelaySetupInfoClickableRow( onClick: () -> Unit, nip11CachedRetriever: Nip11CachedRetriever, modifier: Modifier = Modifier, - dragModifier: Modifier = Modifier, + index: Int = -1, + dragState: RelayDragState? = null, countResult: RelayCountResult? = null, accountViewModel: AccountViewModel, nav: INav, ) { val clipboardManager = LocalClipboard.current val scope = rememberCoroutineScope() - Column( - Modifier - .fillMaxWidth() - .combinedClickable( - onClick = onClick, - onLongClick = { - scope.launch { - clipboardManager.setText(item.relay.url) - } - }, - ), - ) { + + val itemModifier = + if (dragState != null && index >= 0) { + Modifier + .fillMaxWidth() + .draggableRelayItem(index, dragState) + .combinedClickable( + onClick = onClick, + onLongClick = { + scope.launch { + clipboardManager.setText(item.relay.url) + } + }, + ) + } else { + Modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = { + scope.launch { + clipboardManager.setText(item.relay.url) + } + }, + ) + } + + Column(itemModifier) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier, ) { - if (dragModifier != Modifier) { + if (dragState != null && index >= 0) { Icon( Icons.Default.DragIndicator, contentDescription = stringRes(R.string.relay_reorder), modifier = - dragModifier - .size(24.dp), + Modifier + .size(24.dp) + .relayDragHandle(index, dragState), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 2ecc90a6d..07127a721 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -34,7 +33,8 @@ fun BasicRelaySetupInfoDialog( nip11CachedRetriever: Nip11CachedRetriever, onDelete: ((BasicRelaySetupInfo) -> Unit)?, countResult: RelayCountResult? = null, - dragModifier: Modifier = Modifier, + index: Int = -1, + dragState: RelayDragState? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -46,7 +46,8 @@ fun BasicRelaySetupInfoDialog( onClick = { nav.nav(Route.RelayInfo(item.relay.url)) }, nip11CachedRetriever = nip11CachedRetriever, modifier = HalfVertPadding, - dragModifier = dragModifier, + index = index, + dragState = dragState, countResult = countResult, accountViewModel = accountViewModel, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/DraggableRelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/DraggableRelayList.kt index ea2d47b20..7c107a0d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/DraggableRelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/DraggableRelayList.kt @@ -22,10 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -38,106 +36,112 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.zIndex -@Composable -fun DraggableRelayList( - items: List, - onMove: (from: Int, to: Int) -> Unit, - modifier: Modifier = Modifier, - content: @Composable ( - index: Int, - item: BasicRelaySetupInfo, - dragModifier: Modifier, - ) -> Unit, +@Stable +class RelayDragState( + val onMove: (from: Int, to: Int) -> Unit, + val itemCount: () -> Int, ) { - var draggedItemIndex by remember { mutableIntStateOf(-1) } - var dragOffset by remember { mutableFloatStateOf(0f) } - val itemHeights = remember { mutableStateMapOf() } + var draggedItemIndex by mutableIntStateOf(-1) + var dragOffset by mutableFloatStateOf(0f) + val itemHeights = mutableStateMapOf() - Column(modifier = modifier.fillMaxWidth()) { - items.forEachIndexed { index, item -> - val isDragging = draggedItemIndex == index - val targetElevation = if (isDragging) 8f else 0f - val animatedElevation by animateFloatAsState( - targetValue = targetElevation, - label = "dragElevation", - ) + fun onDragStart(index: Int) { + draggedItemIndex = index + dragOffset = 0f + } - Box( - modifier = - Modifier - .zIndex(if (isDragging) 1f else 0f) - .onGloballyPositioned { coordinates -> - itemHeights[index] = coordinates.size.height.toFloat() - }.graphicsLayer { - translationY = if (isDragging) dragOffset else 0f - shadowElevation = animatedElevation - if (isDragging) { - scaleX = 1.02f - scaleY = 1.02f - } - }, - ) { - val dragModifier = - Modifier.pointerInput(Unit) { - detectDragGestures( - onDragStart = { - draggedItemIndex = index - dragOffset = 0f - }, - onDrag = { change, dragAmount -> - change.consume() - dragOffset += dragAmount.y + fun onDrag(dragAmount: Float) { + dragOffset += dragAmount - val currentIndex = draggedItemIndex - if (currentIndex < 0) return@detectDragGestures + val currentIndex = draggedItemIndex + if (currentIndex < 0) return - // Check if we should swap with the item above - if (dragOffset < 0 && currentIndex > 0) { - val aboveHeight = itemHeights[currentIndex - 1] ?: 0f - if (-dragOffset > aboveHeight / 2f) { - onMove(currentIndex, currentIndex - 1) + // Check if we should swap with the item above + if (dragOffset < 0 && currentIndex > 0) { + val aboveHeight = itemHeights[currentIndex - 1] ?: 0f + if (-dragOffset > aboveHeight / 2f) { + onMove(currentIndex, currentIndex - 1) - // Transfer heights - val h1 = itemHeights[currentIndex] - val h2 = itemHeights[currentIndex - 1] - if (h1 != null) itemHeights[currentIndex - 1] = h1 - if (h2 != null) itemHeights[currentIndex] = h2 + 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 - } - } + 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) { - onMove(currentIndex, currentIndex + 1) + // Check if we should swap with the item below + if (dragOffset > 0 && currentIndex < itemCount() - 1) { + val belowHeight = itemHeights[currentIndex + 1] ?: 0f + if (dragOffset > belowHeight / 2f) { + onMove(currentIndex, currentIndex + 1) - // Transfer heights - val h1 = itemHeights[currentIndex] - val h2 = itemHeights[currentIndex + 1] - if (h1 != null) itemHeights[currentIndex + 1] = h1 - if (h2 != null) itemHeights[currentIndex] = h2 + 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 - } - } - }, - onDragEnd = { - draggedItemIndex = -1 - dragOffset = 0f - }, - onDragCancel = { - draggedItemIndex = -1 - dragOffset = 0f - }, - ) - } - - content(index, item, dragModifier) + dragOffset -= belowHeight + draggedItemIndex = currentIndex + 1 } } } + + fun onDragEnd() { + draggedItemIndex = -1 + dragOffset = 0f + } } + +@Composable +fun rememberRelayDragState( + onMove: (from: Int, to: Int) -> Unit, + itemCount: () -> Int, +): RelayDragState = + remember(onMove, itemCount) { + RelayDragState(onMove, itemCount) + } + +@Composable +fun Modifier.draggableRelayItem( + index: Int, + dragState: RelayDragState, +): Modifier { + val isDragging = dragState.draggedItemIndex == index + val targetElevation = if (isDragging) 8f else 0f + val animatedElevation by animateFloatAsState( + targetValue = targetElevation, + label = "dragElevation", + ) + + return this + .zIndex(if (isDragging) 1f else 0f) + .onGloballyPositioned { coordinates -> + dragState.itemHeights[index] = coordinates.size.height.toFloat() + }.graphicsLayer { + translationY = if (isDragging) dragState.dragOffset else 0f + shadowElevation = animatedElevation + if (isDragging) { + scaleX = 1.02f + scaleY = 1.02f + } + } +} + +fun Modifier.relayDragHandle( + index: Int, + dragState: RelayDragState, +): Modifier = + this.pointerInput(index) { + detectDragGestures( + onDragStart = { dragState.onDragStart(index) }, + onDrag = { change, dragAmount -> + change.consume() + dragState.onDrag(dragAmount.y) + }, + onDragEnd = { dragState.onDragEnd() }, + onDragCancel = { dragState.onDragEnd() }, + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index b8306a2c5..6a79a8f35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +35,11 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -51,12 +53,17 @@ fun DMRelayList( ) { val newNav = rememberExtendedNav(nav, onClose) val feedState by postViewModel.relays.collectAsStateWithLifecycle() + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderDMItems(feedState, postViewModel, accountViewModel, newNav) + renderDMItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -67,22 +74,19 @@ fun LazyListScope.renderDMItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "DM_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "DM" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt index 62a785bc6..70656390c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,9 +35,10 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -48,14 +50,18 @@ fun RelayFeedsList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderRelayFeedsItems(feedState, postViewModel, accountViewModel, newNav) + renderRelayFeedsItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -65,21 +71,18 @@ fun LazyListScope.renderRelayFeedsItems( postViewModel: RelayFeedsListViewModel, accountViewModel: AccountViewModel, nav: INav, + dragState: RelayDragState? = null, ) { - item(key = "RelayFeeds_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "RelayFeeds" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt index ed728371a..b7746faab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +35,11 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -50,14 +52,18 @@ fun IndexerRelayList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderIndexerItems(feedState, postViewModel, accountViewModel, newNav) + renderIndexerItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -68,22 +74,19 @@ fun LazyListScope.renderIndexerItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "Indexer_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index db8c459e1..d7fea4dc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,9 +35,10 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -49,12 +51,17 @@ fun LocalRelayList( ) { val newNav = rememberExtendedNav(nav, onClose) val feedState by postViewModel.relays.collectAsStateWithLifecycle() + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderLocalItems(feedState, postViewModel, accountViewModel, newNav) + renderLocalItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -64,21 +71,18 @@ fun LazyListScope.renderLocalItems( postViewModel: LocalRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + dragState: RelayDragState? = null, ) { - item(key = "Local_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Local" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index 152cbf371..25eb4d486 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +35,11 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -51,12 +53,17 @@ fun PrivateOutboxRelayList( ) { val newNav = rememberExtendedNav(nav, onClose) val feedState by postViewModel.relays.collectAsStateWithLifecycle() + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderPrivateOutboxItems(feedState, postViewModel, accountViewModel, newNav) + renderPrivateOutboxItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -67,22 +74,19 @@ fun LazyListScope.renderPrivateOutboxItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "Outbox_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Outbox" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index efa1a3ce6..1ce299fa5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +35,11 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -54,12 +56,23 @@ fun Nip65RelayList( val homeFeedState by postViewModel.homeRelays.collectAsStateWithLifecycle() val notifFeedState by postViewModel.notificationRelays.collectAsStateWithLifecycle() + val homeDragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveHomeRelay(from, to) }, + itemCount = { homeFeedState.size }, + ) + val notifDragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveNotifRelay(from, to) }, + itemCount = { notifFeedState.size }, + ) + Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderNip65HomeItems(homeFeedState, postViewModel, accountViewModel, newNav) - renderNip65NotifItems(notifFeedState, postViewModel, accountViewModel, newNav) + renderNip65HomeItems(homeFeedState, postViewModel, accountViewModel, newNav, dragState = homeDragState) + renderNip65NotifItems(notifFeedState, postViewModel, accountViewModel, newNav, dragState = notifDragState) } } } @@ -72,14 +85,18 @@ fun Nip65InboxRelayList( nav: INav, ) { val newNav = rememberExtendedNav(nav, onClose) - val notifFeedState by postViewModel.notificationRelays.collectAsStateWithLifecycle() + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveNotifRelay(from, to) }, + itemCount = { notifFeedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderNip65NotifItems(notifFeedState, postViewModel, accountViewModel, newNav) + renderNip65NotifItems(notifFeedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -92,14 +109,18 @@ fun Nip65OutboxRelayList( nav: INav, ) { val newNav = rememberExtendedNav(nav, onClose) - val homeFeedState by postViewModel.homeRelays.collectAsStateWithLifecycle() + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveHomeRelay(from, to) }, + itemCount = { homeFeedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderNip65HomeItems(homeFeedState, postViewModel, accountViewModel, newNav) + renderNip65HomeItems(homeFeedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -110,22 +131,19 @@ fun LazyListScope.renderNip65HomeItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "Nip65Home_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveHomeRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteHomeRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteHomeRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { @@ -144,22 +162,19 @@ fun LazyListScope.renderNip65NotifItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "Nip65Notif_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveNotifRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteNotifRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteNotifRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt index b3e2cbe60..77a9e5f6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +35,11 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -50,14 +52,18 @@ fun ProxyRelayList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderProxyItems(feedState, postViewModel, accountViewModel, newNav) + renderProxyItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -68,22 +74,19 @@ fun LazyListScope.renderProxyItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "Proxy_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Proxy" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index aa01622f3..dbead82fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,10 +35,11 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -50,14 +52,18 @@ fun SearchRelayList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderSearchItems(feedState, postViewModel, accountViewModel, newNav) + renderSearchItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -68,22 +74,19 @@ fun LazyListScope.renderSearchItems( accountViewModel: AccountViewModel, nav: INav, countResults: Map = emptyMap(), + dragState: RelayDragState? = null, ) { - item(key = "Search_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - countResult = countResults[item.relay], - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Search" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt index e1ec3bad4..4146ba4ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -34,9 +35,10 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.DraggableRelayList +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -48,14 +50,18 @@ fun TrustedRelayList( nav: INav, ) { val feedState by postViewModel.relays.collectAsStateWithLifecycle() - val newNav = rememberExtendedNav(nav, onClose) + val dragState = + rememberRelayDragState( + onMove = { from, to -> postViewModel.moveRelay(from, to) }, + itemCount = { feedState.size }, + ) Row(verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = FeedPadding, ) { - renderTrustedItems(feedState, postViewModel, accountViewModel, newNav) + renderTrustedItems(feedState, postViewModel, accountViewModel, newNav, dragState = dragState) } } } @@ -65,21 +71,18 @@ fun LazyListScope.renderTrustedItems( postViewModel: TrustedRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + dragState: RelayDragState? = null, ) { - item(key = "Trusted_draggable_list") { - DraggableRelayList( - items = feedState, - onMove = { from, to -> postViewModel.moveRelay(from, to) }, - ) { _, item, dragModifier -> - BasicRelaySetupInfoDialog( - item, - onDelete = { postViewModel.deleteRelay(item) }, - nip11CachedRetriever = Amethyst.instance.nip11Cache, - dragModifier = dragModifier, - accountViewModel = accountViewModel, - nav = nav, - ) - } + itemsIndexed(feedState, key = { _, item -> "Trusted" + item.relay.url }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + index = index, + dragState = dragState, + accountViewModel = accountViewModel, + nav = nav, + ) } item {