Browse Source

feat: support dnd to reorder relay sets

imwald
codytseng 7 months ago
parent
commit
9bdee807ee
  1. 17
      src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
  2. 25
      src/components/FavoriteRelaysSetting/RelaySet.tsx
  3. 72
      src/components/FavoriteRelaysSetting/RelaySetList.tsx
  4. 29
      src/components/FavoriteRelaysSetting/index.tsx
  5. 14
      src/lib/draft-event.ts
  6. 3
      src/lib/event-metadata.ts
  7. 24
      src/providers/FavoriteRelaysProvider.tsx
  8. 1
      src/types/index.d.ts

17
src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx

@ -0,0 +1,17 @@ @@ -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>
)
}

25
src/components/FavoriteRelaysSetting/RelaySet.tsx

@ -10,12 +10,15 @@ import { Input } from '@/components/ui/input' @@ -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,16 +31,35 @@ import { useRelaySetsSettingComponent } from './provider' @@ -28,16 +31,35 @@ 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 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 })}
@ -47,6 +69,7 @@ export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) { @@ -47,6 +69,7 @@ export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
</div>
{expandedRelaySetId === relaySet.id && <RelayUrls relaySetId={relaySet.id} />}
</div>
</div>
)
}

72
src/components/FavoriteRelaysSetting/RelaySetList.tsx

@ -0,0 +1,72 @@ @@ -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>
)
}

29
src/components/FavoriteRelaysSetting/index.tsx

@ -1,39 +1,18 @@ @@ -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>

14
src/lib/draft-event.ts

@ -135,7 +135,7 @@ export async function createShortTextNoteDraftEvent( @@ -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[][] = []): @@ -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) { @@ -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])

3
src/lib/event-metadata.ts

@ -1,6 +1,7 @@ @@ -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 { @@ -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) {

24
src/providers/FavoriteRelaysProvider.tsx

@ -21,6 +21,7 @@ type TFavoriteRelaysContext = { @@ -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 @@ -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 @@ -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 @@ -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

@ -59,6 +59,7 @@ export type TWebMetadata = { @@ -59,6 +59,7 @@ export type TWebMetadata = {
export type TRelaySet = {
id: string
aTag: string[]
name: string
relayUrls: string[]
}

Loading…
Cancel
Save