feat: support dnd to reorder favorite relays

This commit is contained in:
codytseng
2025-08-17 18:33:52 +08:00
parent 9bdee807ee
commit 4e40662f22
4 changed files with 90 additions and 8 deletions

View File

@@ -1,17 +1,63 @@
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors
} from '@dnd-kit/core'
import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy
} from '@dnd-kit/sortable'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RelayItem from './RelayItem' import RelayItem from './RelayItem'
export default function FavoriteRelayList() { export default function FavoriteRelayList() {
const { t } = useTranslation() const { t } = useTranslation()
const { favoriteRelays } = useFavoriteRelays() const { favoriteRelays, reorderFavoriteRelays } = useFavoriteRelays()
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates
})
)
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (over && active.id !== over.id) {
const oldIndex = favoriteRelays.findIndex((relay) => relay === active.id)
const newIndex = favoriteRelays.findIndex((relay) => relay === over.id)
const reorderedRelays = arrayMove(favoriteRelays, oldIndex, newIndex)
reorderFavoriteRelays(reorderedRelays)
}
}
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-muted-foreground font-semibold select-none">{t('Relays')}</div> <div className="text-muted-foreground font-semibold select-none">{t('Relays')}</div>
{favoriteRelays.map((relay) => ( <DndContext
<RelayItem key={relay} relay={relay} /> sensors={sensors}
))} collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
>
<SortableContext items={favoriteRelays} strategy={verticalListSortingStrategy}>
<div className="grid gap-2">
{favoriteRelays.map((relay) => (
<RelayItem key={relay} relay={relay} />
))}
</div>
</SortableContext>
</DndContext>
</div> </div>
) )
} }

View File

@@ -1,18 +1,43 @@
import { toRelay } from '@/lib/link' import { toRelay } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { GripVertical } from 'lucide-react'
import RelayIcon from '../RelayIcon' import RelayIcon from '../RelayIcon'
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
export default function RelayItem({ relay }: { relay: string }) { export default function RelayItem({ relay }: { relay: string }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: relay
})
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1
}
return ( return (
<div <div
className="flex gap-2 border rounded-lg p-4 items-center clickable select-none" className="relative group clickable flex gap-2 border rounded-lg p-2 pr-2.5 items-center justify-between select-none"
ref={setNodeRef}
style={style}
onClick={() => push(toRelay(relay))} onClick={() => push(toRelay(relay))}
> >
<RelayIcon url={relay} /> <div className="flex items-center gap-1 flex-1">
<div className="flex-1 w-0 truncate font-semibold">{relay}</div> <div
className="cursor-grab active:cursor-grabbing p-2 hover:bg-muted rounded touch-none shrink-0"
{...attributes}
{...listeners}
>
<GripVertical className="size-4 text-muted-foreground" />
</div>
<div className="flex gap-2 items-center flex-1">
<RelayIcon url={relay} />
<div className="flex-1 w-0 truncate font-semibold">{relay}</div>
</div>
</div>
<SaveRelayDropdownMenu urls={[relay]} /> <SaveRelayDropdownMenu urls={[relay]} />
</div> </div>
) )

View File

@@ -88,7 +88,9 @@ export default function SaveRelayDropdownMenu({
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger> <DropdownMenuTrigger asChild className="px-2">
{trigger}
</DropdownMenuTrigger>
<DropdownMenuContent onClick={(e) => e.stopPropagation()}> <DropdownMenuContent onClick={(e) => e.stopPropagation()}>
<DropdownMenuLabel>{t('Save to')} ...</DropdownMenuLabel> <DropdownMenuLabel>{t('Save to')} ...</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />

View File

@@ -16,6 +16,7 @@ type TFavoriteRelaysContext = {
favoriteRelays: string[] favoriteRelays: string[]
addFavoriteRelays: (relayUrls: string[]) => Promise<void> addFavoriteRelays: (relayUrls: string[]) => Promise<void>
deleteFavoriteRelays: (relayUrls: string[]) => Promise<void> deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>
reorderFavoriteRelays: (reorderedRelays: string[]) => Promise<void>
relaySets: TRelaySet[] relaySets: TRelaySet[]
createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void> createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
addRelaySets: (newRelaySetEvents: Event[]) => Promise<void> addRelaySets: (newRelaySetEvents: Event[]) => Promise<void>
@@ -219,6 +220,13 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
}) })
} }
const reorderFavoriteRelays = async (reorderedRelays: string[]) => {
setFavoriteRelays(reorderedRelays)
const draftEvent = createFavoriteRelaysDraftEvent(reorderedRelays, relaySetEvents)
const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const reorderRelaySets = async (reorderedSets: TRelaySet[]) => { const reorderRelaySets = async (reorderedSets: TRelaySet[]) => {
setRelaySets(reorderedSets) setRelaySets(reorderedSets)
const draftEvent = createFavoriteRelaysDraftEvent( const draftEvent = createFavoriteRelaysDraftEvent(
@@ -235,6 +243,7 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
favoriteRelays, favoriteRelays,
addFavoriteRelays, addFavoriteRelays,
deleteFavoriteRelays, deleteFavoriteRelays,
reorderFavoriteRelays,
relaySets, relaySets,
createRelaySet, createRelaySet,
addRelaySets, addRelaySets,