From f54ad9aa1e12c40baa31ab1a91faa285d99cfac6 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 11 Nov 2025 14:05:49 +0100 Subject: [PATCH] made kind 1 highlights more stylish --- src/components/Note/Highlight/index.tsx | 224 ++++++++++++++++++++++-- src/i18n/locales/en.ts | 1 + 2 files changed, 213 insertions(+), 12 deletions(-) diff --git a/src/components/Note/Highlight/index.tsx b/src/components/Note/Highlight/index.tsx index 68bd1f5..cdcdeb7 100644 --- a/src/components/Note/Highlight/index.tsx +++ b/src/components/Note/Highlight/index.tsx @@ -1,8 +1,17 @@ -import { Event } from 'nostr-tools' +import { Event, kinds } from 'nostr-tools' import { Highlighter } from 'lucide-react' import { nip19 } from 'nostr-tools' import logger from '@/lib/logger' import HighlightSourcePreview from '@/components/UniversalContent/HighlightSourcePreview' +import UserAvatar from '@/components/UserAvatar' +import Username from '@/components/Username' +import { useTranslation } from 'react-i18next' +import { useSmartNoteNavigation } from '@/PageManager' +import { toNote } from '@/lib/link' +import { useFetchEvent } from '@/hooks' +import { useEffect, useState, useMemo } from 'react' +import client from '@/services/client.service' +import { ExtendedKind } from '@/constants' /** * Check if a string is a URL or Nostr address @@ -40,6 +49,52 @@ function isUrlOrNostrAddress(value: string | undefined): boolean { return false } +/** + * Simple author card for highlights with Nostr sources (e-tags, r-tags) + * Shows just "A note from: [user badge]" instead of the full embedded note + * The word "note" is a hyperlink to the referenced event + */ +function HighlightAuthorCard({ + authorPubkey, + eventId, + onClick +}: { + authorPubkey: string + eventId?: string + onClick?: () => void +}) { + const { t } = useTranslation() + const { navigateToNote } = useSmartNoteNavigation() + + const handleNoteClick = (e: React.MouseEvent) => { + e.stopPropagation() + if (onClick) { + onClick() + } else if (eventId) { + navigateToNote(toNote(eventId)) + } + } + + return ( +
+ + A{' '} + + {' '}from: + + + +
+ ) +} + export default function Highlight({ event, className @@ -47,6 +102,11 @@ export default function Highlight({ event: Event className?: string }) { + // State for storing the referenced event's author + const [referencedEventAuthor, setReferencedEventAuthor] = useState(null) + const [sourceEventId, setSourceEventId] = useState(null) + const [sourceBech32, setSourceBech32] = useState(null) + try { // Extract the source (e-tag, a-tag, or r-tag) with improved priority handling @@ -86,6 +146,9 @@ export default function Highlight({ } // Process the selected source tag + // We'll fetch the referenced event to get the author pubkey + let tempSourceEventId: string | null = null // Event ID or bech32 for fetching the event + let tempSourceBech32: string | null = null // Bech32 ID for navigation if (sourceTag) { if (sourceTag[0] === 'e' && sourceTag[1]) { source = { @@ -93,26 +156,80 @@ export default function Highlight({ value: sourceTag[1], bech32: nip19.noteEncode(sourceTag[1]) } + tempSourceEventId = sourceTag[1] // Store event ID for fetching + tempSourceBech32 = nip19.noteEncode(sourceTag[1]) // Store bech32 for navigation } else if (sourceTag[0] === 'a' && sourceTag[1]) { const [kind, pubkey, identifier] = sourceTag[1].split(':') const relay = sourceTag[2] + const bech32 = nip19.naddrEncode({ + kind: parseInt(kind), + pubkey, + identifier: identifier || '', + relays: relay ? [relay] : [] + }) source = { type: 'addressable' as const, value: sourceTag[1], - bech32: nip19.naddrEncode({ - kind: parseInt(kind), - pubkey, - identifier: identifier || '', - relays: relay ? [relay] : [] - }) + bech32 } + tempSourceEventId = bech32 // Store bech32 for fetching the event + tempSourceBech32 = bech32 // Store bech32 for navigation } else if (sourceTag[0] === 'r') { // Check if the r-tag value is a URL or Nostr address if (sourceTag[1] && isUrlOrNostrAddress(sourceTag[1])) { - source = { - type: 'url' as const, - value: sourceTag[1], - bech32: sourceTag[1] + // Try to decode as Nostr address to extract author + try { + const decoded = nip19.decode(sourceTag[1]) + if (decoded.type === 'naddr') { + // For naddr, we have the pubkey directly + nostrSourceAuthor = decoded.data.pubkey + source = { + type: 'url' as const, + value: sourceTag[1], + bech32: sourceTag[1] + } + } else if (decoded.type === 'nevent') { + // For nevent, we can fetch the event to get the author + tempSourceEventId = sourceTag[1] // Store bech32 for fetching + tempSourceBech32 = sourceTag[1] // Store bech32 for navigation + source = { + type: 'url' as const, + value: sourceTag[1], + bech32: sourceTag[1] + } + } else if (decoded.type === 'note') { + // For note, we can fetch the event to get the author + tempSourceEventId = sourceTag[1] // Store bech32 for fetching + tempSourceBech32 = sourceTag[1] // Store bech32 for navigation + source = { + type: 'url' as const, + value: sourceTag[1], + bech32: sourceTag[1] + } + } else if (decoded.type === 'naddr') { + // For naddr, we have the pubkey directly, but also fetch the event for consistency + tempSourceEventId = sourceTag[1] // Store bech32 for fetching + tempSourceBech32 = sourceTag[1] // Store bech32 for navigation + source = { + type: 'url' as const, + value: sourceTag[1], + bech32: sourceTag[1] + } + } else { + // Other Nostr types or URL + source = { + type: 'url' as const, + value: sourceTag[1], + bech32: sourceTag[1] + } + } + } catch { + // Not a valid Nostr address, treat as regular URL + source = { + type: 'url' as const, + value: sourceTag[1], + bech32: sourceTag[1] + } } } else if (sourceTag[1]) { // It's plain text, store it as a quote source @@ -120,6 +237,76 @@ export default function Highlight({ } } } + + // Update state for fetching the referenced event + useEffect(() => { + if (tempSourceEventId) { + setSourceEventId(tempSourceEventId) + setSourceBech32(tempSourceBech32) + } + }, [tempSourceEventId, tempSourceBech32]) + + // Fetch the referenced event to get the author pubkey and check if it has a special card + const { event: referencedEvent } = useFetchEvent(sourceEventId || undefined) + + // Determine if the referenced event has a special card that should be used instead of simple author card + const hasSpecialCard = useMemo(() => { + // For r-tags that are regular URLs (http/https), they have OpenGraph cards - always use those + if (sourceTag && sourceTag[0] === 'r' && sourceTag[1]) { + if (sourceTag[1].startsWith('http://') || sourceTag[1].startsWith('https://')) { + return true // URLs have OpenGraph cards - use full preview + } + } + + if (!referencedEvent) { + // For a-tags, check the kind from the tag itself (before event is loaded) + if (sourceTag && sourceTag[0] === 'a' && sourceTag[1]) { + const [kindStr] = sourceTag[1].split(':') + const kind = parseInt(kindStr) + // Longform articles (30023) have their own preview card + if (kind === kinds.LongFormArticle) { + return true + } + } + return false // Don't know yet - wait for event to load + } + + // Events with special preview cards that should always use full preview + const specialCardKinds = [ + kinds.LongFormArticle, // 30023 - has LongFormArticlePreview + ExtendedKind.POLL, // Has PollPreview + ExtendedKind.DISCUSSION, // Has DiscussionNote + ExtendedKind.VIDEO, // Has VideoNotePreview + ExtendedKind.SHORT_VIDEO, // Has VideoNotePreview + ExtendedKind.PICTURE, // Has PictureNotePreview + ExtendedKind.PUBLICATION, // Has PublicationCard + ExtendedKind.WIKI_ARTICLE, // Has special card + ExtendedKind.WIKI_ARTICLE_MARKDOWN, // Has special card + ExtendedKind.VOICE, // Has special card + ExtendedKind.VOICE_COMMENT, // Has special card + ] + + return specialCardKinds.includes(referencedEvent.kind) + }, [referencedEvent, sourceTag]) + + // Update the author when we get the referenced event + useEffect(() => { + if (referencedEvent) { + setReferencedEventAuthor(referencedEvent.pubkey) + } + }, [referencedEvent]) + + // For a-tags, we can also extract the pubkey directly from the tag (for immediate display) + useEffect(() => { + if (sourceTag && sourceTag[0] === 'a' && sourceTag[1] && !referencedEventAuthor && !hasSpecialCard) { + const [kindStr, pubkey] = sourceTag[1].split(':') + const kind = parseInt(kindStr) + // Only set author for a-tags that don't have special cards + if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey) && kind !== kinds.LongFormArticle) { + setReferencedEventAuthor(pubkey) + } + } + }, [sourceTag, referencedEventAuthor, hasSpecialCard]) // Extract the context (the main quote/full text being highlighted from) const contextTag = event.tags.find(tag => tag[0] === 'context') @@ -186,7 +373,20 @@ export default function Highlight({ {/* Source preview card */} {source && (
- + {/* Only show simple author card if: + 1. We have the author pubkey + 2. The referenced event doesn't have a special card (like LongFormArticle preview) + 3. For r-tags: only if it's a Nostr address, not a regular URL (URLs have OpenGraph cards) + */} + {referencedEventAuthor && !hasSpecialCard ? ( + + ) : ( + // For sources with special cards, URLs with OpenGraph, or while loading, show full preview + + )}
)} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index cc772e3..291f1f6 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -374,6 +374,7 @@ export default { Posts: 'Posts', Articles: 'Articles', Highlights: 'Highlights', + 'A note from': 'A note from', Polls: 'Polls', 'Voice Posts': 'Voice Posts', 'Photo Posts': 'Photo Posts',