From 3b8d7fb0269464407f8b36621a3e9c4071e92879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Wed, 15 Apr 2026 23:09:03 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=89=20img=20tag=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/RenderMessageContent.tsx | 21 ++++++-- src/owl/components/ExternalImageCard.css.tsx | 28 ++++++++++ src/owl/components/ExternalImageCard.tsx | 57 ++++++++++++++++++++ src/owl/utils/imageUrl.ts | 10 ++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 src/owl/components/ExternalImageCard.css.tsx create mode 100644 src/owl/components/ExternalImageCard.tsx create mode 100644 src/owl/utils/imageUrl.ts diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index 4cfcb7dc..c71d6407 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -25,6 +25,8 @@ import { VideoContent, } from './message'; import { UrlPreviewCard, UrlPreviewHolder } from './url-preview'; +import { ExternalImageCard } from '../../owl/components/ExternalImageCard'; +import { isImageUrl } from '../../owl/utils/imageUrl'; import { Image, MediaControl, Video } from './media'; import { ImageViewer } from './image-viewer'; import { PdfViewer } from './Pdf-viewer'; @@ -61,12 +63,23 @@ export function RenderMessageContent({ const renderUrlsPreview = (urls: string[]) => { const filteredUrls = urls.filter((url) => !testMatrixTo(url)); if (filteredUrls.length === 0) return undefined; + + const imageUrls = filteredUrls.filter(isImageUrl); + const otherUrls = filteredUrls.filter((url) => !isImageUrl(url)); + return ( - - {filteredUrls.map((url) => ( - + <> + {imageUrls.map((url) => ( + ))} - + {otherUrls.length > 0 && ( + + {otherUrls.map((url) => ( + + ))} + + )} + ); }; const renderCaption = () => { diff --git a/src/owl/components/ExternalImageCard.css.tsx b/src/owl/components/ExternalImageCard.css.tsx new file mode 100644 index 00000000..86b1b8d3 --- /dev/null +++ b/src/owl/components/ExternalImageCard.css.tsx @@ -0,0 +1,28 @@ +import { style } from '@vanilla-extract/css'; +import { DefaultReset, color, config, toRem } from 'folds'; + +export const ExternalImageCard = style([ + DefaultReset, + { + display: 'inline-block', + maxWidth: toRem(400), + borderRadius: config.radii.R300, + overflow: 'hidden', + border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`, + cursor: 'pointer', + + ':hover': { + filter: 'brightness(0.95)', + }, + }, +]); + +export const ExternalImage = style([ + DefaultReset, + { + display: 'block', + maxWidth: '100%', + maxHeight: toRem(400), + objectFit: 'contain', + }, +]); diff --git a/src/owl/components/ExternalImageCard.tsx b/src/owl/components/ExternalImageCard.tsx new file mode 100644 index 00000000..ec94270b --- /dev/null +++ b/src/owl/components/ExternalImageCard.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { Box, Text, as, color } from 'folds'; +import { ImageOverlay } from '../../app/components/ImageOverlay'; +import { ImageViewer } from '../../app/components/image-viewer'; +import { onEnterOrSpace } from '../../app/utils/keyboard'; +import * as css from './ExternalImageCard.css'; +import { tryDecodeURIComponent } from '../../app/utils/dom'; + +const linkStyles = { color: color.Success.Main }; + +export const ExternalImageCard = as<'div', { url: string }>(({ url, ...props }, ref) => { + const [viewer, setViewer] = useState(false); + const [error, setError] = useState(false); + + if (error) return null; + + const filename = url.split('/').pop()?.split('?')[0] || url; + + return ( + +
onEnterOrSpace(() => setViewer(true))(evt)} + onClick={() => setViewer(true)} + > + {filename} setError(true)} + /> +
+ + {tryDecodeURIComponent(url)} + + setViewer(false)} + renderViewer={(p) => } + /> +
+ ); +}); diff --git a/src/owl/utils/imageUrl.ts b/src/owl/utils/imageUrl.ts new file mode 100644 index 00000000..882a7cd3 --- /dev/null +++ b/src/owl/utils/imageUrl.ts @@ -0,0 +1,10 @@ +const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.avif', '.svg', '.bmp', '.ico']; + +export function isImageUrl(url: string): boolean { + try { + const { pathname } = new URL(url); + return IMAGE_EXTENSIONS.some((ext) => pathname.toLowerCase().endsWith(ext)); + } catch { + return false; + } +}