feat: add support for publishing highlights

This commit is contained in:
codytseng 2025-12-18 21:53:07 +08:00
parent c4881e3435
commit 079a2f90ef
29 changed files with 578 additions and 171 deletions

View file

@ -22,7 +22,7 @@ export default function Highlight({ event, className }: { event: Event; classNam
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap space-y-4', className)}>
{comment && <Content event={createFakeEvent({ content: comment })} />}
{comment && <Content event={createFakeEvent({ content: comment, tags: event.tags })} />}
<div className="flex gap-4">
<div className="w-1 flex-shrink-0 my-1 bg-primary/60 rounded-md" />
<div className="italic whitespace-pre-line">

View file

@ -1,10 +1,12 @@
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox'
import HighlightButton from '@/components/HighlightButton'
import PostEditor from '@/components/PostEditor'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList, toProfile } from '@/lib/link'
import { ExternalLink } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import { useMemo, useRef, useState } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import NostrNode from './NostrNode'
@ -20,6 +22,14 @@ export default function LongFormArticle({
}) {
const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const contentRef = useRef<HTMLDivElement>(null)
const [showHighlightEditor, setShowHighlightEditor] = useState(false)
const [selectedText, setSelectedText] = useState('')
const handleHighlight = (text: string) => {
setSelectedText(text)
setShowHighlightEditor(true)
}
const components = useMemo(
() =>
@ -74,54 +84,64 @@ export default function LongFormArticle({
/>
)
}) as Components,
[]
[event.pubkey]
)
return (
<div
className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
>
<h1 className="break-words">{metadata.title}</h1>
{metadata.summary && (
<blockquote>
<p className="break-words">{metadata.summary}</p>
</blockquote>
)}
{metadata.image && (
<ImageWithLightbox
image={{ url: metadata.image, pubkey: event.pubkey }}
className="w-full aspect-[3/1] object-cover my-0"
/>
)}
<Markdown
remarkPlugins={[remarkGfm, remarkNostr]}
urlTransform={(url) => {
if (url.startsWith('nostr:')) {
return url.slice(6) // Remove 'nostr:' prefix for rendering
}
return url
}}
components={components}
<>
<div
ref={contentRef}
className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
>
{event.content}
</Markdown>
{metadata.tags.length > 0 && (
<div className="flex gap-2 flex-wrap pb-2">
{metadata.tags.map((tag) => (
<div
key={tag}
title={tag}
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
onClick={(e) => {
e.stopPropagation()
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
}}
>
#<span className="truncate">{tag}</span>
</div>
))}
</div>
)}
</div>
<h1 className="break-words">{metadata.title}</h1>
{metadata.summary && (
<blockquote>
<p className="break-words">{metadata.summary}</p>
</blockquote>
)}
{metadata.image && (
<ImageWithLightbox
image={{ url: metadata.image, pubkey: event.pubkey }}
className="w-full aspect-[3/1] object-cover my-0"
/>
)}
<Markdown
remarkPlugins={[remarkGfm, remarkNostr]}
urlTransform={(url) => {
if (url.startsWith('nostr:')) {
return url.slice(6) // Remove 'nostr:' prefix for rendering
}
return url
}}
components={components}
>
{event.content}
</Markdown>
{metadata.tags.length > 0 && (
<div className="flex gap-2 flex-wrap pb-2">
{metadata.tags.map((tag) => (
<div
key={tag}
title={tag}
className="flex items-center rounded-full px-3 bg-muted text-muted-foreground max-w-44 cursor-pointer hover:bg-accent hover:text-accent-foreground"
onClick={(e) => {
e.stopPropagation()
push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
}}
>
#<span className="truncate">{tag}</span>
</div>
))}
</div>
)}
</div>
<HighlightButton onHighlight={handleHighlight} containerRef={contentRef} />
<PostEditor
highlightedText={selectedText}
parentStuff={event}
open={showHighlightEditor}
setOpen={setShowHighlightEditor}
/>
</>
)
}

View file

@ -117,7 +117,7 @@ export default function Note({
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
content = <FollowPack className="mt-2" event={event} />
} else {
content = <Content className="mt-2" event={event} />
content = <Content className="mt-2" event={event} enableHighlight />
}
return (