- {metadata.image && autoLoadMedia && (
-
- )}
-
- {titleComponent}
- {summaryComponent}
- {tagsComponent}
+
+
+ {metadata.image && autoLoadMedia && (
+
+ )}
+
+ {titleComponent}
+ {summaryComponent}
+ {tagsComponent}
+
diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
index 4e51481..eb74384 100644
--- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
+++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
@@ -1,10 +1,12 @@
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox'
+import ImageCarousel from '@/components/ImageCarousel/ImageCarousel'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList, toProfile } from '@/lib/link'
-import { ExternalLink } from 'lucide-react'
+import { extractAllImagesFromEvent } from '@/lib/image-extraction'
+import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
-import React, { useMemo, useEffect, useRef } from 'react'
+import React, { useMemo, useEffect, useRef, useState } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
@@ -13,6 +15,8 @@ import 'katex/dist/katex.min.css'
import NostrNode from './NostrNode'
import { remarkNostr } from './remarkNostr'
import { Components } from './types'
+import { Button } from '@/components/ui/button'
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
export default function MarkdownArticle({
event,
@@ -23,6 +27,10 @@ export default function MarkdownArticle({
}) {
const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
+ const [isImagesOpen, setIsImagesOpen] = useState(false)
+
+ // Extract all images from the event
+ const allImages = useMemo(() => extractAllImagesFromEvent(event), [event])
const contentRef = useRef
(null)
// Initialize highlight.js for syntax highlighting
@@ -156,15 +164,10 @@ export default function MarkdownArticle({
return <>{children}>
},
- img: (props) => (
-
- )
+ img: () => {
+ // Don't render inline images - they'll be shown in the carousel
+ return null
+ }
}) as Components,
[]
)
@@ -269,6 +272,21 @@ export default function MarkdownArticle({
>
{event.content}
+
+ {/* Image Carousel - Collapsible */}
+ {allImages.length > 0 && (
+
+
+
+
+
+
+
+
+ )}
{metadata.tags.length > 0 && (
{metadata.tags.map((tag) => (
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index 04896d5..f4587d1 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -106,6 +106,8 @@ export default function Note({
)
} else if (event.kind === ExtendedKind.PUBLICATION) {
+ content =
+ } else if (event.kind === ExtendedKind.PUBLICATION_CONTENT) {
content = showFull ? (
) : (
diff --git a/src/lib/image-extraction.ts b/src/lib/image-extraction.ts
new file mode 100644
index 0000000..6ee5de2
--- /dev/null
+++ b/src/lib/image-extraction.ts
@@ -0,0 +1,190 @@
+import { Event } from 'nostr-tools'
+import { TImetaInfo } from '@/types'
+import { getImetaInfosFromEvent } from '@/lib/event'
+
+/**
+ * Extract and normalize all images from an event
+ * This includes images from:
+ * - imeta tags
+ * - content (markdown images, HTML img tags, etc.)
+ * - metadata (title image, etc.)
+ */
+export function extractAllImagesFromEvent(event: Event): TImetaInfo[] {
+ const images: TImetaInfo[] = []
+ const seenUrls = new Set
()
+
+ // Helper function to add media if not already seen
+ const addMedia = (url: string, pubkey: string = event.pubkey) => {
+ if (!url || seenUrls.has(url)) return
+
+ // Normalize URL
+ const normalizedUrl = normalizeImageUrl(url)
+ if (!normalizedUrl) return
+
+ // Check if it's media (image or video)
+ const isVideo = isVideoUrl(normalizedUrl)
+ const isImage = isImageUrl(normalizedUrl)
+
+ if (!isImage && !isVideo) return
+
+ images.push({
+ url: normalizedUrl,
+ pubkey,
+ m: isVideo ? 'video/*' : 'image/*'
+ })
+ seenUrls.add(normalizedUrl)
+ }
+
+ // 1. Extract from imeta tags
+ const imetaMedia = getImetaInfosFromEvent(event)
+ imetaMedia.forEach((item: TImetaInfo) => {
+ if (item.m?.startsWith('image/') || item.m?.startsWith('video/')) {
+ addMedia(item.url, item.pubkey)
+ }
+ })
+
+ // 2. Extract from content - markdown images
+ const markdownImageRegex = /!\[.*?\]\((.*?)\)/g
+ let match
+ while ((match = markdownImageRegex.exec(event.content)) !== null) {
+ addMedia(match[1])
+ }
+
+ // 3. Extract from content - HTML img tags
+ const htmlImgRegex = /
]+src=["']([^"']+)["'][^>]*>/gi
+ while ((match = htmlImgRegex.exec(event.content)) !== null) {
+ addMedia(match[1])
+ }
+
+ // 4. Extract from content - HTML video tags
+ const htmlVideoRegex = /