import JsonViewDialog from '@/components/JsonViewDialog' import PersonalListBech32List from '@/components/PersonalListBech32List' import { RefreshButton } from '@/components/RefreshButton' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { bookmarkBech32IdsFromListEvent } from '@/lib/personal-list-refs' import { createBookmarkDraftEvent } from '@/lib/draft-event' import { useNostr } from '@/providers/NostrProvider' import { getLatestEvent } from '@/lib/event' import { buildAccountListRelayUrlsForMerge } from '@/lib/account-list-relay-urls' import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' import { normalizeUrl } from '@/lib/url' import { PROFILE_RELAY_URLS } from '@/constants' import { queryService } from '@/services/client.service' import dayjs from 'dayjs' import { Code, Eraser, MoreVertical } from 'lucide-react' import { kinds } from 'nostr-tools' import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import NotFoundPage from '../NotFoundPage' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' const BookmarkListPage = forwardRef( ({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { const { t } = useTranslation() const { registerPrimaryPanelRefresh } = usePrimaryNoteView() const { profile, pubkey, bookmarkListEvent, relayList, publish, updateBookmarkListEvent } = useNostr() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const [jsonOpen, setJsonOpen] = useState(false) const [jsonPayload, setJsonPayload] = useState(null) const [cleanConfirmOpen, setCleanConfirmOpen] = useState(false) const [cleaning, setCleaning] = useState(false) const bech32Ids = useMemo(() => bookmarkBech32IdsFromListEvent(bookmarkListEvent), [bookmarkListEvent]) const refreshFromRelays = useCallback(async () => { if (!pubkey) return const comprehensiveRelays = await buildAccountListRelayUrlsForMerge({ accountPubkey: pubkey, favoriteRelays: favoriteRelays ?? [], blockedRelays }) let latest = (await fetchLatestReplaceableListEvent(pubkey, kinds.BookmarkList, comprehensiveRelays)) ?? null if (!latest) { const urls = Array.from( new Set( [ ...PROFILE_RELAY_URLS.map((u) => normalizeUrl(u) || u), ...(relayList?.write ?? []).map((u) => normalizeUrl(u) || u) ].filter(Boolean) ) ).slice(0, 12) if (urls.length) { try { const events = await queryService.fetchEvents(urls, { kinds: [kinds.BookmarkList], authors: [pubkey], limit: 5 }) latest = getLatestEvent(events) ?? null } catch { /* ignore */ } } } if (latest) await updateBookmarkListEvent(latest) }, [pubkey, favoriteRelays, blockedRelays, relayList?.write, updateBookmarkListEvent]) const openJson = useCallback(() => { setJsonPayload({ bookmarkListEvent: bookmarkListEvent ?? null, derivedBech32Ids: bech32Ids, note: 'Bookmarks are `e` / `a` tags on your kind 10003 (NIP-51) bookmark list replaceable event.' }) setJsonOpen(true) }, [bookmarkListEvent, bech32Ids]) useEffect(() => { if (!hideTitlebar) { registerPrimaryPanelRefresh(null) return } registerPrimaryPanelRefresh(() => { void refreshFromRelays() }) return () => registerPrimaryPanelRefresh(null) }, [hideTitlebar, registerPrimaryPanelRefresh, refreshFromRelays]) const handleCleanList = useCallback(async () => { if (!pubkey || cleaning) return setCleaning(true) try { if (dayjs().unix() === bookmarkListEvent?.created_at) { await new Promise((resolve) => setTimeout(resolve, 1000)) } const comprehensiveRelays = await buildAccountListRelayUrlsForMerge({ accountPubkey: pubkey, favoriteRelays: favoriteRelays ?? [], blockedRelays }) const draft = createBookmarkDraftEvent([], '') const published = await publish(draft, { specifiedRelayUrls: comprehensiveRelays }) await updateBookmarkListEvent(published) await refreshFromRelays() toast.success(t('List cleaned')) } catch (e) { toast.error(t('Failed to clean list') + ': ' + (e instanceof Error ? e.message : String(e))) } finally { setCleaning(false) setCleanConfirmOpen(false) } }, [pubkey, cleaning, bookmarkListEvent?.created_at, favoriteRelays, blockedRelays, publish, updateBookmarkListEvent, refreshFromRelays, t]) if (!profile || !pubkey) { return } return ( void refreshFromRelays()} /> openJson()}> {t('View JSON')} setCleanConfirmOpen(true)} > {t('Clean list')} ) } displayScrollToTopButton > setJsonOpen(false)} /> {t('Clean this list?')} {t('Clean list confirm')} {t('Cancel')} { e.preventDefault() void handleCleanList() }} > {cleaning ? t('loading...') : t('Clean list')}
{bech32Ids.length === 0 ? (

{t('No entries in bookmark list')}

) : ( )}
) } ) BookmarkListPage.displayName = 'BookmarkListPage' export default BookmarkListPage