import { useEffect, useState } from "react"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useUIStore } from "../../stores/ui"; import { fetchNoteById } from "../../lib/nostr"; import { useProfile } from "../../hooks/useProfile"; import { shortenPubkey } from "../../lib/utils"; import { ImageLightbox } from "../shared/ImageLightbox"; import { parseContent } from "../../lib/parsing"; import { renderTextSegments } from "./TextSegments"; import { VideoBlock, AudioBlock, YouTubeCard, VimeoCard, SpotifyCard, TidalCard } from "./MediaCards"; import { FountainCard } from "./FountainCard"; function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (index: number) => void }) { const count = images.length; if (count === 0) return null; const maxVisible = Math.min(count, 4); const extraCount = count - 4; const visible = images.slice(0, maxVisible); if (count === 1) { return (
{ e.stopPropagation(); onImageClick(0); }} onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
); } if (count === 2) { return (
{visible.map((src, idx) => ( { e.stopPropagation(); onImageClick(idx); }} onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} /> ))}
); } if (count === 3) { return (
{ e.stopPropagation(); onImageClick(0); }} onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} /> { e.stopPropagation(); onImageClick(1); }} onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} /> { e.stopPropagation(); onImageClick(2); }} onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
); } // 4+ images: 2x2 grid with "+N more" overlay on 4th return (
{visible.map((src, idx) => (
{ e.stopPropagation(); onImageClick(idx); }} onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} /> {idx === 3 && extraCount > 0 && (
{ e.stopPropagation(); onImageClick(idx); }} > +{extraCount}
)}
))}
); } function QuotePreview({ eventId }: { eventId: string }) { const [event, setEvent] = useState(null); const { openThread, currentView } = useUIStore(); const profile = useProfile(event?.pubkey ?? ""); useEffect(() => { if (!eventId) return; fetchNoteById(eventId).then(setEvent); }, [eventId]); if (!event) return null; const rawName = profile?.displayName || profile?.name; const name = (typeof rawName === "string" ? rawName : null) || shortenPubkey(event.pubkey); const preview = event.content.slice(0, 160) + (event.content.length > 160 ? "…" : ""); return (
{ e.stopPropagation(); openThread(event, currentView as "feed" | "profile"); }} >
{profile?.picture && ( { (e.target as HTMLImageElement).style.display = "none"; }} /> )} {name}

{preview}

); } interface NoteContentProps { content: string; /** Render only inline text (no media blocks). Used inside the clickable area. */ inline?: boolean; /** Render only media blocks (videos, embeds, quotes). Used outside the clickable area. */ mediaOnly?: boolean; } export function NoteContent({ content, inline, mediaOnly }: NoteContentProps) { const { openHashtag } = useUIStore(); const segments = parseContent(content); const images: string[] = segments.filter((s) => s.type === "image").map((s) => s.value); const videos: string[] = segments.filter((s) => s.type === "video").map((s) => s.value); const audios: string[] = segments.filter((s) => s.type === "audio").map((s) => s.value); const youtubes = segments.filter((s) => s.type === "youtube"); const vimeos = segments.filter((s) => s.type === "vimeo"); const spotifys = segments.filter((s) => s.type === "spotify"); const tidals = segments.filter((s) => s.type === "tidal"); const fountains = segments.filter((s) => s.type === "fountain"); const quoteIds: string[] = segments.filter((s) => s.type === "quote").map((s) => s.value); const [lightboxIndex, setLightboxIndex] = useState(null); // --- Inline text + images (safe inside clickable wrapper) --- if (inline) { return (
{renderTextSegments(segments, openHashtag, { resolveMentions: true })}
{lightboxIndex !== null && ( setLightboxIndex(null)} onNavigate={setLightboxIndex} /> )}
); } // --- Media blocks only (rendered OUTSIDE the clickable wrapper) --- if (mediaOnly) { const hasMedia = videos.length > 0 || audios.length > 0 || youtubes.length > 0 || vimeos.length > 0 || spotifys.length > 0 || tidals.length > 0 || fountains.length > 0 || quoteIds.length > 0; if (!hasMedia) return null; return (
e.stopPropagation()}> {youtubes.map((seg, i) => )} {vimeos.map((seg, i) => )} {spotifys.map((seg, i) => )} {tidals.map((seg, i) => )} {fountains.map((seg, i) => )} {quoteIds.map((id) => )}
); } // --- Default: full render (used in ThreadView, SearchView, etc.) --- return (
{renderTextSegments(segments, openHashtag)}
{lightboxIndex !== null && ( setLightboxIndex(null)} onNavigate={setLightboxIndex} /> )} {youtubes.map((seg, i) => )} {vimeos.map((seg, i) => )} {spotifys.map((seg, i) => )} {tidals.map((seg, i) => )} {fountains.map((seg, i) => )} {quoteIds.map((id) => )}
); }