feat: improve support for long-form articles
This commit is contained in:
@@ -34,10 +34,10 @@ export default function ImageWithLightbox({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-fit max-w-full">
|
||||
<div className="w-full">
|
||||
<Image
|
||||
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={{
|
||||
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
||||
import ImageWithLightbox from '@/components/ImageWithLightbox'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
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 Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
@@ -16,6 +18,7 @@ export default function LongFormArticle({
|
||||
event: Event
|
||||
className?: string
|
||||
}) {
|
||||
const { push } = useSecondaryPage()
|
||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||
|
||||
return (
|
||||
@@ -28,15 +31,6 @@ export default function LongFormArticle({
|
||||
<p className="break-words">{metadata.summary}</p>
|
||||
</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 && (
|
||||
<ImageWithLightbox
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
@@ -45,12 +39,55 @@ export default function LongFormArticle({
|
||||
)}
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm, remarkNostr]}
|
||||
urlTransform={(url) => {
|
||||
if (url.startsWith('nostr:')) {
|
||||
return url.slice(6) // Remove 'nostr:' prefix for rendering
|
||||
}
|
||||
return url
|
||||
}}
|
||||
components={
|
||||
{
|
||||
nostr: (props) => <NostrNode {...props} />,
|
||||
a: (props) => (
|
||||
<a {...props} target="_blank" rel="noreferrer noopener" className="break-words" />
|
||||
),
|
||||
a: ({ href, children, ...props }) => {
|
||||
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" />,
|
||||
div: (props) => <div {...props} className="break-words" />,
|
||||
code: (props) => <code {...props} className="break-words whitespace-pre-wrap" />
|
||||
@@ -59,6 +96,23 @@ export default function LongFormArticle({
|
||||
>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
||||
import { toNoteList } from '@/lib/link'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import Image from '../Image'
|
||||
|
||||
@@ -13,6 +14,7 @@ export default function LongFormArticlePreview({
|
||||
className?: string
|
||||
}) {
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { push } = useSecondaryPage()
|
||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||
|
||||
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 && (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{metadata.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
<div
|
||||
key={tag}
|
||||
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>
|
||||
)
|
||||
|
||||
@@ -119,8 +119,8 @@ const NoteList = forwardRef(
|
||||
subRequests.map(({ urls, filter }) => ({
|
||||
urls,
|
||||
filter: {
|
||||
...filter,
|
||||
kinds: KINDS,
|
||||
...filter,
|
||||
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
||||
}
|
||||
})),
|
||||
|
||||
@@ -11,16 +11,21 @@ export const toNoteList = ({
|
||||
hashtag,
|
||||
search,
|
||||
externalContentId,
|
||||
domain
|
||||
domain,
|
||||
kinds
|
||||
}: {
|
||||
hashtag?: string
|
||||
search?: string
|
||||
externalContentId?: string
|
||||
domain?: string
|
||||
kinds?: number[]
|
||||
}) => {
|
||||
const path = '/notes'
|
||||
const query = new URLSearchParams()
|
||||
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 (externalContentId) query.set('i', externalContentId)
|
||||
if (domain) query.set('d', domain)
|
||||
|
||||
@@ -22,10 +22,12 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const [data, setData] = useState<
|
||||
| {
|
||||
type: 'hashtag' | 'search' | 'externalContent'
|
||||
kinds?: number[]
|
||||
}
|
||||
| {
|
||||
type: 'domain'
|
||||
domain: string
|
||||
kinds?: number[]
|
||||
}
|
||||
| null
|
||||
>(null)
|
||||
@@ -34,13 +36,17 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
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')
|
||||
if (hashtag) {
|
||||
setData({ type: 'hashtag' })
|
||||
setTitle(`# ${hashtag}`)
|
||||
setSubRequests([
|
||||
{
|
||||
filter: { '#t': [hashtag] },
|
||||
filter: { '#t': [hashtag], ...(kinds.length > 0 ? { kinds } : {}) },
|
||||
urls: BIG_RELAY_URLS
|
||||
}
|
||||
])
|
||||
@@ -52,7 +58,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
setTitle(`${t('Search')}: ${search}`)
|
||||
setSubRequests([
|
||||
{
|
||||
filter: { search },
|
||||
filter: { search, ...(kinds.length > 0 ? { kinds } : {}) },
|
||||
urls: SEARCHABLE_RELAY_URLS
|
||||
}
|
||||
])
|
||||
@@ -64,7 +70,7 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
setTitle(externalContentId)
|
||||
setSubRequests([
|
||||
{
|
||||
filter: { '#I': [externalContentId] },
|
||||
filter: { '#I': [externalContentId], ...(kinds.length > 0 ? { kinds } : {}) },
|
||||
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 = {
|
||||
urls: string[]
|
||||
filter: Omit<Filter, 'since' | 'until' | 'kinds'>
|
||||
filter: Omit<Filter, 'since' | 'until'>
|
||||
}
|
||||
|
||||
export type TProfile = {
|
||||
|
||||
Reference in New Issue
Block a user