diff --git a/src/components/CommunityDetailPage.tsx b/src/components/CommunityDetailPage.tsx index e89d9d51..5d6894c5 100644 --- a/src/components/CommunityDetailPage.tsx +++ b/src/components/CommunityDetailPage.tsx @@ -176,7 +176,7 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) { } = useCommunityBookmarks(); const bookmarked = !!communityATag && isCommunityBookmarked(communityATag); const handleToggleBookmark = useCallback(() => { - if (!user || !communityATag) return; + if (!user || !communityATag || toggleCommunityBookmark.isPending) return; toggleCommunityBookmark.mutate({ aTag: communityATag }); }, [user, communityATag, toggleCommunityBookmark]); @@ -376,10 +376,12 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) { )} {user && communityATag && ( diff --git a/src/hooks/useCommunityBookmarks.ts b/src/hooks/useCommunityBookmarks.ts index c0d18a7e..07f3001e 100644 --- a/src/hooks/useCommunityBookmarks.ts +++ b/src/hooks/useCommunityBookmarks.ts @@ -5,11 +5,22 @@ import { useCurrentUser } from './useCurrentUser'; import { useNostrPublish } from './useNostrPublish'; import { fetchFreshEvent } from '@/lib/fetchFreshEvent'; import { COMMUNITY_DEFINITION_KIND } from '@/lib/communityUtils'; +import { parseATagCoordinate } from '@/lib/nostrEvents'; import { toast } from '@/hooks/useToast'; /** NIP-51 Communities list — kind 10004. */ export const COMMUNITIES_LIST_KIND = 10004; +const HEX_PUBKEY_RE = /^[0-9a-f]{64}$/i; + +/** Parse and validate a NIP-51 community list coordinate. */ +export function parseCommunityBookmarkATag(aTag: string): { pubkey: string; dTag: string } | undefined { + const coord = parseATagCoordinate(aTag); + if (!coord || coord.kind !== COMMUNITY_DEFINITION_KIND) return undefined; + if (!HEX_PUBKEY_RE.test(coord.pubkey) || !coord.identifier) return undefined; + return { pubkey: coord.pubkey, dTag: coord.identifier }; +} + /** * Hook to manage the user's NIP-51 Communities list (kind 10004). * @@ -42,7 +53,7 @@ export function useCommunityBookmarks() { // Extract bookmarked community a-tags (only `34550:` coordinates) const bookmarkedATags: string[] = (listQuery.data?.tags ?? []) .filter(([name, value]) => - name === 'a' && typeof value === 'string' && value.startsWith(`${COMMUNITY_DEFINITION_KIND}:`), + name === 'a' && typeof value === 'string' && !!parseCommunityBookmarkATag(value), ) .map(([, value]) => value); diff --git a/src/hooks/useMyCommunities.ts b/src/hooks/useMyCommunities.ts index 22430196..7ff047c2 100644 --- a/src/hooks/useMyCommunities.ts +++ b/src/hooks/useMyCommunities.ts @@ -3,7 +3,7 @@ import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import { useCurrentUser } from './useCurrentUser'; -import { COMMUNITIES_LIST_KIND } from './useCommunityBookmarks'; +import { COMMUNITIES_LIST_KIND, parseCommunityBookmarkATag } from './useCommunityBookmarks'; import { COMMUNITY_DEFINITION_KIND, BADGE_AWARD_KIND, @@ -108,44 +108,34 @@ export function useMyCommunities() { .filter(([n, v]) => n === 'a' && typeof v === 'string' - && v.startsWith(`${COMMUNITY_DEFINITION_KIND}:`), + && !!parseCommunityBookmarkATag(v), ) .map(([, v]) => v); // Group bookmarked coords by author pubkey: author -> Set const coordsByAuthor = new Map>(); for (const coord of bookmarkedCoords) { - // Format: "34550::" -- d-tag may itself contain ":" - const firstColon = coord.indexOf(':'); - const secondColon = coord.indexOf(':', firstColon + 1); - if (firstColon === -1 || secondColon === -1) continue; - const authorPubkey = coord.slice(firstColon + 1, secondColon); - const dTag = coord.slice(secondColon + 1); - if (!authorPubkey || !dTag) continue; - const existing = coordsByAuthor.get(authorPubkey); + const parsed = parseCommunityBookmarkATag(coord); + if (!parsed) continue; + const existing = coordsByAuthor.get(parsed.pubkey); if (existing) { - existing.add(dTag); + existing.add(parsed.dTag); } else { - coordsByAuthor.set(authorPubkey, new Set([dTag])); + coordsByAuthor.set(parsed.pubkey, new Set([parsed.dTag])); } } let bookmarkedCommunityEvents: NostrEvent[] = []; if (coordsByAuthor.size > 0) { - const bookmarkQueries = await Promise.all( - Array.from(coordsByAuthor.entries()).map(([authorPubkey, dTags]) => - nostr.query( - [{ - kinds: [COMMUNITY_DEFINITION_KIND], - authors: [authorPubkey], - '#d': [...dTags], - limit: dTags.size, - }], - { signal: combinedSignal }, - ), - ), + bookmarkedCommunityEvents = await nostr.query( + Array.from(coordsByAuthor.entries()).map(([authorPubkey, dTags]) => ({ + kinds: [COMMUNITY_DEFINITION_KIND], + authors: [authorPubkey], + '#d': [...dTags], + limit: dTags.size, + })), + { signal: combinedSignal }, ); - bookmarkedCommunityEvents = bookmarkQueries.flat(); } const bookmarkedATagSet = new Set(bookmarkedCoords);