From ec7ae6933c3314c3cb2a261ab735b38d11e5da7d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 13:50:35 +0000 Subject: [PATCH] refactor: add global loadMedia setting with CompactMediaRenderer - Add `loadMedia` setting to Appearance section (enabled by default) - Create CompactMediaRenderer in src/components/nostr/ (renamed from ChatMediaRenderer) - Link.tsx and Gallery.tsx now check the setting and use CompactMediaRenderer when disabled - Tooltip format improved: "Field " with field name and value side by side - Shows: Hash, Size, Dimensions, Type, Duration, Alt - Remove renderMedia prop from ChatViewer (now automatic based on setting) - Delete old ChatMediaRenderer.tsx This makes media rendering a site-wide setting rather than chat-specific. https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD --- src/components/ChatViewer.tsx | 7 +-- src/components/SettingsViewer.tsx | 21 +++++++++ .../CompactMediaRenderer.tsx} | 44 ++++++++++++++----- src/components/nostr/RichText.tsx | 36 ++++----------- src/components/nostr/RichText/Gallery.tsx | 25 ++++++----- src/components/nostr/RichText/Link.tsx | 25 ++++++----- src/services/settings.ts | 3 ++ 7 files changed, 94 insertions(+), 67 deletions(-) rename src/components/{chat/ChatMediaRenderer.tsx => nostr/CompactMediaRenderer.tsx} (79%) diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index 30ff83e..efd7d23 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -40,7 +40,6 @@ import { } from "@/lib/chat/group-system-messages"; import { UserName } from "./nostr/UserName"; import { RichText } from "./nostr/RichText"; -import { ChatMediaRenderer } from "./chat/ChatMediaRenderer"; import Timestamp from "./Timestamp"; import { ReplyPreview } from "./chat/ReplyPreview"; import { MembersDropdown } from "./chat/MembersDropdown"; @@ -459,11 +458,7 @@ const MessageItem = memo(function MessageItem({
{message.event ? ( - + {message.replyTo && (
+ +
+
+ +

+ Render links to media as inline images, videos, and audio +

+
+ + updateSetting("appearance", "loadMedia", checked) + } + /> +
diff --git a/src/components/chat/ChatMediaRenderer.tsx b/src/components/nostr/CompactMediaRenderer.tsx similarity index 79% rename from src/components/chat/ChatMediaRenderer.tsx rename to src/components/nostr/CompactMediaRenderer.tsx index a835fcc..8fc04ae 100644 --- a/src/components/chat/ChatMediaRenderer.tsx +++ b/src/components/nostr/CompactMediaRenderer.tsx @@ -1,5 +1,5 @@ /** - * Chat-specific media renderer + * Compact media renderer for RichText * * Shows compact inline file info with expandable media: * [icon] truncated-hash [blossom] @@ -111,7 +111,7 @@ function MediaIcon({ type }: { type: "image" | "video" | "audio" }) { } } -export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) { +export function CompactMediaRenderer({ url, type, imeta }: MediaRendererProps) { const { addWindow } = useGrimoire(); const [expanded, setExpanded] = useState(false); @@ -160,19 +160,43 @@ export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) { } // Build tooltip content from imeta if available + // Format: "Field " with field name and value side by side const tooltipContent = imeta ? ( -
- {imeta.alt &&
{imeta.alt}
} - {imeta.m &&
{imeta.m}
} - {imeta.dim &&
{imeta.dim}
} +
+ {imeta.x && ( +
+ Hash + {imeta.x} +
+ )} {imeta.size && ( -
- {formatFileSize(imeta.size)} +
+ Size + {formatFileSize(imeta.size)} +
+ )} + {imeta.dim && ( +
+ Dimensions + {imeta.dim} +
+ )} + {imeta.m && ( +
+ Type + {imeta.m}
)} {imeta.duration && ( -
- {formatDuration(imeta.duration)} +
+ Duration + {formatDuration(imeta.duration)} +
+ )} + {imeta.alt && ( +
+ Alt + {imeta.alt}
)}
diff --git a/src/components/nostr/RichText.tsx b/src/components/nostr/RichText.tsx index 58436f5..74ec506 100644 --- a/src/components/nostr/RichText.tsx +++ b/src/components/nostr/RichText.tsx @@ -26,14 +26,6 @@ export interface MediaRendererProps { 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); @@ -113,8 +105,6 @@ 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; } @@ -143,12 +133,8 @@ export function RichText({ depth = 1, options = {}, parserOptions = {}, - renderMedia, children, }: RichTextProps) { - // Get parent media renderer to inherit if not explicitly overridden - const parentMediaRenderer = useMediaRenderer(); - // Merge provided options with defaults const mergedOptions: Required = { ...defaultOptions, @@ -194,19 +180,15 @@ 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 d4441f0..67addc4 100644 --- a/src/components/nostr/RichText/Gallery.tsx +++ b/src/components/nostr/RichText/Gallery.tsx @@ -6,12 +6,10 @@ import { } from "applesauce-core/helpers/url"; import { MediaDialog } from "../MediaDialog"; import { MediaEmbed } from "../MediaEmbed"; -import { - useRichTextOptions, - useMediaRenderer, - useRichTextEvent, -} from "../RichText"; +import { CompactMediaRenderer } from "../CompactMediaRenderer"; +import { useRichTextOptions, useRichTextEvent } from "../RichText"; import { findImetaForUrl } from "@/lib/imeta"; +import { useSettings } from "@/hooks/useSettings"; function MediaPlaceholder({ type, @@ -29,11 +27,14 @@ interface GalleryNodeProps { export function Gallery({ node }: GalleryNodeProps) { const options = useRichTextOptions(); - const CustomMediaRenderer = useMediaRenderer(); const event = useRichTextEvent(); + const { settings } = useSettings(); const [dialogOpen, setDialogOpen] = useState(false); const [initialIndex, setInitialIndex] = useState(0); + // Check global loadMedia setting + const loadMedia = settings?.appearance?.loadMedia ?? true; + const links = node.links || []; const handleAudioClick = (index: number) => { @@ -50,8 +51,8 @@ export function Gallery({ node }: GalleryNodeProps) { if (isImageURL(url)) { if (shouldShowMedia && options.showImages) { - if (CustomMediaRenderer) { - return ; + if (!loadMedia) { + return ; } return ; } @@ -59,8 +60,8 @@ export function Gallery({ node }: GalleryNodeProps) { } if (isVideoURL(url)) { if (shouldShowMedia && options.showVideos) { - if (CustomMediaRenderer) { - return ; + if (!loadMedia) { + return ; } return ; } @@ -68,8 +69,8 @@ export function Gallery({ node }: GalleryNodeProps) { } if (isAudioURL(url)) { if (shouldShowMedia && options.showAudio) { - if (CustomMediaRenderer) { - return ; + if (!loadMedia) { + return ; } return ( [{type}]; @@ -26,11 +24,14 @@ interface LinkNodeProps { export function Link({ node }: LinkNodeProps) { const options = useRichTextOptions(); - const CustomMediaRenderer = useMediaRenderer(); const event = useRichTextEvent(); + const { settings } = useSettings(); const [dialogOpen, setDialogOpen] = useState(false); const { href } = node; + // Check global loadMedia setting + const loadMedia = settings?.appearance?.loadMedia ?? true; + // Look up imeta for this URL if event is available const imeta = event ? findImetaForUrl(event, href) : undefined; @@ -44,8 +45,8 @@ export function Link({ node }: LinkNodeProps) { // Render appropriate link type if (isImageURL(href)) { if (shouldShowMedia && options.showImages) { - if (CustomMediaRenderer) { - return ; + if (!loadMedia) { + return ; } return ( ; + if (!loadMedia) { + return ; } return ( ; + if (!loadMedia) { + return ; } return ( <> diff --git a/src/services/settings.ts b/src/services/settings.ts index 4effa8f..bc798c1 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -22,6 +22,8 @@ export interface PostSettings { export interface AppearanceSettings { /** Show client tags in event UI */ showClientTags: boolean; + /** Load media inline (images, videos, audio) - when false, show compact links */ + loadMedia: boolean; } /** @@ -43,6 +45,7 @@ const DEFAULT_POST_SETTINGS: PostSettings = { const DEFAULT_APPEARANCE_SETTINGS: AppearanceSettings = { showClientTags: true, + loadMedia: true, }; export const DEFAULT_SETTINGS: AppSettings = {