From 9bdee807ee6e9f9ef74eabbbc3cb48d0e72abfc9 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 17 Aug 2025 18:04:08 +0800 Subject: [PATCH] feat: support dnd to reorder relay sets --- .../FavoriteRelayList.tsx | 17 +++++ .../FavoriteRelaysSetting/RelaySet.tsx | 49 +++++++++---- .../FavoriteRelaysSetting/RelaySetList.tsx | 72 +++++++++++++++++++ .../FavoriteRelaysSetting/index.tsx | 29 ++------ src/lib/draft-event.ts | 14 ++-- src/lib/event-metadata.ts | 3 +- src/providers/FavoriteRelaysProvider.tsx | 24 ++++++- src/types/index.d.ts | 1 + 8 files changed, 163 insertions(+), 46 deletions(-) create mode 100644 src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx create mode 100644 src/components/FavoriteRelaysSetting/RelaySetList.tsx diff --git a/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx b/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx new file mode 100644 index 0000000..7ff3574 --- /dev/null +++ b/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx @@ -0,0 +1,17 @@ +import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' +import { useTranslation } from 'react-i18next' +import RelayItem from './RelayItem' + +export default function FavoriteRelayList() { + const { t } = useTranslation() + const { favoriteRelays } = useFavoriteRelays() + + return ( +
+
{t('Relays')}
+ {favoriteRelays.map((relay) => ( + + ))} +
+ ) +} diff --git a/src/components/FavoriteRelaysSetting/RelaySet.tsx b/src/components/FavoriteRelaysSetting/RelaySet.tsx index b066ba7..d52387d 100644 --- a/src/components/FavoriteRelaysSetting/RelaySet.tsx +++ b/src/components/FavoriteRelaysSetting/RelaySet.tsx @@ -10,12 +10,15 @@ import { Input } from '@/components/ui/input' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { TRelaySet } from '@/types' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { Check, ChevronDown, Edit, EllipsisVertical, FolderClosed, + GripVertical, Link, Trash2 } from 'lucide-react' @@ -28,24 +31,44 @@ import { useRelaySetsSettingComponent } from './provider' export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) { const { t } = useTranslation() const { expandedRelaySetId } = useRelaySetsSettingComponent() + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: relaySet.id + }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1 + } return ( -
-
-
-
- +
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+ + {t('n relays', { n: relaySet.relayUrls.length })} + +
- -
-
- - {t('n relays', { n: relaySet.relayUrls.length })} - -
+ {expandedRelaySetId === relaySet.id && }
- {expandedRelaySetId === relaySet.id && }
) } diff --git a/src/components/FavoriteRelaysSetting/RelaySetList.tsx b/src/components/FavoriteRelaysSetting/RelaySetList.tsx new file mode 100644 index 0000000..c581a39 --- /dev/null +++ b/src/components/FavoriteRelaysSetting/RelaySetList.tsx @@ -0,0 +1,72 @@ +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 PullRelaySetsButton from './PullRelaySetsButton' +import RelaySet from './RelaySet' + +export default function RelaySetList() { + const { t } = useTranslation() + const { relaySets, reorderRelaySets } = 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 = relaySets.findIndex((item) => item.id === active.id) + const newIndex = relaySets.findIndex((item) => item.id === over.id) + + const reorderedSets = arrayMove(relaySets, oldIndex, newIndex) + reorderRelaySets(reorderedSets) + } + } + + return ( +
+
+
+ {t('Relay sets')} +
+ +
+ + set.id)} + strategy={verticalListSortingStrategy} + > +
+ {relaySets.map((relaySet) => ( + + ))} +
+
+
+
+ ) +} diff --git a/src/components/FavoriteRelaysSetting/index.tsx b/src/components/FavoriteRelaysSetting/index.tsx index ffbc66b..0a9c202 100644 --- a/src/components/FavoriteRelaysSetting/index.tsx +++ b/src/components/FavoriteRelaysSetting/index.tsx @@ -1,39 +1,18 @@ -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import { useTranslation } from 'react-i18next' import AddNewRelay from './AddNewRelay' import AddNewRelaySet from './AddNewRelaySet' +import FavoriteRelayList from './FavoriteRelayList' import { RelaySetsSettingComponentProvider } from './provider' -import RelayItem from './RelayItem' -import RelaySet from './RelaySet' +import RelaySetList from './RelaySetList' import TemporaryRelaySet from './TemporaryRelaySet' -import PullRelaySetsButton from './PullRelaySetsButton' export default function FavoriteRelaysSetting() { - const { t } = useTranslation() - const { relaySets, favoriteRelays } = useFavoriteRelays() - return (
-
-
-
- {t('Relay sets')} -
- -
- {relaySets.map((relaySet) => ( - - ))} -
+ -
-
{t('Relays')}
- {favoriteRelays.map((relay) => ( - - ))} -
+
diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index fc5be39..977c859 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -135,7 +135,7 @@ export async function createShortTextNoteDraftEvent( } // https://github.com/nostr-protocol/nips/blob/master/51.md -export function createRelaySetDraftEvent(relaySet: TRelaySet): TDraftEvent { +export function createRelaySetDraftEvent(relaySet: Omit): TDraftEvent { return { kind: kinds.Relaysets, content: '', @@ -312,14 +312,18 @@ export function createProfileDraftEvent(content: string, tags: string[][] = []): export function createFavoriteRelaysDraftEvent( favoriteRelays: string[], - relaySetEvents: Event[] + relaySetEventsOrATags: Event[] | string[][] ): TDraftEvent { const tags: string[][] = [] favoriteRelays.forEach((url) => { tags.push(buildRelayTag(url)) }) - relaySetEvents.forEach((event) => { - tags.push(buildATag(event)) + relaySetEventsOrATags.forEach((eventOrATag) => { + if (Array.isArray(eventOrATag)) { + tags.push(eventOrATag) + } else { + tags.push(buildATag(eventOrATag)) + } }) return { kind: ExtendedKind.FAVORITE_RELAYS, @@ -579,7 +583,7 @@ function extractImagesFromContent(content: string) { return content.match(/https?:\/\/[^\s"']+\.(jpg|jpeg|png|gif|webp|heic)/gi) } -function buildATag(event: Event, upperCase: boolean = false) { +export function buildATag(event: Event, upperCase: boolean = false) { const coordinate = getReplaceableCoordinateFromEvent(event) const hint = client.getEventHint(event.id) return trimTagEnd([upperCase ? 'A' : 'a', coordinate, hint]) diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts index 5457cfd..442fe62 100644 --- a/src/lib/event-metadata.ts +++ b/src/lib/event-metadata.ts @@ -1,6 +1,7 @@ import { BIG_RELAY_URLS, POLL_TYPE } from '@/constants' import { TPollType, TRelayList, TRelaySet } from '@/types' import { Event, kinds } from 'nostr-tools' +import { buildATag } from './draft-event' import { getReplaceableEventIdentifier } from './event' import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning' import { formatPubkey, pubkeyToNpub } from './pubkey' @@ -91,7 +92,7 @@ export function getRelaySetFromEvent(event: Event): TRelaySet { name = id } - return { id, name, relayUrls } + return { id, name, relayUrls, aTag: buildATag(event) } } export function getZapInfoFromEvent(receiptEvent: Event) { diff --git a/src/providers/FavoriteRelaysProvider.tsx b/src/providers/FavoriteRelaysProvider.tsx index d381345..1fbaf73 100644 --- a/src/providers/FavoriteRelaysProvider.tsx +++ b/src/providers/FavoriteRelaysProvider.tsx @@ -21,6 +21,7 @@ type TFavoriteRelaysContext = { addRelaySets: (newRelaySetEvents: Event[]) => Promise deleteRelaySet: (id: string) => Promise updateRelaySet: (newSet: TRelaySet) => Promise + reorderRelaySets: (reorderedSets: TRelaySet[]) => Promise } const FavoriteRelaysContext = createContext(undefined) @@ -109,7 +110,15 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode relaySetEventMap.set(d, event) } }) - const uniqueNewRelaySetEvents = Array.from(relaySetEventMap.values()) + const uniqueNewRelaySetEvents = relaySetIds + .map((id, index) => { + const event = relaySetEventMap.get(id) + if (event) { + return event + } + return storedRelaySetEvents[index] || null + }) + .filter(Boolean) as Event[] setRelaySetEvents(uniqueNewRelaySetEvents) await Promise.all( uniqueNewRelaySetEvents.map((event) => { @@ -210,6 +219,16 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode }) } + const reorderRelaySets = async (reorderedSets: TRelaySet[]) => { + setRelaySets(reorderedSets) + const draftEvent = createFavoriteRelaysDraftEvent( + favoriteRelays, + reorderedSets.map((set) => set.aTag) + ) + const newFavoriteRelaysEvent = await publish(draftEvent) + updateFavoriteRelaysEvent(newFavoriteRelaysEvent) + } + return ( {children} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 5749826..0e93c29 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -59,6 +59,7 @@ export type TWebMetadata = { export type TRelaySet = { id: string + aTag: string[] name: string relayUrls: string[] }