index.tsx raw
1 import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
2 import ImageWithLightbox from '@/components/ImageWithLightbox'
3 import HighlightButton from '@/components/HighlightButton'
4 import PostEditor from '@/components/PostEditor'
5 import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
6 import { toNote, toNoteList, toProfile } from '@/lib/link'
7 import { ExternalLink } from 'lucide-react'
8 import { Event, kinds } from 'nostr-tools'
9 import { useMemo, useRef, useState } from 'react'
10 import Markdown from 'react-markdown'
11 import remarkGfm from 'remark-gfm'
12 import NostrNode from './NostrNode'
13 import { remarkNostr } from './remarkNostr'
14 import { Components } from './types'
15
16 export default function LongFormArticle({
17 event,
18 className
19 }: {
20 event: Event
21 className?: string
22 }) {
23 const { push } = useSecondaryPage()
24 const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
25 const contentRef = useRef<HTMLDivElement>(null)
26 const [showHighlightEditor, setShowHighlightEditor] = useState(false)
27 const [selectedText, setSelectedText] = useState('')
28
29 const handleHighlight = (text: string) => {
30 setSelectedText(text)
31 setShowHighlightEditor(true)
32 }
33
34 const components = useMemo(
35 () =>
36 ({
37 nostr: ({ rawText, bech32Id }) => <NostrNode rawText={rawText} bech32Id={bech32Id} />,
38 a: ({ href, children, ...props }) => {
39 if (!href) {
40 return <span {...props} className="break-words" />
41 }
42 if (href.startsWith('note1') || href.startsWith('nevent1') || href.startsWith('naddr1')) {
43 return (
44 <SecondaryPageLink
45 to={toNote(href)}
46 className="break-words underline text-foreground"
47 >
48 {children}
49 </SecondaryPageLink>
50 )
51 }
52 if (href.startsWith('npub1') || href.startsWith('nprofile1')) {
53 return (
54 <SecondaryPageLink
55 to={toProfile(href)}
56 className="break-words underline text-foreground"
57 >
58 {children}
59 </SecondaryPageLink>
60 )
61 }
62 return (
63 <a
64 {...props}
65 href={href}
66 target="_blank"
67 rel="noreferrer noopener"
68 className="break-words inline-flex items-baseline gap-1"
69 >
70 {children} <ExternalLink className="size-3" />
71 </a>
72 )
73 },
74 p: (props) => <p {...props} className="break-words" />,
75 div: (props) => <div {...props} className="break-words" />,
76 code: (props) => <code {...props} className="break-words whitespace-pre-wrap" />,
77 img: (props) => (
78 <ImageWithLightbox
79 image={{ url: props.src || '', pubkey: event.pubkey }}
80 className="max-h-[80vh] sm:max-h-[50vh] object-contain my-0"
81 classNames={{
82 wrapper: 'w-fit max-w-full'
83 }}
84 />
85 )
86 }) as Components,
87 [event.pubkey]
88 )
89
90 return (
91 <>
92 <div
93 ref={contentRef}
94 className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
95 >
96 <h1 className="break-words">{metadata.title}</h1>
97 {metadata.summary && (
98 <blockquote>
99 <p className="break-words">{metadata.summary}</p>
100 </blockquote>
101 )}
102 {metadata.image && (
103 <ImageWithLightbox
104 image={{ url: metadata.image, pubkey: event.pubkey }}
105 className="w-full aspect-[3/1] object-cover my-0"
106 />
107 )}
108 <Markdown
109 remarkPlugins={[remarkGfm, remarkNostr]}
110 urlTransform={(url) => {
111 if (url.startsWith('nostr:')) {
112 return url.slice(6) // Remove 'nostr:' prefix for rendering
113 }
114 return url
115 }}
116 components={components}
117 >
118 {event.content}
119 </Markdown>
120 {metadata.tags.length > 0 && (
121 <div className="flex gap-2 flex-wrap pb-2">
122 {metadata.tags.map((tag) => (
123 <div
124 key={tag}
125 title={tag}
126 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"
127 onClick={(e) => {
128 e.stopPropagation()
129 push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
130 }}
131 >
132 #<span className="truncate">{tag}</span>
133 </div>
134 ))}
135 </div>
136 )}
137 </div>
138 <HighlightButton onHighlight={handleHighlight} containerRef={contentRef} />
139 <PostEditor
140 highlightedText={selectedText}
141 parentStuff={event}
142 open={showHighlightEditor}
143 setOpen={setShowHighlightEditor}
144 />
145 </>
146 )
147 }
148