diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 530b0109..f1c0f43f 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -586,35 +586,23 @@ export function NoteCard({ event, className, repostedBy, compact, threaded, thre onClick={handleCardClick} onAuxClick={handleAuxClick} > - {/* Repost header */} - {repostedBy && ( - - )} - - {/* Treasure header — " hid/found a treasure" */} - {isTreasure && ( - - )} - - {/* Deck header — " shared a deck" */} - {isMagicDeck && !repostedBy && ( - - )} - - {/* Stream header — " is streaming / streamed" */} - {isStream && ( - - )} - - {/* Theme header — " shared/updated a theme" */} - {isTheme && !repostedBy && ( - - )} - - {/* Emoji pack header — " shared an emoji pack" */} - {isEmojiPack && !repostedBy && ( - - )} + {/* Action header — repost takes priority, otherwise derived from event kind */} + {repostedBy ? ( + + ) : KIND_HEADER_MAP[event.kind] && (() => { + const cfg = KIND_HEADER_MAP[event.kind]; + const isLive = event.kind === 30311 && getTag(event.tags, 'status') === 'live'; + return ( + + ); + })()} {/* Header: avatar + name/handle stacked */}
@@ -1036,35 +1024,47 @@ function StreamContent({ event }: { event: NostrEvent }) { ); } -function RepostHeader({ pubkey }: { pubkey: string }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - const url = useProfileUrl(pubkey, author.data?.metadata); - - return ( -
-
- -
-
- {author.isLoading ? ( - - ) : ( - e.stopPropagation()} - > - {author.data?.event ? {name} : name} - - )} - reposted -
-
- ); +interface EventActionHeaderProps { + /** Pubkey of the person performing the action. */ + pubkey: string; + /** Lucide icon component shown to the left of the author name. */ + icon: React.ComponentType<{ className?: string }>; + /** Optional className for the icon (defaults to text-primary). */ + iconClassName?: string; + /** Verb phrase shown after the author name, e.g. "hid a" or "is streaming". */ + action: string; + /** Optional noun shown after the verb, linked to a page route, e.g. "treasure" → /treasures. */ + noun?: string; + /** Route to link the noun to, e.g. "/treasures". */ + nounRoute?: string; } -function StreamHeader({ pubkey, isLive }: { pubkey: string; isLive: boolean }) { +/** Static config for deriving the action header from an event's kind and tags. */ +interface KindHeaderConfig { + icon: React.ComponentType<{ className?: string }>; + iconClassName?: string; + /** Static action string, or a function that computes it from the event's tags. */ + action: string | ((tags: string[][]) => string); + noun?: string; + nounRoute?: string; +} + +const KIND_HEADER_MAP: Record = { + 37516: { icon: ChestIcon, action: 'hid a', noun: 'treasure', nounRoute: '/treasures' }, + 7516: { icon: ChestIcon, action: 'found a', noun: 'treasure', nounRoute: '/treasures' }, + 37381: { icon: CardsIcon, action: 'shared a', noun: 'deck', nounRoute: '/decks' }, + 36767: { icon: Palette, action: 'shared a', noun: 'theme', nounRoute: '/themes' }, + 16767: { icon: Palette, action: 'updated their', noun: 'theme', nounRoute: '/themes' }, + 30030: { icon: SmilePlus, action: 'shared an', noun: 'emoji pack', nounRoute: '/emoji-packs' }, + 30311: { + icon: Radio, + iconClassName: undefined, // computed dynamically below + action: (tags) => tags.find(([n]) => n === 'status')?.[1] === 'live' ? 'is streaming' : 'streamed', + }, +}; + +/** Generic action header: icon · [author name] [action] [linked noun] */ +function EventActionHeader({ pubkey, icon: Icon, iconClassName, action, noun, nounRoute }: EventActionHeaderProps) { const author = useAuthor(pubkey); const name = author.data?.metadata?.name || genUserName(pubkey); const url = useProfileUrl(pubkey, author.data?.metadata); @@ -1072,7 +1072,7 @@ function StreamHeader({ pubkey, isLive }: { pubkey: string; isLive: boolean }) { return (
- +
{author.isLoading ? ( @@ -1086,128 +1086,25 @@ function StreamHeader({ pubkey, isLive }: { pubkey: string; isLive: boolean }) { {author.data?.event ? {name} : name} )} - - {isLive ? 'is streaming' : 'streamed'} + + {action} + {noun && nounRoute && ( + <> + {' '} + e.stopPropagation()} + > + {noun} + + + )} + {noun && !nounRoute && <>{' '}{noun}}
); } -function DeckHeader({ pubkey }: { pubkey: string }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - const url = useProfileUrl(pubkey, author.data?.metadata); - - return ( -
-
- -
-
- {author.isLoading ? ( - - ) : ( - e.stopPropagation()} - > - {author.data?.event ? {name} : name} - - )} - shared a deck -
-
- ); -} - -function TreasureHeader({ pubkey, variant }: { pubkey: string; variant: 'hid' | 'found' }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - const url = useProfileUrl(pubkey, author.data?.metadata); - - return ( -
-
- -
-
- {author.isLoading ? ( - - ) : ( - e.stopPropagation()} - > - {author.data?.event ? {name} : name} - - )} - - {variant === 'hid' ? 'hid a treasure' : 'found a treasure'} - -
-
- ); -} - -function ThemeHeader({ pubkey, variant }: { pubkey: string; variant: 'shared' | 'updated' }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - const url = useProfileUrl(pubkey, author.data?.metadata); - - return ( -
-
- -
-
- {author.isLoading ? ( - - ) : ( - e.stopPropagation()} - > - {author.data?.event ? {name} : name} - - )} - - {variant === 'shared' ? 'shared a theme' : 'updated their theme'} - -
-
- ); -} - -function EmojiPackHeader({ pubkey }: { pubkey: string }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - const url = useProfileUrl(pubkey, author.data?.metadata); - - return ( -
-
- -
-
- {author.isLoading ? ( - - ) : ( - e.stopPropagation()} - > - {author.data?.event ? {name} : name} - - )} - shared an emoji pack -
-
- ); -} -