fix: reject empty-identifier addresses and surface bookmark load errors

Per review:
- parseAddress() now rejects an empty d-identifier as malformed
  (e.g. "30023:<pubkey>:") instead of producing a "#d: ['']" relay
  query and an unopenable bookmark.
- useMyBookmarkedArticles() filters out matched events with empty
  content, the same non-renderable criteria the Reader's own list
  uses, so a broken/blank article can't land in the Bookmarked view.
- BookmarksApp now distinguishes "the query failed" from "there are
  no bookmarks" — React Query leaves data undefined in both cases, so
  a relay/network failure no longer reads as an empty list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
2026-09-06 18:19:39 +02:00
parent 9dfc073f48
commit ff4aaa00c8
2 changed files with 35 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import type { NostrEvent } from '@nostrify/nostrify';
import { AppBody, AppLayout, AppSectionTitle, AppToolbar, EmptyState } from '@/components/os/AppChrome';
import { LoginRequired } from '@/components/nostr/LoginRequired';
import { NoteCard } from '@/components/nostr/NoteCard';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { useAuthor } from '@/hooks/useAuthor';
import { useBookmarkedNoteIds, useMyBookmarkedArticles } from '@/hooks/useBookmarks';
@@ -45,7 +46,12 @@ export default function BookmarksApp({ setTitle }: AppProps) {
}
const isLoading = (noteIds.length > 0 && notes.isLoading) || articles.isLoading;
const isEmpty = !isLoading && (notes.data?.length ?? 0) === 0 && (articles.data?.length ?? 0) === 0;
// React Query leaves `data` undefined on a failed query too, so an error
// must be checked before treating "no data" as "no bookmarks" — otherwise
// a relay/network failure reads as an empty list.
const isError = notes.isError || articles.isError;
const isEmpty =
!isLoading && !isError && (notes.data?.length ?? 0) === 0 && (articles.data?.length ?? 0) === 0;
return (
<AppLayout>
@@ -60,6 +66,23 @@ export default function BookmarksApp({ setTitle }: AppProps) {
<Skeleton key={index} className="h-16 w-full" />
))}
</div>
) : isError ? (
<EmptyState
title="Couldn't load your bookmarks"
hint="None of your relays responded. Check the Relays app or try again."
action={
<Button
size="sm"
variant="outline"
onClick={() => {
notes.refetch();
articles.refetch();
}}
>
Try again
</Button>
}
/>
) : isEmpty ? (
<EmptyState
title="No bookmarks yet"

View File

@@ -101,12 +101,16 @@ interface ParsedAddress {
identifier: string;
}
/** Parses a NIP-01 `kind:pubkey:d-identifier` address tag value, or null if malformed. */
/**
* Parses a NIP-01 `kind:pubkey:d-identifier` address tag value, or null if
* malformed — including an empty identifier, which would otherwise produce
* a `#d: ['']` relay query and a bookmark nothing can reliably resolve.
*/
function parseAddress(address: string): ParsedAddress | null {
const [kindPart, pubkey, ...rest] = address.split(':');
const kind = Number(kindPart);
const identifier = rest.join(':');
if (!Number.isInteger(kind) || !pubkey) return null;
if (!Number.isInteger(kind) || !pubkey || !identifier) return null;
return { kind, pubkey, identifier };
}
@@ -148,7 +152,11 @@ export function useMyBookmarkedArticles() {
{ signal: AbortSignal.any([signal, AbortSignal.timeout(6000)]) },
);
const wanted = new Set(addresses);
return events.filter((event) => wanted.has(`${event.kind}:${event.pubkey}:${tagValue(event, 'd')}`));
return events.filter(
(event) =>
wanted.has(`${event.kind}:${event.pubkey}:${tagValue(event, 'd')}`) &&
event.content.trim().length > 0,
);
},
staleTime: 60_000,
});