diff --git a/src/components/nostr/RichText.tsx b/src/components/nostr/RichText.tsx index d5487a3..a6b4cac 100644 --- a/src/components/nostr/RichText.tsx +++ b/src/components/nostr/RichText.tsx @@ -14,6 +14,32 @@ import { nipReferences } from "@/lib/nip-transformer"; import { relayReferences } from "@/lib/relay-transformer"; import type { NostrEvent } from "@/types/nostr"; import type { Root } from "applesauce-content/nast"; +import type { ImetaEntry } from "@/lib/imeta"; + +/** + * Props for custom media renderers + */ +export interface MediaRendererProps { + url: string; + type: "image" | "video" | "audio"; + /** Image/video metadata from imeta tags (NIP-92) if available */ + imeta?: ImetaEntry; +} + +// Context for custom media rendering across RichText subtree +const MediaRendererContext = + createContext | null>(null); + +export function useMediaRenderer() { + return useContext(MediaRendererContext); +} + +// Context for passing the source event (for imeta lookup) +const EventContext = createContext(null); + +export function useRichTextEvent() { + return useContext(EventContext); +} /** Transformer function type compatible with applesauce-content */ export type ContentTransformer = () => (tree: Root) => void; @@ -87,6 +113,8 @@ interface RichTextProps { options?: RichTextOptions; /** Parser options for customizing content parsing */ parserOptions?: ParserOptions; + /** Custom media renderer for images, videos, and audio */ + renderMedia?: React.ComponentType; children?: React.ReactNode; } @@ -115,6 +143,7 @@ export function RichText({ depth = 1, options = {}, parserOptions = {}, + renderMedia, children, }: RichTextProps) { // Merge provided options with defaults @@ -162,13 +191,17 @@ export function RichText({ return ( -
- {children} - {renderedContent} -
+ + +
+ {children} + {renderedContent} +
+
+
); diff --git a/src/components/nostr/RichText/Gallery.tsx b/src/components/nostr/RichText/Gallery.tsx index 659d470..d4441f0 100644 --- a/src/components/nostr/RichText/Gallery.tsx +++ b/src/components/nostr/RichText/Gallery.tsx @@ -6,7 +6,12 @@ import { } from "applesauce-core/helpers/url"; import { MediaDialog } from "../MediaDialog"; import { MediaEmbed } from "../MediaEmbed"; -import { useRichTextOptions } from "../RichText"; +import { + useRichTextOptions, + useMediaRenderer, + useRichTextEvent, +} from "../RichText"; +import { findImetaForUrl } from "@/lib/imeta"; function MediaPlaceholder({ type, @@ -24,6 +29,8 @@ interface GalleryNodeProps { export function Gallery({ node }: GalleryNodeProps) { const options = useRichTextOptions(); + const CustomMediaRenderer = useMediaRenderer(); + const event = useRichTextEvent(); const [dialogOpen, setDialogOpen] = useState(false); const [initialIndex, setInitialIndex] = useState(0); @@ -38,20 +45,32 @@ export function Gallery({ node }: GalleryNodeProps) { // Check if media should be shown const shouldShowMedia = options.showMedia; + // Look up imeta for this URL if event is available + const imeta = event ? findImetaForUrl(event, url) : undefined; + if (isImageURL(url)) { if (shouldShowMedia && options.showImages) { + if (CustomMediaRenderer) { + return ; + } return ; } return ; } if (isVideoURL(url)) { if (shouldShowMedia && options.showVideos) { + if (CustomMediaRenderer) { + return ; + } return ; } return ; } if (isAudioURL(url)) { if (shouldShowMedia && options.showAudio) { + if (CustomMediaRenderer) { + return ; + } return ( [{type}]; @@ -21,9 +26,14 @@ interface LinkNodeProps { export function Link({ node }: LinkNodeProps) { const options = useRichTextOptions(); + const CustomMediaRenderer = useMediaRenderer(); + const event = useRichTextEvent(); const [dialogOpen, setDialogOpen] = useState(false); const { href } = node; + // Look up imeta for this URL if event is available + const imeta = event ? findImetaForUrl(event, href) : undefined; + const handleAudioClick = () => { setDialogOpen(true); }; @@ -34,6 +44,9 @@ export function Link({ node }: LinkNodeProps) { // Render appropriate link type if (isImageURL(href)) { if (shouldShowMedia && options.showImages) { + if (CustomMediaRenderer) { + return ; + } return ( ; + } return ( ; + } return ( <> entry !== null); } +/** + * Find imeta entry for a specific URL + */ +export function findImetaForUrl( + event: NostrEvent, + url: string, +): ImetaEntry | undefined { + const entries = parseImetaTags(event); + return entries.find((entry) => entry.url === url); +} + /** * Parse file metadata from NIP-94 kind 1063 event tags */