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)} + > + 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; + } +}