diff --git a/src/components/Note/PublicationCoverFallback.tsx b/src/components/Note/PublicationCoverFallback.tsx
new file mode 100644
index 00000000..a2abc8b7
--- /dev/null
+++ b/src/components/Note/PublicationCoverFallback.tsx
@@ -0,0 +1,33 @@
+import { cn } from '@/lib/utils'
+import { BookOpen } from 'lucide-react'
+import {
+ LIBRARY_PUBLICATION_COVER_MAX_CLASS,
+ PUBLICATION_COVER_MAX_CLASS
+} from './PublicationCoverImage'
+
+export default function PublicationCoverFallback({
+ layout,
+ size = 'default',
+ className
+}: {
+ layout: 'stacked' | 'row'
+ size?: 'library' | 'default'
+ className?: string
+}) {
+ const maxClass = size === 'library' ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS
+
+ return (
+
+ )
+}
diff --git a/src/components/Note/PublicationCoverImage.tsx b/src/components/Note/PublicationCoverImage.tsx
new file mode 100644
index 00000000..b3e23dd0
--- /dev/null
+++ b/src/components/Note/PublicationCoverImage.tsx
@@ -0,0 +1,47 @@
+import { cn } from '@/lib/utils'
+import Image from '../Image'
+
+/** Max cover height in the library grid (3-column cards). */
+export const LIBRARY_PUBLICATION_COVER_MAX_CLASS = 'max-h-48'
+
+/** Max cover height in publication detail / default cards. */
+export const PUBLICATION_COVER_MAX_CLASS = 'max-h-[400px]'
+
+export default function PublicationCoverImage({
+ imageUrl,
+ pubkey,
+ autoLoadMedia,
+ size = 'default',
+ layout = 'stacked',
+ className
+}: {
+ imageUrl: string
+ pubkey: string
+ autoLoadMedia: boolean
+ size?: 'library' | 'default'
+ layout?: 'stacked' | 'row'
+ className?: string
+}) {
+ const maxClass = size === 'library' ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS
+
+ return (
+
+ )
+}
diff --git a/src/components/Note/PublicationIndexMetadata.tsx b/src/components/Note/PublicationIndexMetadata.tsx
index b957bfb9..ef4912c7 100644
--- a/src/components/Note/PublicationIndexMetadata.tsx
+++ b/src/components/Note/PublicationIndexMetadata.tsx
@@ -11,7 +11,8 @@ import { BookOpen, ExternalLink } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
-import Image from '../Image'
+import PublicationCoverFallback from './PublicationCoverFallback'
+import PublicationCoverImage from './PublicationCoverImage'
function formatAuthorLine(authors: PublicationAuthor[]): string {
if (authors.length === 0) return ''
@@ -120,16 +121,16 @@ export default function PublicationIndexMetadata({
return (
{isFull && metadata.image?.trim() ? (
-
) : isFull ? (
-
-
-
+
) : null}
{showTitle ? (
diff --git a/src/components/ReplyNoteList/ThreadQuoteBacklink.tsx b/src/components/ReplyNoteList/ThreadQuoteBacklink.tsx
index 839d02cf..4fec404e 100644
--- a/src/components/ReplyNoteList/ThreadQuoteBacklink.tsx
+++ b/src/components/ReplyNoteList/ThreadQuoteBacklink.tsx
@@ -3,6 +3,7 @@ import { ExtendedKind } from '@/constants'
import { getKindDescription } from '@/lib/kind-description'
import { toNote } from '@/lib/link'
import { stripNostrIdsFromPlainTextSnippet } from '@/lib/snippet-sanitize'
+import { formatNip32LabelSnippet } from '@/lib/nip32-label'
import { cn } from '@/lib/utils'
import { FormattedTimestamp } from '@/components/FormattedTimestamp'
import UserAvatar from '@/components/UserAvatar'
@@ -41,15 +42,8 @@ function quoteBacklinkSnippet(event: Event, maxLen = 96): string {
}
}
if (event.kind === kinds.Label) {
- const L = event.tags.find((t) => t[0] === 'l' || t[0] === 'L')
- if (L) {
- const parts = [L[1], L[2], L[3]].filter(Boolean)
- if (parts.length) return trim(parts.join(' · '))
- }
- if (event.content.trim()) {
- const out = trim(event.content)
- if (out) return out
- }
+ const snippet = formatNip32LabelSnippet(event, maxLen)
+ if (snippet) return trim(snippet)
}
if (event.kind === kinds.Report || event.kind === ExtendedKind.REPORT) {
const rep = event.tags.find((t) => t[0] === 'report' || t[0] === 'Report')?.[1]
diff --git a/src/hooks/useLibraryPublications.ts b/src/hooks/useLibraryPublications.ts
index da5d5271..bbc675b6 100644
--- a/src/hooks/useLibraryPublications.ts
+++ b/src/hooks/useLibraryPublications.ts
@@ -21,6 +21,8 @@ const LOAD_TIMEOUT_MS = 120_000
const EMPTY_ENGAGEMENT: PublicationEngagementMaps = {
labelAddresses: new Set(),
labelEventIds: new Set(),
+ labelValuesByAddress: new Map(),
+ labelValuesByEventId: new Map(),
commentAddresses: new Set(),
highlightAddresses: new Set()
}
diff --git a/src/lib/event-metadata.publication-index.test.ts b/src/lib/event-metadata.publication-index.test.ts
index a7c68b9d..0cf0de4f 100644
--- a/src/lib/event-metadata.publication-index.test.ts
+++ b/src/lib/event-metadata.publication-index.test.ts
@@ -42,6 +42,7 @@ describe('getPublicationIndexMetadataFromEvent', () => {
{ name: 'Arthur W. Ryder', role: 'translator' }
])
expect(meta.source).toBe('https://www.gutenberg.org/ebooks/21020')
+ expect(meta.image).toBe('https://www.gutenberg.org/cache/epub/21020/pg21020.cover.medium.jpg')
expect(meta.language).toBe('en')
expect(meta.releaseDate).toBe('April 10, 2007')
expect(meta.type).toBe('book')
@@ -58,4 +59,30 @@ describe('getPublicationIndexMetadataFromEvent', () => {
expect(meta.title).toBe('Village Life In China')
expect(meta.sectionCount).toBe(1)
})
+
+ it('uses Project Gutenberg cover when source is gutenberg and image tag is missing', () => {
+ const event = indexEvent([
+ ['d', 'pg58363-sketches-of-indian-character'],
+ ['title', 'Sketches of Indian Character'],
+ ['author', 'James Napier Bailey', 'author'],
+ ['source', 'https://www.gutenberg.org/ebooks/58363'],
+ ['a', `30041:${PK}:intro`]
+ ])
+ const meta = getPublicationIndexMetadataFromEvent(event)
+ expect(meta.image).toBe(
+ 'https://www.gutenberg.org/cache/epub/58363/pg58363.cover.medium.jpg'
+ )
+ })
+
+ it('keeps explicit image tag over Gutenberg fallback', () => {
+ const event = indexEvent([
+ ['d', 'book'],
+ ['title', 'Book'],
+ ['source', 'https://www.gutenberg.org/ebooks/58363'],
+ ['image', 'https://example.com/cover.jpg'],
+ ['a', `30041:${PK}:intro`]
+ ])
+ const meta = getPublicationIndexMetadataFromEvent(event)
+ expect(meta.image).toBe('https://example.com/cover.jpg')
+ })
})
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index b2646960..8cb67790 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -2,6 +2,7 @@ import { ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, POLL_TYPE }
import { TEmoji, TMailboxRelay, TPollType, TRelayList, TRelaySet, TPaymentInfo, TProfile } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { buildATag } from './draft-event'
+import { resolveGutenbergCoverImageUrl } from './gutenberg-cover'
import { getLatestEvent, getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, pubkeyToNpub } from './pubkey'
@@ -718,8 +719,14 @@ export function getPublicationIndexMetadataFromEvent(event: Event): PublicationI
}
}
+ let image = base.image?.trim() || undefined
+ if (!image) {
+ image = resolveGutenbergCoverImageUrl(source)
+ }
+
return {
...base,
+ image,
authors,
source,
type,
diff --git a/src/lib/gutenberg-cover.test.ts b/src/lib/gutenberg-cover.test.ts
new file mode 100644
index 00000000..cb9ae631
--- /dev/null
+++ b/src/lib/gutenberg-cover.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from 'vitest'
+import {
+ gutenbergCoverImageUrl,
+ parseGutenbergEbookId,
+ resolveGutenbergCoverImageUrl
+} from '@/lib/gutenberg-cover'
+
+describe('gutenberg-cover', () => {
+ it('parses ebook id from gutenberg.org URLs', () => {
+ expect(parseGutenbergEbookId('https://www.gutenberg.org/ebooks/58363')).toBe('58363')
+ expect(parseGutenbergEbookId('https://www.gutenberg.org/ebooks/58363/')).toBe('58363')
+ expect(parseGutenbergEbookId('https://www.gutenberg.org/files/21020/21020-h/21020-h.htm')).toBe(
+ '21020'
+ )
+ })
+
+ it('builds medium cover URL', () => {
+ expect(gutenbergCoverImageUrl('58363')).toBe(
+ 'https://www.gutenberg.org/cache/epub/58363/pg58363.cover.medium.jpg'
+ )
+ })
+
+ it('resolveGutenbergCoverImageUrl requires gutenberg source', () => {
+ expect(
+ resolveGutenbergCoverImageUrl('https://www.gutenberg.org/ebooks/58363')
+ ).toBe('https://www.gutenberg.org/cache/epub/58363/pg58363.cover.medium.jpg')
+ expect(resolveGutenbergCoverImageUrl('https://example.com/book')).toBeUndefined()
+ })
+})
diff --git a/src/lib/gutenberg-cover.ts b/src/lib/gutenberg-cover.ts
new file mode 100644
index 00000000..48dd3b8f
--- /dev/null
+++ b/src/lib/gutenberg-cover.ts
@@ -0,0 +1,28 @@
+/** Project Gutenberg cover art: https://www.gutenberg.org/cache/epub/{id}/pg{id}.cover.medium.jpg */
+
+const GUTENBERG_EBOOK_URL = /gutenberg\.org\/ebooks\/(\d+)/i
+const GUTENBERG_FILES_URL = /gutenberg\.org\/files\/(\d+)/i
+
+export function parseGutenbergEbookId(source: string): string | null {
+ const trimmed = source.trim()
+ if (!trimmed) return null
+ for (const pattern of [GUTENBERG_EBOOK_URL, GUTENBERG_FILES_URL]) {
+ const match = trimmed.match(pattern)
+ if (match?.[1]) return match[1]
+ }
+ return null
+}
+
+export function gutenbergCoverImageUrl(ebookId: string): string {
+ const id = ebookId.trim()
+ return `https://www.gutenberg.org/cache/epub/${id}/pg${id}.cover.medium.jpg`
+}
+
+/** When `source` points at Project Gutenberg, return the standard medium cover URL. */
+export function resolveGutenbergCoverImageUrl(source: string | undefined): string | undefined {
+ if (!source?.trim()) return undefined
+ if (!source.toLowerCase().includes('gutenberg')) return undefined
+ const id = parseGutenbergEbookId(source)
+ if (!id) return undefined
+ return gutenbergCoverImageUrl(id)
+}
diff --git a/src/lib/library-publication-index.test.ts b/src/lib/library-publication-index.test.ts
index 352ca7de..6f130fdc 100644
--- a/src/lib/library-publication-index.test.ts
+++ b/src/lib/library-publication-index.test.ts
@@ -74,6 +74,31 @@ describe('library-publication-index', () => {
const engaged = filterEngagedPublications([root], indexByAddress, engagement)
expect(engaged).toHaveLength(1)
expect(engaged[0].hasLabel).toBe(true)
+ expect(engaged[0].labelNames).toEqual(['MIT'])
+ })
+
+ it('extracts NIP-32 l tag values, not L namespace declarations', () => {
+ const rootAddr = `30040:${PK}:jane-eyre-an-autobiography`
+ const root = indexEvent('jane-eyre-an-autobiography', [`30041:${PK}:intro`])
+ root.tags = [['d', 'jane-eyre-an-autobiography'], ['title', 'Jane Eyre'], ['a', `30041:${PK}:intro`]]
+ const indexByAddress = buildIndexByAddress([root])
+ const label: Event = {
+ id: '5'.repeat(64),
+ kind: ExtendedKind.LABEL,
+ pubkey: 'f'.repeat(64),
+ created_at: 50,
+ content: '',
+ tags: [
+ ['L', 'ugc'],
+ ['l', 'booklist', 'ugc'],
+ ['a', rootAddr, 'wss://theforest.nostr1.com']
+ ],
+ sig: 'e'.repeat(128)
+ }
+ const engagement = buildEngagementMapsFromEvents([label], [], [])
+ const engaged = filterEngagedPublications([root], indexByAddress, engagement)
+ expect(engaged).toHaveLength(1)
+ expect(engaged[0].labelNames).toEqual(['booklist'])
})
it('filterLibraryPublicationsBySearch matches title', () => {
@@ -82,6 +107,7 @@ describe('library-publication-index', () => {
{
event: root,
hasLabel: true,
+ labelNames: ['MIT'],
hasComment: false,
hasHighlight: false,
engagementCount: 1
diff --git a/src/lib/library-publication-index.ts b/src/lib/library-publication-index.ts
index a222ab5f..8f3710da 100644
--- a/src/lib/library-publication-index.ts
+++ b/src/lib/library-publication-index.ts
@@ -7,6 +7,7 @@ import {
} from '@/lib/general-search-text-match'
import { normalizeToDTag, parseAdvancedSearch } from '@/lib/search-parser'
import logger from '@/lib/logger'
+import { extractNip32LabelValues } from '@/lib/nip32-label'
import { queryIndexRelay, queryIndexRelayForLibrary, queryIndexRelayPublicationSearch } from '@/lib/index-relay-http'
import {
buildIndexByAddress,
@@ -57,6 +58,8 @@ const QUERY_OPTS = {
export type PublicationEngagementMaps = {
labelAddresses: Set
labelEventIds: Set
+ labelValuesByAddress: Map>
+ labelValuesByEventId: Map>
commentAddresses: Set
highlightAddresses: Set
}
@@ -64,6 +67,8 @@ export type PublicationEngagementMaps = {
export type LibraryPublicationEntry = {
event: Event
hasLabel: boolean
+ /** NIP-32 `l` tag values from kind-1985 events (e.g. "booklist"), not `L` namespaces (e.g. "ugc"). */
+ labelNames: string[]
hasComment: boolean
hasHighlight: boolean
engagementCount: number
@@ -301,16 +306,36 @@ export function buildEngagementMapsFromEvents(
): PublicationEngagementMaps {
const labelAddresses = new Set()
const labelEventIds = new Set()
+ const labelValuesByAddress = new Map>()
+ const labelValuesByEventId = new Map>()
const commentAddresses = new Set()
const highlightAddresses = new Set()
const addressMatches = (addr: string) => !targetAddresses || targetAddresses.has(addr)
const eventIdMatches = (id: string) => !targetEventIds || targetEventIds.has(id.toLowerCase())
+ const addLabelValues = (map: Map>, key: string, values: string[]) => {
+ if (values.length === 0) return
+ let set = map.get(key)
+ if (!set) {
+ set = new Set()
+ map.set(key, set)
+ }
+ for (const value of values) set.add(value)
+ }
+
for (const ev of labels) {
+ const labelValues = extractNip32LabelValues(ev.tags)
for (const tag of ev.tags) {
- if (tag[0] === 'a' && tag[1] && addressMatches(tag[1])) labelAddresses.add(tag[1])
- if (tag[0] === 'e' && tag[1] && eventIdMatches(tag[1])) labelEventIds.add(tag[1].toLowerCase())
+ if (tag[0] === 'a' && tag[1] && addressMatches(tag[1])) {
+ labelAddresses.add(tag[1])
+ addLabelValues(labelValuesByAddress, tag[1], labelValues)
+ }
+ if (tag[0] === 'e' && tag[1] && eventIdMatches(tag[1])) {
+ const eventId = tag[1].toLowerCase()
+ labelEventIds.add(eventId)
+ addLabelValues(labelValuesByEventId, eventId, labelValues)
+ }
}
}
@@ -326,7 +351,14 @@ export function buildEngagementMapsFromEvents(
}
}
- return { labelAddresses, labelEventIds, commentAddresses, highlightAddresses }
+ return {
+ labelAddresses,
+ labelEventIds,
+ labelValuesByAddress,
+ labelValuesByEventId,
+ commentAddresses,
+ highlightAddresses
+ }
}
async function fetchHttpEngagementByAddresses(
@@ -367,6 +399,8 @@ export async function fetchPublicationEngagementMaps(
return {
labelAddresses: new Set(),
labelEventIds: new Set(),
+ labelValuesByAddress: new Map(),
+ labelValuesByEventId: new Map(),
commentAddresses: new Set(),
highlightAddresses: new Set()
}
@@ -442,6 +476,24 @@ function addressHasEngagement(
return { hasLabel, hasComment, hasHighlight }
}
+function collectLabelNamesForTarget(
+ address: string,
+ eventId: string | undefined,
+ maps: PublicationEngagementMaps,
+ out: Set
+): void {
+ const byAddress = maps.labelValuesByAddress.get(address)
+ if (byAddress) {
+ for (const value of byAddress) out.add(value)
+ }
+ if (eventId) {
+ const byEventId = maps.labelValuesByEventId.get(eventId.toLowerCase())
+ if (byEventId) {
+ for (const value of byEventId) out.add(value)
+ }
+ }
+}
+
export function filterEngagedPublications(
roots: Event[],
indexByAddress: Map,
@@ -458,11 +510,15 @@ export function filterEngagedPublications(
let hasComment = false
let hasHighlight = false
let engagementCount = 0
+ const labelNames = new Set()
for (const addr of reachable) {
const indexed = indexByAddress.get(addr)
const flags = addressHasEngagement(addr, indexed?.id, engagement)
- if (flags.hasLabel) hasLabel = true
+ if (flags.hasLabel) {
+ hasLabel = true
+ collectLabelNamesForTarget(addr, indexed?.id, engagement, labelNames)
+ }
if (flags.hasComment) hasComment = true
if (flags.hasHighlight) hasHighlight = true
if (flags.hasLabel || flags.hasComment || flags.hasHighlight) engagementCount++
@@ -472,11 +528,15 @@ export function filterEngagedPublications(
hasLabel = hasLabel || rootFlags.hasLabel
hasComment = hasComment || rootFlags.hasComment
hasHighlight = hasHighlight || rootFlags.hasHighlight
+ if (rootFlags.hasLabel) {
+ collectLabelNamesForTarget(rootAddr ?? '', root.id, engagement, labelNames)
+ }
if (hasLabel || hasComment || hasHighlight) {
out.push({
event: root,
hasLabel,
+ labelNames: [...labelNames].sort((a, b) => a.localeCompare(b)),
hasComment,
hasHighlight,
engagementCount: Math.max(engagementCount, 1)
@@ -497,6 +557,7 @@ export function buildRecentPublicationEntries(
.map((event) => ({
event,
hasLabel: false,
+ labelNames: [],
hasComment: false,
hasHighlight: false,
engagementCount: 0
@@ -525,6 +586,8 @@ export function sortLibraryPublications(entries: LibraryPublicationEntry[]): Lib
const EMPTY_ENGAGEMENT: PublicationEngagementMaps = {
labelAddresses: new Set(),
labelEventIds: new Set(),
+ labelValuesByAddress: new Map(),
+ labelValuesByEventId: new Map(),
commentAddresses: new Set(),
highlightAddresses: new Set()
}
@@ -596,6 +659,7 @@ function libraryEntriesFromRoots(
return {
event: root,
hasLabel: false,
+ labelNames: [],
hasComment: false,
hasHighlight: false,
engagementCount: 0
@@ -1102,6 +1166,8 @@ export async function loadLibraryPublicationIndex(
resolve({
labelAddresses: new Set(),
labelEventIds: new Set(),
+ labelValuesByAddress: new Map(),
+ labelValuesByEventId: new Map(),
commentAddresses: new Set(),
highlightAddresses: new Set()
}),
@@ -1118,6 +1184,8 @@ export async function loadLibraryPublicationIndex(
engagement = {
labelAddresses: new Set(),
labelEventIds: new Set(),
+ labelValuesByAddress: new Map(),
+ labelValuesByEventId: new Map(),
commentAddresses: new Set(),
highlightAddresses: new Set()
}
diff --git a/src/lib/nip32-label.test.ts b/src/lib/nip32-label.test.ts
new file mode 100644
index 00000000..d3c9f301
--- /dev/null
+++ b/src/lib/nip32-label.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from 'vitest'
+import { extractNip32LabelValues, formatNip32LabelSnippet } from '@/lib/nip32-label'
+import type { Event } from 'nostr-tools'
+
+describe('nip32-label', () => {
+ it('extracts lowercase l tag values, not uppercase L namespace declarations', () => {
+ const tags = [
+ ['L', 'ugc'],
+ ['l', 'booklist', 'ugc'],
+ ['a', '30040:abc:book', 'wss://relay.example']
+ ]
+ expect(extractNip32LabelValues(tags)).toEqual(['booklist'])
+ })
+
+ it('dedupes label values case-insensitively', () => {
+ const tags = [
+ ['l', 'Booklist', 'ugc'],
+ ['l', 'booklist', 'ugc']
+ ]
+ expect(extractNip32LabelValues(tags)).toEqual(['Booklist'])
+ })
+
+ it('formatNip32LabelSnippet prefers l tag values over content', () => {
+ const event = {
+ kind: 1985,
+ content: 'ignored',
+ tags: [
+ ['L', 'license'],
+ ['l', 'MIT', 'license']
+ ]
+ } as Event
+ expect(formatNip32LabelSnippet(event)).toBe('MIT')
+ })
+})
diff --git a/src/lib/nip32-label.ts b/src/lib/nip32-label.ts
new file mode 100644
index 00000000..55a4796b
--- /dev/null
+++ b/src/lib/nip32-label.ts
@@ -0,0 +1,33 @@
+import type { Event } from 'nostr-tools'
+
+/** NIP-32 lowercase `l` tag values (actual labels), not uppercase `L` namespace declarations. */
+export function extractNip32LabelValues(tags: string[][]): string[] {
+ const out: string[] = []
+ const seen = new Set()
+ for (const tag of tags) {
+ if (tag[0] !== 'l') continue
+ const value = tag[1]?.trim()
+ if (!value) continue
+ const key = value.toLowerCase()
+ if (seen.has(key)) continue
+ seen.add(key)
+ out.push(value)
+ }
+ return out
+}
+
+/** One-line display text for a kind-1985 label event. */
+export function formatNip32LabelSnippet(event: Event, maxLen = 96): string {
+ const values = extractNip32LabelValues(event.tags)
+ if (values.length > 0) {
+ const joined = values.join(' · ')
+ if (joined.length <= maxLen) return joined
+ return `${joined.slice(0, maxLen - 1).trimEnd()}…`
+ }
+ const content = event.content?.trim()
+ if (content) {
+ if (content.length <= maxLen) return content
+ return `${content.slice(0, maxLen - 1).trimEnd()}…`
+ }
+ return ''
+}
diff --git a/src/lib/parent-reply-blurb.ts b/src/lib/parent-reply-blurb.ts
index b5e8ea3e..1dea5b34 100644
--- a/src/lib/parent-reply-blurb.ts
+++ b/src/lib/parent-reply-blurb.ts
@@ -5,6 +5,7 @@ import {
getLongFormArticleMetadataFromEvent
} from '@/lib/event-metadata'
import { tagNameEquals } from '@/lib/tag'
+import { formatNip32LabelSnippet } from '@/lib/nip32-label'
import { stripTrailingStringifiedNostrEvent } from '@/lib/nostr-event-json'
import { Event, kinds } from 'nostr-tools'
@@ -46,6 +47,11 @@ export function getParentReplyBlurbDisplayText(
const subjectTag = event.tags.find(tagNameEquals('subject'))?.[1]?.trim()
if (subjectTag) return truncateBlurb(stripMarkupForPreview(subjectTag), maxLen)
+ if (event.kind === kinds.Label) {
+ const labelSnippet = formatNip32LabelSnippet(event, maxLen)
+ if (labelSnippet) return labelSnippet
+ }
+
if (
event.kind === kinds.LongFormArticle ||
event.kind === ExtendedKind.PUBLICATION ||