From 29213ceb1cca1abb80b5cb8a419ba90a912922e3 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 14 Oct 2025 11:01:02 +0200 Subject: [PATCH] feat(highlights): add citation attribution to highlight items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create HighlightCitation component to show source attribution - For nostr-native content: display as '— Author, Article Title' - For web URLs: display hostname as '— domain.com' - Automatically resolves article titles from event references - Resolves author names from profile data - Add styling for citation line below highlight text - Keep code DRY by reusing existing articleTitleResolver service --- src/components/HighlightCitation.tsx | 87 ++++++++++++++++++++++++++++ src/components/HighlightItem.tsx | 8 +++ src/styles/layout/highlights.css | 1 + 3 files changed, 96 insertions(+) create mode 100644 src/components/HighlightCitation.tsx diff --git a/src/components/HighlightCitation.tsx b/src/components/HighlightCitation.tsx new file mode 100644 index 00000000..86ca188b --- /dev/null +++ b/src/components/HighlightCitation.tsx @@ -0,0 +1,87 @@ +import React, { useState, useEffect } from 'react' +import { RelayPool } from 'applesauce-relay' +import { useEventModel } from 'applesauce-react/hooks' +import { Models } from 'applesauce-core' +import { nip19 } from 'nostr-tools' +import { fetchArticleTitle } from '../services/articleTitleResolver' + +interface HighlightCitationProps { + eventReference?: string + urlReference?: string + authorPubkey?: string + relayPool?: RelayPool | null +} + +export const HighlightCitation: React.FC = ({ + eventReference, + urlReference, + authorPubkey, + relayPool +}) => { + const [articleTitle, setArticleTitle] = useState() + const authorProfile = useEventModel(Models.ProfileModel, authorPubkey ? [authorPubkey] : null) + + useEffect(() => { + if (!eventReference || !relayPool) { + return + } + + const loadTitle = async () => { + try { + // Convert eventReference to naddr if needed + let naddr: string + if (eventReference.includes(':')) { + const parts = eventReference.split(':') + const kind = parseInt(parts[0]) + const pubkey = parts[1] + const identifier = parts[2] || '' + + naddr = nip19.naddrEncode({ + kind, + pubkey, + identifier + }) + } else { + naddr = eventReference + } + + const title = await fetchArticleTitle(relayPool, naddr) + if (title) { + setArticleTitle(title) + } + } catch (error) { + console.error('Failed to load article title:', error) + } + } + + loadTitle() + }, [eventReference, relayPool]) + + const authorName = authorProfile?.name || authorProfile?.display_name + + // For nostr-native content with article reference + if (eventReference && (authorName || articleTitle)) { + return ( +
+ — {authorName || 'Unknown'}{articleTitle ? `, ${articleTitle}` : ''} +
+ ) + } + + // For web URLs + if (urlReference) { + try { + const url = new URL(urlReference) + return ( +
+ — {url.hostname} +
+ ) + } catch { + return null + } + } + + return null +} + diff --git a/src/components/HighlightItem.tsx b/src/components/HighlightItem.tsx index b47ba003..88319c1b 100644 --- a/src/components/HighlightItem.tsx +++ b/src/components/HighlightItem.tsx @@ -15,6 +15,7 @@ import { createDeletionRequest } from '../services/deletionService' import ConfirmDialog from './ConfirmDialog' import { getNostrUrl } from '../config/nostrGateways' import CompactButton from './CompactButton' +import { HighlightCitation } from './HighlightCitation' interface HighlightWithLevel extends Highlight { level?: 'mine' | 'friends' | 'nostrverse' @@ -338,6 +339,13 @@ export const HighlightItem: React.FC = ({ {highlight.content} + + {highlight.comment && (
{highlight.comment} diff --git a/src/styles/layout/highlights.css b/src/styles/layout/highlights.css index 4a2f72b3..7a50aa37 100644 --- a/src/styles/layout/highlights.css +++ b/src/styles/layout/highlights.css @@ -128,6 +128,7 @@ .highlight-content { flex: 1; display: flex; flex-direction: column; gap: 0.5rem; padding: 2.25rem 0.75rem 2.5rem; } .highlight-text { margin: 0; padding: 0 0 0 1.25rem; font-style: italic; color: var(--color-text); line-height: 1.6; border-left: none; font-size: 0.95rem; } +.highlight-citation { margin-left: 1.25rem; font-size: 0.8rem; color: var(--color-text-secondary); font-style: normal; padding-top: 0.25rem; } .highlight-comment { margin-top: 0.5rem; margin-left: 1.25rem; padding: 0.75rem; border-left: 3px solid; border-radius: 4px; font-size: 0.875rem; color: var(--color-text); line-height: 1.5; } /* Level-colored comments */