From 27b60b2a6fa4bfeca0880496a3e2fd79b2e8f2cc Mon Sep 17 00:00:00 2001 From: lemon Date: Mon, 27 Apr 2026 12:33:13 -0700 Subject: [PATCH] Refactor goal components: deduplicate GoalCard/GoalContent, fix staleness - Unify GoalCard and GoalContent into a single component with variant prop - Extract useGoalDisplay hook for shared display logic (author, progress, community link, deadline, image) - Add useNow(60s) interval so deadline labels refresh automatically - Add generic parseATagCoordinate utility to nostrEvents.ts - Replace DOM-mutating image onError with React state - Remove dead isGoalFunded export and redundant created_at in publish - Delete GoalContent.tsx (-144 net lines) --- src/components/CreateGoalDialog.tsx | 1 - src/components/GoalCard.tsx | 401 +++++++++++++++------------- src/components/GoalContent.tsx | 181 ------------- src/components/NoteCard.tsx | 4 +- src/hooks/useGoalDisplay.ts | 124 +++++++++ src/lib/goalUtils.ts | 20 +- src/lib/nostrEvents.ts | 18 ++ src/pages/PostDetailPage.tsx | 5 +- 8 files changed, 367 insertions(+), 387 deletions(-) delete mode 100644 src/components/GoalContent.tsx create mode 100644 src/hooks/useGoalDisplay.ts diff --git a/src/components/CreateGoalDialog.tsx b/src/components/CreateGoalDialog.tsx index 7eeb2739..1e379657 100644 --- a/src/components/CreateGoalDialog.tsx +++ b/src/components/CreateGoalDialog.tsx @@ -103,7 +103,6 @@ export function CreateGoalDialog({ communityATag, children, open: controlledOpen kind: ZAP_GOAL_KIND, content: title.trim(), tags, - created_at: Math.floor(Date.now() / 1000), }); // Refresh the fundraising tab and the community activity feed diff --git a/src/components/GoalCard.tsx b/src/components/GoalCard.tsx index 9098216b..3291ff8c 100644 --- a/src/components/GoalCard.tsx +++ b/src/components/GoalCard.tsx @@ -1,224 +1,253 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { Clock, Target, Users, Zap } from 'lucide-react'; import { nip19 } from 'nostr-tools'; -import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; +import type { NostrEvent } from '@nostrify/nostrify'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; -import { getAvatarShape } from '@/lib/avatarShape'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Skeleton } from '@/components/ui/skeleton'; import { ZapDialog } from '@/components/ZapDialog'; -import { useAddrEvent } from '@/hooks/useEvent'; -import { useAuthor } from '@/hooks/useAuthor'; -import { useGoalProgress } from '@/hooks/useGoalProgress'; +import { useGoalDisplay } from '@/hooks/useGoalDisplay'; import { useOpenPost } from '@/hooks/useOpenPost'; -import { useProfileUrl } from '@/hooks/useProfileUrl'; import { canZap } from '@/lib/canZap'; -import { formatSats, isGoalExpired, parseCommunityATag, type ParsedGoal } from '@/lib/goalUtils'; -import { genUserName } from '@/lib/genUserName'; -import { sanitizeUrl } from '@/lib/sanitizeUrl'; +import { formatSats, parseGoalEvent, type ParsedGoal } from '@/lib/goalUtils'; import { cn } from '@/lib/utils'; +// ── Public API ──────────────────────────────────────────────────────────────── + interface GoalCardProps { event: NostrEvent; - goal: ParsedGoal; + /** Pre-parsed goal. When omitted the component parses the event itself. */ + goal?: ParsedGoal; + /** + * `card` — standalone clickable card with border, stats row, and zap button + * (community fundraising tab). + * `compact` — inline content block for NoteCard / PostDetailPage. + */ + variant?: 'card' | 'compact'; } -export function GoalCard({ event, goal }: GoalCardProps) { - const expired = isGoalExpired(goal); - const { currentSats, percentage, contributors, zapCount, isLoading: progressLoading } = - useGoalProgress(event.id, goal.amountMsat, goal.closedAt); +/** + * Renders a NIP-75 zap goal. + * + * - `variant="card"` (default): standalone card used in the community + * fundraising tab. Includes a clickable wrapper, stats row, and Contribute button. + * - `variant="compact"`: inline renderer used inside NoteCard feeds and + * PostDetailPage. + */ +export function GoalCard({ event, goal: goalProp, variant = 'card' }: GoalCardProps) { + const goal = useMemo(() => goalProp ?? parseGoalEvent(event), [goalProp, event]); + if (!goal) return null; + return ; +} - const funded = percentage >= 100; +// ── Inner renderer (hooks are safe here) ────────────────────────────────────── - // Navigation to post detail +function GoalCardInner({ + event, + goal, + variant, +}: { + event: NostrEvent; + goal: ParsedGoal; + variant: 'card' | 'compact'; +}) { + const isCard = variant === 'card'; + const d = useGoalDisplay(event, goal); + const [imgError, setImgError] = useState(false); + + // Navigation (card variant only) const postPath = useMemo( () => `/${nip19.neventEncode({ id: event.id, author: event.pubkey })}`, [event.id, event.pubkey], ); const { onClick: openPost, onAuxClick: auxOpenPost } = useOpenPost(postPath); - // Recipient info - const author = useAuthor(goal.beneficiary); - const metadata: NostrMetadata | undefined = author.data?.metadata; - const displayName = metadata?.display_name || metadata?.name || genUserName(goal.beneficiary); - const avatarShape = getAvatarShape(metadata); - const profileUrl = useProfileUrl(goal.beneficiary, metadata); - const lightningAddress = metadata?.lud16 || metadata?.lud06 || undefined; + // ── Shared sub-sections ─────────────────────────────────────────────────── - // Deadline display - const deadlineLabel = useMemo(() => { - if (!goal.closedAt) return null; - const now = Math.floor(Date.now() / 1000); - const diff = goal.closedAt - now; - if (diff <= 0) return 'Ended'; - const days = Math.floor(diff / 86400); - const hours = Math.floor((diff % 86400) / 3600); - if (days > 0) return `${days}d ${hours}h left`; - const mins = Math.floor((diff % 3600) / 60); - return hours > 0 ? `${hours}h ${mins}m left` : `${mins}m left`; - }, [goal.closedAt]); + const imageSection = d.image && !imgError && ( +
+ {goal.title} setImgError(true)} + /> +
+ ); - const image = goal.image ? sanitizeUrl(goal.image) : undefined; - - // Community link - const communityAddr = useMemo(() => goal.communityATag ? parseCommunityATag(goal.communityATag) : undefined, [goal.communityATag]); - const { data: communityEvent } = useAddrEvent(communityAddr); - const communityName = communityEvent?.tags.find(([n]) => n === 'name')?.[1] - || communityEvent?.tags.find(([n]) => n === 'd')?.[1]; - const communityUrl = useMemo(() => { - if (!communityAddr) return undefined; - try { - return `/${nip19.naddrEncode({ kind: communityAddr.kind, pubkey: communityAddr.pubkey, identifier: communityAddr.identifier })}`; - } catch { - return undefined; - } - }, [communityAddr]); - - return ( -
- {/* Goal image */} - {image && ( -
- {goal.title} { (e.currentTarget.parentElement as HTMLElement).style.display = 'none'; }} - /> -
- )} - -
- {/* Header: title + community link / status badge */} -
-
- -

{goal.title}

-
-
- {communityUrl && communityName && ( - e.stopPropagation()} - > - - {communityName} - - )} - {funded ? ( - - Funded - - ) : expired ? ( - Ended - ) : null} -
-
- - {/* Summary */} - {goal.summary && ( -

{goal.summary}

- )} - - {/* Progress bar */} -
- div]:bg-emerald-500')} - /> -
- - {progressLoading ? ( - - ) : ( - <>{formatSats(currentSats)} sats - )} - - - of {formatSats(goal.amountSats)} sats ({percentage}%) - -
-
- - {/* Stats row */} -
- {zapCount > 0 && ( - - - {zapCount} zap{zapCount !== 1 ? 's' : ''} - - )} - {contributors.length > 0 && ( - - - {contributors.length} contributor{contributors.length !== 1 ? 's' : ''} - - )} - {deadlineLabel && ( - - - {deadlineLabel} - - )} -
- - {/* Recipient info — who is receiving the zaps */} -
- e.stopPropagation()}> - - - - {displayName.charAt(0).toUpperCase()} - - + const headerSection = ( +
+
+ +

+ {goal.title} +

+
+
+ {d.communityUrl && d.communityName && ( + e.stopPropagation()} + > + + {d.communityName} -
-

Receiving zaps

- e.stopPropagation()} - > - {displayName} - - {lightningAddress && ( -

- - {lightningAddress} -

- )} -
-
+ )} + {d.funded ? ( + + Funded + + ) : d.expired ? ( + Ended + ) : !isCard && d.deadlineLabel ? ( + + + {d.deadlineLabel} + + ) : null} +
+
+ ); - {/* Zap button */} - {!expired && canZap(metadata) && ( -
e.stopPropagation()}> - - - -
+ const summarySection = goal.summary && ( +

+ {goal.summary} +

+ ); + + const progressSection = ( +
+ div]:bg-emerald-500')} + /> +
+ + {d.progressLoading ? ( + + ) : ( + <>{formatSats(d.currentSats)} sats + )} + + + of {formatSats(goal.amountSats)} sats ({d.percentage}%) + +
+
+ ); + + const recipientSection = ( +
+ e.stopPropagation()}> + + + + {d.displayName.charAt(0).toUpperCase()} + + + +
+

Receiving zaps

+ e.stopPropagation()} + > + {d.displayName} + + {d.lightningAddress && ( +

+ + {d.lightningAddress} +

)}
); + + // ── Card variant ────────────────────────────────────────────────────────── + + if (isCard) { + return ( +
+ {imageSection} + +
+ {headerSection} + {summarySection} + {progressSection} + + {/* Stats row (card only) */} +
+ {d.zapCount > 0 && ( + + + {d.zapCount} zap{d.zapCount !== 1 ? 's' : ''} + + )} + {d.contributors.length > 0 && ( + + + {d.contributors.length} contributor{d.contributors.length !== 1 ? 's' : ''} + + )} + {d.deadlineLabel && ( + + + {d.deadlineLabel} + + )} +
+ + {recipientSection} + + {/* Zap button (card only) */} + {!d.expired && canZap(d.metadata) && ( +
e.stopPropagation()}> + + + +
+ )} +
+
+ ); + } + + // ── Compact variant ─────────────────────────────────────────────────────── + + return ( +
+ {imageSection} + {headerSection} + {summarySection} + {progressSection} + {recipientSection} +
+ ); } +// ── Skeleton ────────────────────────────────────────────────────────────────── + /** Skeleton placeholder for loading goal cards. */ export function GoalCardSkeleton() { return ( diff --git a/src/components/GoalContent.tsx b/src/components/GoalContent.tsx deleted file mode 100644 index 3bc7e86e..00000000 --- a/src/components/GoalContent.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { useMemo } from 'react'; -import { Link } from 'react-router-dom'; -import { Clock, Target, Users, Zap } from 'lucide-react'; -import { nip19 } from 'nostr-tools'; -import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; - -import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; -import { getAvatarShape } from '@/lib/avatarShape'; -import { Badge } from '@/components/ui/badge'; -import { Progress } from '@/components/ui/progress'; -import { Skeleton } from '@/components/ui/skeleton'; -import { useAddrEvent } from '@/hooks/useEvent'; -import { useAuthor } from '@/hooks/useAuthor'; -import { useGoalProgress } from '@/hooks/useGoalProgress'; -import { useProfileUrl } from '@/hooks/useProfileUrl'; -import { formatSats, isGoalExpired, parseGoalEvent, parseCommunityATag } from '@/lib/goalUtils'; -import { genUserName } from '@/lib/genUserName'; -import { sanitizeUrl } from '@/lib/sanitizeUrl'; -import { cn } from '@/lib/utils'; - -/** - * Compact goal renderer for NoteCard feed views. - * Shows goal title, progress bar, recipient profile + lightning address, and deadline. - */ -export function GoalContent({ event }: { event: NostrEvent }) { - const goal = useMemo(() => parseGoalEvent(event), [event]); - if (!goal) return null; - - return ; -} - -function GoalContentInner({ - event, - goal, -}: { - event: NostrEvent; - goal: NonNullable>; -}) { - const expired = isGoalExpired(goal); - const { currentSats, percentage, isLoading: progressLoading } = - useGoalProgress(event.id, goal.amountMsat, goal.closedAt); - const funded = percentage >= 100; - - // Recipient info - const author = useAuthor(goal.beneficiary); - const metadata: NostrMetadata | undefined = author.data?.metadata; - const displayName = metadata?.display_name || metadata?.name || genUserName(goal.beneficiary); - const avatarShape = getAvatarShape(metadata); - const profileUrl = useProfileUrl(goal.beneficiary, metadata); - const lightningAddress = metadata?.lud16 || metadata?.lud06 || undefined; - - // Deadline display - const deadlineLabel = useMemo(() => { - if (!goal.closedAt) return null; - const now = Math.floor(Date.now() / 1000); - const diff = goal.closedAt - now; - if (diff <= 0) return 'Ended'; - const days = Math.floor(diff / 86400); - const hours = Math.floor((diff % 86400) / 3600); - if (days > 0) return `${days}d ${hours}h left`; - const mins = Math.floor((diff % 3600) / 60); - return hours > 0 ? `${hours}h ${mins}m left` : `${mins}m left`; - }, [goal.closedAt]); - - const image = goal.image ? sanitizeUrl(goal.image) : undefined; - - // Community link - const communityAddr = useMemo(() => goal.communityATag ? parseCommunityATag(goal.communityATag) : undefined, [goal.communityATag]); - const { data: communityEvent } = useAddrEvent(communityAddr); - const communityName = communityEvent?.tags.find(([n]) => n === 'name')?.[1] - || communityEvent?.tags.find(([n]) => n === 'd')?.[1]; - const communityUrl = useMemo(() => { - if (!communityAddr) return undefined; - try { - return `/${nip19.naddrEncode({ kind: communityAddr.kind, pubkey: communityAddr.pubkey, identifier: communityAddr.identifier })}`; - } catch { - return undefined; - } - }, [communityAddr]); - - return ( -
- {/* Goal image */} - {image && ( -
- {goal.title} { (e.currentTarget.parentElement as HTMLElement).style.display = 'none'; }} - /> -
- )} - - {/* Title + community link / status */} -
-
- -

{goal.title}

-
-
- {communityUrl && communityName && ( - e.stopPropagation()} - > - - {communityName} - - )} - {funded ? ( - - Funded - - ) : expired ? ( - Ended - ) : deadlineLabel ? ( - - - {deadlineLabel} - - ) : null} -
-
- - {/* Summary */} - {goal.summary && ( -

{goal.summary}

- )} - - {/* Progress bar */} -
- div]:bg-emerald-500')} - /> -
- - {progressLoading ? ( - - ) : ( - <>{formatSats(currentSats)} sats - )} - - of {formatSats(goal.amountSats)} sats ({percentage}%) -
-
- - {/* Recipient — who is receiving the zaps */} -
- e.stopPropagation()}> - - - - {displayName.charAt(0).toUpperCase()} - - - -
-
-

Receiving zaps

-
- e.stopPropagation()} - > - {displayName} - - {lightningAddress && ( -

- - {lightningAddress} -

- )} -
-
-
- ); -} diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 1339fd61..1229f926 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -49,7 +49,7 @@ import { FollowPackContent } from "@/components/FollowPackContent"; import { FoundLogContent } from "@/components/FoundLogContent"; import { GeocacheContent } from "@/components/GeocacheContent"; import { GitRepoCard } from "@/components/GitRepoCard"; -import { GoalContent } from "@/components/GoalContent"; +import { GoalCard } from "@/components/GoalCard"; import { NsiteCard } from "@/components/NsiteCard"; import { ImageGallery } from "@/components/ImageGallery"; import { CardsIcon } from "@/components/icons/CardsIcon"; @@ -606,7 +606,7 @@ export const NoteCard = memo(function NoteCard({ ) : isCommunity ? ( ) : isZapGoal ? ( - + ) : isVoiceMessage ? ( ) : isCalendarEvent ? ( diff --git a/src/hooks/useGoalDisplay.ts b/src/hooks/useGoalDisplay.ts new file mode 100644 index 00000000..35ab2c98 --- /dev/null +++ b/src/hooks/useGoalDisplay.ts @@ -0,0 +1,124 @@ +import { useEffect, useMemo, useState } from 'react'; +import { nip19 } from 'nostr-tools'; +import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; + +import { getAvatarShape } from '@/lib/avatarShape'; +import { isGoalExpired, parseCommunityATag, type ParsedGoal } from '@/lib/goalUtils'; +import { genUserName } from '@/lib/genUserName'; +import { useAddrEvent } from '@/hooks/useEvent'; +import { useAuthor } from '@/hooks/useAuthor'; +import { useGoalProgress } from '@/hooks/useGoalProgress'; +import { useProfileUrl } from '@/hooks/useProfileUrl'; + +/** Re-renders every `intervalMs` so time-dependent values stay fresh. */ +function useNow(intervalMs: number): number { + const [now, setNow] = useState(() => Math.floor(Date.now() / 1000)); + useEffect(() => { + const id = setInterval(() => setNow(Math.floor(Date.now() / 1000)), intervalMs); + return () => clearInterval(id); + }, [intervalMs]); + return now; +} + +export interface GoalDisplayData { + // Status + expired: boolean; + funded: boolean; + + // Progress + currentSats: number; + percentage: number; + contributors: string[]; + zapCount: number; + progressLoading: boolean; + + // Recipient + metadata: NostrMetadata | undefined; + displayName: string; + avatarShape: string | undefined; + profileUrl: string; + lightningAddress: string | undefined; + + // Deadline + deadlineLabel: string | null; + + // Community link + communityName: string | undefined; + communityUrl: string | undefined; + + // Image (already sanitized at parse time) + image: string | undefined; +} + +/** + * Consolidates all display-related hooks and derived state for a goal event. + * Used by both the standalone GoalCard and the compact NoteCard/detail renderers. + */ +export function useGoalDisplay(event: NostrEvent, goal: ParsedGoal): GoalDisplayData { + const now = useNow(60_000); + const expired = isGoalExpired(goal); + const { currentSats, percentage, contributors, zapCount, isLoading: progressLoading } = + useGoalProgress(event.id, goal.amountMsat, goal.closedAt); + const funded = percentage >= 100; + + // Recipient info + const author = useAuthor(goal.beneficiary); + const metadata: NostrMetadata | undefined = author.data?.metadata; + const displayName = metadata?.display_name || metadata?.name || genUserName(goal.beneficiary); + const avatarShape = getAvatarShape(metadata); + const profileUrl = useProfileUrl(goal.beneficiary, metadata); + const lightningAddress = metadata?.lud16 || metadata?.lud06 || undefined; + + // Deadline label — `now` dependency ensures it refreshes every minute + const deadlineLabel = useMemo(() => { + if (!goal.closedAt) return null; + const diff = goal.closedAt - now; + if (diff <= 0) return 'Ended'; + const days = Math.floor(diff / 86400); + const hours = Math.floor((diff % 86400) / 3600); + if (days > 0) return `${days}d ${hours}h left`; + const mins = Math.floor((diff % 3600) / 60); + return hours > 0 ? `${hours}h ${mins}m left` : `${mins}m left`; + }, [goal.closedAt, now]); + + // Community link + const communityAddr = useMemo( + () => (goal.communityATag ? parseCommunityATag(goal.communityATag) : undefined), + [goal.communityATag], + ); + const { data: communityEvent } = useAddrEvent(communityAddr); + const communityName = + communityEvent?.tags.find(([n]) => n === 'name')?.[1] || + communityEvent?.tags.find(([n]) => n === 'd')?.[1]; + const communityUrl = useMemo(() => { + if (!communityAddr) return undefined; + try { + return `/${nip19.naddrEncode({ + kind: communityAddr.kind, + pubkey: communityAddr.pubkey, + identifier: communityAddr.identifier, + })}`; + } catch { + return undefined; + } + }, [communityAddr]); + + return { + expired, + funded, + currentSats, + percentage, + contributors, + zapCount, + progressLoading, + metadata, + displayName, + avatarShape, + profileUrl, + lightningAddress, + deadlineLabel, + communityName, + communityUrl, + image: goal.image, + }; +} diff --git a/src/lib/goalUtils.ts b/src/lib/goalUtils.ts index f9aafa30..46ecd202 100644 --- a/src/lib/goalUtils.ts +++ b/src/lib/goalUtils.ts @@ -1,5 +1,6 @@ import type { NostrEvent } from '@nostrify/nostrify'; +import { parseATagCoordinate } from '@/lib/nostrEvents'; import { sanitizeUrl } from '@/lib/sanitizeUrl'; // ── Kind constant ───────────────────────────────────────────────────────────── @@ -83,11 +84,6 @@ export function isGoalExpired(goal: ParsedGoal): boolean { return Math.floor(Date.now() / 1000) > goal.closedAt; } -/** Check whether a goal has been fully funded (current >= target). */ -export function isGoalFunded(currentMsat: number, goal: ParsedGoal): boolean { - return currentMsat >= goal.amountMsat; -} - /** Format satoshis with locale-aware separators. */ export function formatSats(sats: number): string { return sats.toLocaleString(); @@ -95,15 +91,11 @@ export function formatSats(sats: number): string { /** * Parse a community `a` tag coordinate (`34550::`) into its - * constituent parts. Returns `undefined` if the format is invalid. + * constituent parts. Returns `undefined` if the format is invalid or the kind + * is not 34550. */ export function parseCommunityATag(aTag: string): { kind: number; pubkey: string; identifier: string } | undefined { - const parts = aTag.split(':'); - if (parts.length < 3) return undefined; - const kind = parseInt(parts[0], 10); - if (kind !== 34550) return undefined; - const pubkey = parts[1]; - if (!pubkey) return undefined; - const identifier = parts.slice(2).join(':'); - return { kind, pubkey, identifier }; + const addr = parseATagCoordinate(aTag); + if (!addr || addr.kind !== 34550) return undefined; + return addr; } diff --git a/src/lib/nostrEvents.ts b/src/lib/nostrEvents.ts index 64604fc4..b3c97ec3 100644 --- a/src/lib/nostrEvents.ts +++ b/src/lib/nostrEvents.ts @@ -78,3 +78,21 @@ function getParentEventTag(event: NostrEvent): string[] | undefined { // Deprecated positional scheme: last non-mention e-tag is the reply target return eTags[eTags.length - 1]; } + +/** + * Parse a Nostr `a`-tag coordinate string (`kind:pubkey:identifier`) into its + * constituent parts. Returns `undefined` if the format is invalid. + * + * Handles d-tags that contain colons by joining everything after the second + * colon back together. + */ +export function parseATagCoordinate(aTag: string): { kind: number; pubkey: string; identifier: string } | undefined { + const parts = aTag.split(':'); + if (parts.length < 3) return undefined; + const kind = parseInt(parts[0], 10); + if (isNaN(kind) || kind < 0) return undefined; + const pubkey = parts[1]; + if (!pubkey) return undefined; + const identifier = parts.slice(2).join(':'); + return { kind, pubkey, identifier }; +} diff --git a/src/pages/PostDetailPage.tsx b/src/pages/PostDetailPage.tsx index 79cc0e09..c6222daa 100644 --- a/src/pages/PostDetailPage.tsx +++ b/src/pages/PostDetailPage.tsx @@ -39,7 +39,7 @@ import { const CustomNipCard = lazy(() => import("@/components/CustomNipCard").then(m => ({ default: m.CustomNipCard }))); import { FileMetadataContent } from "@/components/FileMetadataContent"; import { FollowPackContent } from "@/components/FollowPackContent"; -import { GoalContent } from "@/components/GoalContent"; +import { GoalCard } from "@/components/GoalCard"; import { FollowPackDetailContent } from "@/components/FollowPackDetailContent"; import { FoundLogContent } from "@/components/FoundLogContent"; import { GeocacheContent } from "@/components/GeocacheContent"; @@ -421,7 +421,6 @@ export function AddrPostDetailPage({ addr, relays }: AddrPostDetailPageProps) { ); } - /** NoteCard + NIP-22 comments section for kind 10008/30008 profile badges detail page. */ function ProfileBadgesDetailView({ event }: { event: NostrEvent }) { const { muteItems } = useMuteList(); @@ -2143,7 +2142,7 @@ function PostDetailContent({ event }: { event: NostrEvent }) { ) : isLetter ? ( ) : isZapGoal ? ( - + ) : isVine || isPoll || isGeocache ||