feat: support dnd to reorder relay sets
This commit is contained in:
17
src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
Normal file
17
src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-2">
|
||||
<div className="text-muted-foreground font-semibold select-none">{t('Relays')}</div>
|
||||
{favoriteRelays.map((relay) => (
|
||||
<RelayItem key={relay} relay={relay} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="w-full border rounded-lg pl-4 pr-2 py-2.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex justify-center items-center w-6 h-6 shrink-0">
|
||||
<FolderClosed className="size-4" />
|
||||
<div ref={setNodeRef} style={style} className="relative group">
|
||||
<div className="w-full border rounded-lg px-2 py-2.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="cursor-grab active:cursor-grabbing p-2 hover:bg-muted rounded touch-none"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVertical className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex justify-center items-center w-6 h-6 shrink-0">
|
||||
<FolderClosed className="size-4" />
|
||||
</div>
|
||||
<RelaySetName relaySet={relaySet} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<RelayUrlsExpandToggle relaySetId={relaySet.id}>
|
||||
{t('n relays', { n: relaySet.relayUrls.length })}
|
||||
</RelayUrlsExpandToggle>
|
||||
<RelaySetOptions relaySet={relaySet} />
|
||||
</div>
|
||||
<RelaySetName relaySet={relaySet} />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<RelayUrlsExpandToggle relaySetId={relaySet.id}>
|
||||
{t('n relays', { n: relaySet.relayUrls.length })}
|
||||
</RelayUrlsExpandToggle>
|
||||
<RelaySetOptions relaySet={relaySet} />
|
||||
</div>
|
||||
{expandedRelaySetId === relaySet.id && <RelayUrls relaySetId={relaySet.id} />}
|
||||
</div>
|
||||
{expandedRelaySetId === relaySet.id && <RelayUrls relaySetId={relaySet.id} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
72
src/components/FavoriteRelaysSetting/RelaySetList.tsx
Normal file
72
src/components/FavoriteRelaysSetting/RelaySetList.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-muted-foreground font-semibold select-none shrink-0">
|
||||
{t('Relay sets')}
|
||||
</div>
|
||||
<PullRelaySetsButton />
|
||||
</div>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
||||
>
|
||||
<SortableContext
|
||||
items={relaySets.map((set) => set.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
{relaySets.map((relaySet) => (
|
||||
<RelaySet key={relaySet.id} relaySet={relaySet} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<RelaySetsSettingComponentProvider>
|
||||
<div className="space-y-4">
|
||||
<TemporaryRelaySet />
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-muted-foreground font-semibold select-none shrink-0">
|
||||
{t('Relay sets')}
|
||||
</div>
|
||||
<PullRelaySetsButton />
|
||||
</div>
|
||||
{relaySets.map((relaySet) => (
|
||||
<RelaySet key={relaySet.id} relaySet={relaySet} />
|
||||
))}
|
||||
</div>
|
||||
<RelaySetList />
|
||||
<AddNewRelaySet />
|
||||
<div className="space-y-2">
|
||||
<div className="text-muted-foreground font-semibold select-none">{t('Relays')}</div>
|
||||
{favoriteRelays.map((relay) => (
|
||||
<RelayItem key={relay} relay={relay} />
|
||||
))}
|
||||
</div>
|
||||
<FavoriteRelayList />
|
||||
<AddNewRelay />
|
||||
</div>
|
||||
</RelaySetsSettingComponentProvider>
|
||||
|
||||
@@ -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<TRelaySet, 'aTag'>): 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])
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -21,6 +21,7 @@ type TFavoriteRelaysContext = {
|
||||
addRelaySets: (newRelaySetEvents: Event[]) => Promise<void>
|
||||
deleteRelaySet: (id: string) => Promise<void>
|
||||
updateRelaySet: (newSet: TRelaySet) => Promise<void>
|
||||
reorderRelaySets: (reorderedSets: TRelaySet[]) => Promise<void>
|
||||
}
|
||||
|
||||
const FavoriteRelaysContext = createContext<TFavoriteRelaysContext | undefined>(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 (
|
||||
<FavoriteRelaysContext.Provider
|
||||
value={{
|
||||
@@ -220,7 +239,8 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
|
||||
createRelaySet,
|
||||
addRelaySets,
|
||||
deleteRelaySet,
|
||||
updateRelaySet
|
||||
updateRelaySet,
|
||||
reorderRelaySets
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@@ -59,6 +59,7 @@ export type TWebMetadata = {
|
||||
|
||||
export type TRelaySet = {
|
||||
id: string
|
||||
aTag: string[]
|
||||
name: string
|
||||
relayUrls: string[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user