feat: improve support for long-form articles
This commit is contained in:
@@ -34,10 +34,10 @@ export default function ImageWithLightbox({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-fit max-w-full">
|
<div className="w-full">
|
||||||
<Image
|
<Image
|
||||||
key={0}
|
key={0}
|
||||||
className={cn('rounded-lg max-h-[80vh] sm:max-h-[50vh] border cursor-zoom-in', className)}
|
className={cn('rounded-lg border cursor-zoom-in', className)}
|
||||||
classNames={{
|
classNames={{
|
||||||
errorPlaceholder: 'aspect-square h-[30vh]'
|
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
||||||
import ImageWithLightbox from '@/components/ImageWithLightbox'
|
import ImageWithLightbox from '@/components/ImageWithLightbox'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
||||||
import { Event } from 'nostr-tools'
|
import { toNote, toNoteList, toProfile } from '@/lib/link'
|
||||||
|
import { ExternalLink } from 'lucide-react'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Markdown from 'react-markdown'
|
import Markdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
@@ -16,6 +18,7 @@ export default function LongFormArticle({
|
|||||||
event: Event
|
event: Event
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,15 +31,6 @@ export default function LongFormArticle({
|
|||||||
<p className="break-words">{metadata.summary}</p>
|
<p className="break-words">{metadata.summary}</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
)}
|
)}
|
||||||
{metadata.tags.length > 0 && (
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{metadata.tags.map((tag) => (
|
|
||||||
<Badge key={tag} variant="secondary" className="break-words">
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{metadata.image && (
|
{metadata.image && (
|
||||||
<ImageWithLightbox
|
<ImageWithLightbox
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||||
@@ -45,12 +39,55 @@ export default function LongFormArticle({
|
|||||||
)}
|
)}
|
||||||
<Markdown
|
<Markdown
|
||||||
remarkPlugins={[remarkGfm, remarkNostr]}
|
remarkPlugins={[remarkGfm, remarkNostr]}
|
||||||
|
urlTransform={(url) => {
|
||||||
|
if (url.startsWith('nostr:')) {
|
||||||
|
return url.slice(6) // Remove 'nostr:' prefix for rendering
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}}
|
||||||
components={
|
components={
|
||||||
{
|
{
|
||||||
nostr: (props) => <NostrNode {...props} />,
|
nostr: (props) => <NostrNode {...props} />,
|
||||||
a: (props) => (
|
a: ({ href, children, ...props }) => {
|
||||||
<a {...props} target="_blank" rel="noreferrer noopener" className="break-words" />
|
if (!href) {
|
||||||
),
|
return <span {...props} className="break-words" />
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
href.startsWith('note1') ||
|
||||||
|
href.startsWith('nevent1') ||
|
||||||
|
href.startsWith('naddr1')
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<SecondaryPageLink
|
||||||
|
to={toNote(href)}
|
||||||
|
className="break-words underline text-foreground"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SecondaryPageLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (href.startsWith('npub1') || href.startsWith('nprofile1')) {
|
||||||
|
return (
|
||||||
|
<SecondaryPageLink
|
||||||
|
to={toProfile(href)}
|
||||||
|
className="break-words underline text-foreground"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SecondaryPageLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
className="break-words inline-flex items-baseline gap-1"
|
||||||
|
>
|
||||||
|
{children} <ExternalLink className="size-3" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
},
|
||||||
p: (props) => <p {...props} className="break-words" />,
|
p: (props) => <p {...props} className="break-words" />,
|
||||||
div: (props) => <div {...props} className="break-words" />,
|
div: (props) => <div {...props} className="break-words" />,
|
||||||
code: (props) => <code {...props} className="break-words whitespace-pre-wrap" />
|
code: (props) => <code {...props} className="break-words whitespace-pre-wrap" />
|
||||||
@@ -59,6 +96,23 @@ export default function LongFormArticle({
|
|||||||
>
|
>
|
||||||
{event.content}
|
{event.content}
|
||||||
</Markdown>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
||||||
|
import { toNoteList } from '@/lib/link'
|
||||||
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export default function LongFormArticlePreview({
|
|||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||||
|
|
||||||
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
|
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
|
||||||
@@ -20,9 +22,16 @@ export default function LongFormArticlePreview({
|
|||||||
const tagsComponent = metadata.tags.length > 0 && (
|
const tagsComponent = metadata.tags.length > 0 && (
|
||||||
<div className="flex gap-1 flex-wrap">
|
<div className="flex gap-1 flex-wrap">
|
||||||
{metadata.tags.map((tag) => (
|
{metadata.tags.map((tag) => (
|
||||||
<Badge key={tag} variant="secondary">
|
<div
|
||||||
{tag}
|
key={tag}
|
||||||
</Badge>
|
className="flex items-center rounded-full text-xs px-2.5 py-0.5 bg-muted text-muted-foreground max-w-32 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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ const NoteList = forwardRef(
|
|||||||
subRequests.map(({ urls, filter }) => ({
|
subRequests.map(({ urls, filter }) => ({
|
||||||
urls,
|
urls,
|
||||||
filter: {
|
filter: {
|
||||||
...filter,
|
|
||||||
kinds: KINDS,
|
kinds: KINDS,
|
||||||
|
...filter,
|
||||||
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -11,16 +11,21 @@ export const toNoteList = ({
|
|||||||
hashtag,
|
hashtag,
|
||||||
search,
|
search,
|
||||||
externalContentId,
|
externalContentId,
|
||||||
domain
|
domain,
|
||||||
|
kinds
|
||||||
}: {
|
}: {
|
||||||
hashtag?: string
|
hashtag?: string
|
||||||
search?: string
|
search?: string
|
||||||
externalContentId?: string
|
externalContentId?: string
|
||||||
domain?: string
|
domain?: string
|
||||||
|
kinds?: number[]
|
||||||
}) => {
|
}) => {
|
||||||
const path = '/notes'
|
const path = '/notes'
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (hashtag) query.set('t', hashtag.toLowerCase())
|
if (hashtag) query.set('t', hashtag.toLowerCase())
|
||||||
|
if (kinds?.length) {
|
||||||
|
kinds.forEach((k) => query.append('k', k.toString()))
|
||||||
|
}
|
||||||
if (search) query.set('s', search)
|
if (search) query.set('s', search)
|
||||||
if (externalContentId) query.set('i', externalContentId)
|
if (externalContentId) query.set('i', externalContentId)
|
||||||
if (domain) query.set('d', domain)
|
if (domain) query.set('d', domain)
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
const [data, setData] = useState<
|
const [data, setData] = useState<
|
||||||
| {
|
| {
|
||||||
type: 'hashtag' | 'search' | 'externalContent'
|
type: 'hashtag' | 'search' | 'externalContent'
|
||||||
|
kinds?: number[]
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'domain'
|
type: 'domain'
|
||||||
domain: string
|
domain: string
|
||||||
|
kinds?: number[]
|
||||||
}
|
}
|
||||||
| null
|
| null
|
||||||
>(null)
|
>(null)
|
||||||
@@ -34,13 +36,17 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const searchParams = new URLSearchParams(window.location.search)
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
const kinds = searchParams
|
||||||
|
.getAll('k')
|
||||||
|
.map((k) => parseInt(k))
|
||||||
|
.filter((k) => !isNaN(k))
|
||||||
const hashtag = searchParams.get('t')
|
const hashtag = searchParams.get('t')
|
||||||
if (hashtag) {
|
if (hashtag) {
|
||||||
setData({ type: 'hashtag' })
|
setData({ type: 'hashtag' })
|
||||||
setTitle(`# ${hashtag}`)
|
setTitle(`# ${hashtag}`)
|
||||||
setSubRequests([
|
setSubRequests([
|
||||||
{
|
{
|
||||||
filter: { '#t': [hashtag] },
|
filter: { '#t': [hashtag], ...(kinds.length > 0 ? { kinds } : {}) },
|
||||||
urls: BIG_RELAY_URLS
|
urls: BIG_RELAY_URLS
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -52,7 +58,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
setTitle(`${t('Search')}: ${search}`)
|
setTitle(`${t('Search')}: ${search}`)
|
||||||
setSubRequests([
|
setSubRequests([
|
||||||
{
|
{
|
||||||
filter: { search },
|
filter: { search, ...(kinds.length > 0 ? { kinds } : {}) },
|
||||||
urls: SEARCHABLE_RELAY_URLS
|
urls: SEARCHABLE_RELAY_URLS
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@@ -64,7 +70,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
setTitle(externalContentId)
|
setTitle(externalContentId)
|
||||||
setSubRequests([
|
setSubRequests([
|
||||||
{
|
{
|
||||||
filter: { '#I': [externalContentId] },
|
filter: { '#I': [externalContentId], ...(kinds.length > 0 ? { kinds } : {}) },
|
||||||
urls: BIG_RELAY_URLS.concat(relayList?.write || [])
|
urls: BIG_RELAY_URLS.concat(relayList?.write || [])
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
2
src/types/index.d.ts
vendored
2
src/types/index.d.ts
vendored
@@ -5,7 +5,7 @@ export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: numbe
|
|||||||
|
|
||||||
export type TFeedSubRequest = {
|
export type TFeedSubRequest = {
|
||||||
urls: string[]
|
urls: string[]
|
||||||
filter: Omit<Filter, 'since' | 'until' | 'kinds'>
|
filter: Omit<Filter, 'since' | 'until'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TProfile = {
|
export type TProfile = {
|
||||||
|
|||||||
Reference in New Issue
Block a user