diff --git a/src/components/ContentPreview/LongFormArticlePreview.tsx b/src/components/ContentPreview/LongFormArticlePreview.tsx
deleted file mode 100644
index c42cb965..00000000
--- a/src/components/ContentPreview/LongFormArticlePreview.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
-import { cn } from '@/lib/utils'
-import { Event } from 'nostr-tools'
-import { useMemo } from 'react'
-import { useTranslation } from 'react-i18next'
-
-export default function LongFormArticlePreview({
- event,
- className
-}: {
- event: Event
- className?: string
-}) {
- const { t } = useTranslation()
- const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
-
- return (
-
- [{t('Article')}] {metadata.title}
-
- )
-}
diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx
index 335e3e7f..05025070 100644
--- a/src/components/ContentPreview/index.tsx
+++ b/src/components/ContentPreview/index.tsx
@@ -22,7 +22,7 @@ import CommunityDefinitionPreview from './CommunityDefinitionPreview'
import GroupMetadataPreview from './GroupMetadataPreview'
import HighlightPreview from './HighlightPreview'
import LiveEventPreview from './LiveEventPreview'
-import LongFormArticlePreview from './LongFormArticlePreview'
+import LongFormCard from '../Note/LongFormCard'
import NormalContentPreview from './NormalContentPreview'
import PictureNotePreview from './PictureNotePreview'
import PollPreview from './PollPreview'
@@ -181,7 +181,7 @@ export default function ContentPreview({
}
if (event.kind === kinds.LongFormArticle) {
- return withKindRow()
+ return withKindRow()
}
if (isNip71StyleVideoKind(event.kind)) {
diff --git a/src/components/Note/LongFormCard.tsx b/src/components/Note/LongFormCard.tsx
new file mode 100644
index 00000000..67a9c566
--- /dev/null
+++ b/src/components/Note/LongFormCard.tsx
@@ -0,0 +1,136 @@
+import { cardEventBodyBlurb } from '@/lib/card-event-body-blurb'
+import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
+import { toNote, toNoteList } from '@/lib/link'
+import { useSecondaryPageOptional } from '@/PageManager'
+import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
+import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider'
+import { cn } from '@/lib/utils'
+import { Event, kinds } from 'nostr-tools'
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import Image from '../Image'
+
+/**
+ * Feed / embed / preview surface for NIP-23 long-form (kind 30023): title, summary, image, tags — no “Show more” body.
+ * Full article stays on the note page ({@link showFull} on {@link Note}).
+ */
+export default function LongFormCard({
+ event,
+ className,
+ /** When false (e.g. parent-reply preview strip), card is non-interactive like the old one-line preview. */
+ interactive = true
+}: {
+ event: Event
+ className?: string
+ interactive?: boolean
+}) {
+ const { t } = useTranslation()
+ const screenSize = useScreenSizeOptional()
+ const isSmallScreen = screenSize?.isSmallScreen ?? false
+ const secondaryPage = useSecondaryPageOptional()
+ const push = secondaryPage?.push ?? ((url: string) => {
+ window.location.href = url
+ })
+ const contentPolicy = useContentPolicyOptional()
+ const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true
+ const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
+ const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content])
+ const summaryText = (metadata.summary?.trim() || bodyBlurb).trim()
+
+ const displayTitle = metadata.title?.trim() || t('Long-form Article')
+
+ const handleCardClick = (e: React.MouseEvent) => {
+ if (!interactive) return
+ e.stopPropagation()
+ push(toNote(event.id))
+ }
+
+ const titleComponent = (
+ {displayTitle}
+ )
+
+ const tagsComponent = interactive && metadata.tags.length > 0 && (
+
+ {metadata.tags.map((tag) => (
+
{
+ e.stopPropagation()
+ push(toNoteList({ hashtag: tag, kinds: [kinds.LongFormArticle] }))
+ }}
+ >
+ #{tag}
+
+ ))}
+
+ )
+
+ const tagsReadonly = !interactive && metadata.tags.length > 0 && (
+
+ {metadata.tags.map((tag) => (
+
+ #{tag}
+
+ ))}
+
+ )
+
+ const summaryComponent = summaryText ? (
+ {summaryText}
+ ) : null
+
+ const shellClass = cn(className, !interactive && 'pointer-events-none')
+ const cardClass = cn(
+ 'rounded-lg border p-4 transition-colors',
+ interactive && 'cursor-pointer hover:bg-muted/50'
+ )
+
+ if (isSmallScreen) {
+ return (
+
+
+ {metadata.image && autoLoadMedia && (
+
+ )}
+
+ {titleComponent}
+ {summaryComponent}
+ {tagsComponent}
+ {tagsReadonly}
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ {metadata.image && autoLoadMedia && (
+
+ )}
+
+ {titleComponent}
+ {summaryComponent}
+ {tagsComponent}
+ {tagsReadonly}
+
+
+
+
+ )
+}
diff --git a/src/components/Note/PublicationCard.tsx b/src/components/Note/PublicationCard.tsx
index 6bfd5331..541d5bd6 100644
--- a/src/components/Note/PublicationCard.tsx
+++ b/src/components/Note/PublicationCard.tsx
@@ -1,3 +1,4 @@
+import { cardEventBodyBlurb } from '@/lib/card-event-body-blurb'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList } from '@/lib/link'
import { cn } from '@/lib/utils'
@@ -24,6 +25,8 @@ export default function PublicationCard({
const contentPolicy = useContentPolicyOptional()
const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
+ const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content])
+ const summaryText = (metadata.summary?.trim() || bodyBlurb).trim()
const bookMetadata = useMemo(() => extractBookMetadata(event), [event])
const isBookstrEvent = (event.kind === ExtendedKind.PUBLICATION || event.kind === ExtendedKind.PUBLICATION_CONTENT) && !!bookMetadata.book
@@ -69,11 +72,11 @@ export default function PublicationCard({
)
- const summaryComponent = metadata.summary && (
+ const summaryComponent = summaryText ? (
- {metadata.summary}
+ {summaryText}
- )
+ ) : null
if (isSmallScreen) {
return (
@@ -111,6 +114,7 @@ export default function PublicationCard({
{metadata.image && autoLoadMedia && (
diff --git a/src/components/Note/WikiCard.tsx b/src/components/Note/WikiCard.tsx
index 1ac5f999..42eb2fad 100644
--- a/src/components/Note/WikiCard.tsx
+++ b/src/components/Note/WikiCard.tsx
@@ -1,3 +1,4 @@
+import { cardEventBodyBlurb } from '@/lib/card-event-body-blurb'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList } from '@/lib/link'
import { useSecondaryPageOptional } from '@/PageManager'
@@ -21,6 +22,8 @@ export default function WikiCard({
const contentPolicy = useContentPolicyOptional()
const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
+ const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content])
+ const summaryText = (metadata.summary?.trim() || bodyBlurb).trim()
const handleCardClick = (e: React.MouseEvent) => {
e.stopPropagation()
@@ -46,9 +49,9 @@ export default function WikiCard({
)
- const summaryComponent = metadata.summary && (
- {metadata.summary}
- )
+ const summaryComponent = summaryText ? (
+ {summaryText}
+ ) : null
if (isSmallScreen) {
return (
@@ -84,6 +87,7 @@ export default function WikiCard({
{metadata.image && autoLoadMedia && (
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index 00aba1d1..bd143c4a 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -60,6 +60,7 @@ import AsciidocArticle from './AsciidocArticle/AsciidocArticle'
import PublicationCard from './PublicationCard'
import PublicationIndex from './PublicationIndex/PublicationIndex'
import WikiCard from './WikiCard'
+import LongFormCard from './LongFormCard'
import MutedNote from './MutedNote'
import NsfwNote from './NsfwNote'
import PictureNote from './PictureNote'
@@ -329,7 +330,11 @@ export default function Note({
)
} else if (event.kind === kinds.LongFormArticle) {
- content = renderEventContent({ hideMetadata: true })
+ content = showFull ? (
+ renderEventContent({ hideMetadata: true })
+ ) : (
+
+ )
} else if (event.kind === kinds.LiveEvent || event.kind === 30312 || event.kind === 30313) {
content =
} else if (event.kind === ExtendedKind.GROUP_METADATA) {
diff --git a/src/lib/card-event-body-blurb.ts b/src/lib/card-event-body-blurb.ts
new file mode 100644
index 00000000..f7f9a15d
--- /dev/null
+++ b/src/lib/card-event-body-blurb.ts
@@ -0,0 +1,26 @@
+/** Max length for card blurb when the `summary` tag is absent (article-style events). */
+export const CARD_EVENT_BODY_BLURB_MAX = 250
+
+/**
+ * First {@link CARD_EVENT_BODY_BLURB_MAX} characters of event `content`, stripped of common
+ * markdown / lightweight HTML so feed cards can show a readable teaser without a `summary` tag.
+ */
+export function cardEventBodyBlurb(raw: string | undefined, max = CARD_EVENT_BODY_BLURB_MAX): string {
+ if (raw == null) return ''
+ let s = raw.trim()
+ if (!s) return ''
+
+ s = s.replace(/```[\s\S]*?```/g, ' ')
+ s = s.replace(/`[^`]+`/g, ' ')
+ s = s.replace(/<[^>]+>/g, ' ')
+ s = s.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
+ s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
+ s = s.replace(/^#{1,6}\s+/gm, '')
+ s = s.replace(/\*\*|__/g, '')
+ s = s.replace(/~~/g, '')
+ s = s.replace(/\*|_/g, ' ')
+ s = s.replace(/\s+/g, ' ').trim()
+ if (!s) return ''
+ if (s.length <= max) return s
+ return `${s.slice(0, max).trimEnd()}…`
+}
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index 67f98278..366bcb92 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -518,14 +518,15 @@ export function getLongFormArticleMetadataFromEvent(event: Event) {
const tags = new Set()
event.tags.forEach(([tagName, tagValue]) => {
- if (tagName === 'title') {
- title = tagValue
- } else if (tagName === 'summary') {
- summary = tagValue
- } else if (tagName === 'image') {
- image = tagValue
- } else if (tagName === 't' && tagValue && tags.size < 6) {
- tags.add(tagValue.toLowerCase())
+ const n = tagName?.toLowerCase()
+ if (n === 'title' && tagValue?.trim()) {
+ title = tagValue.trim()
+ } else if (n === 'summary' && tagValue?.trim()) {
+ summary = tagValue.trim()
+ } else if (n === 'image' && tagValue?.trim()) {
+ image = tagValue.trim()
+ } else if (n === 't' && tagValue?.trim() && tags.size < 6) {
+ tags.add(tagValue.trim().replace(/^#/, '').toLowerCase())
}
})