Show image viewer when clicking url preview thumbnail (#2309)

* Show large image overlay when clicking url preview thumbnail

* Move image overlay into its own component

* Move ImageOverlay props into extended type

* Remove export for internal type
This commit is contained in:
LeaPhant
2026-03-14 06:34:55 +01:00
committed by GitHub
parent 6a05ff5840
commit 7570a84dfd
3 changed files with 135 additions and 62 deletions

View File

@@ -0,0 +1,45 @@
import FocusTrap from 'focus-trap-react';
import { as, Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import React, { ReactNode } from 'react';
import { ModalWide } from '../styles/Modal.css';
import { stopPropagation } from '../utils/keyboard';
export type RenderViewerProps = {
src: string;
alt: string;
requestClose: () => void;
};
type ImageOverlayProps = RenderViewerProps & {
viewer: boolean;
renderViewer: (props: RenderViewerProps) => ReactNode;
};
export const ImageOverlay = as<'div', ImageOverlayProps>(
({ src, alt, viewer, requestClose, renderViewer, ...props }, ref) => (
<Overlay {...props} ref={ref} open={viewer} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => requestClose(),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
className={ModalWide}
size="500"
onContextMenu={(evt: any) => evt.stopPropagation()}
>
{renderViewer({
src,
alt,
requestClose,
})}
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)
);

View File

@@ -64,7 +64,12 @@ export function RenderMessageContent({
return ( return (
<UrlPreviewHolder> <UrlPreviewHolder>
{filteredUrls.map((url) => ( {filteredUrls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} /> <UrlPreviewCard
key={url}
url={url}
renderViewer={(p) => <ImageViewer {...p} />}
ts={ts}
/>
))} ))}
</UrlPreviewHolder> </UrlPreviewHolder>
); );

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { IPreviewUrlResponse } from 'matrix-js-sdk'; import { IPreviewUrlResponse } from 'matrix-js-sdk';
import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds'; import { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
import { RenderViewerProps, ImageOverlay } from '../ImageOverlay';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview'; import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -15,10 +16,13 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const linkStyles = { color: color.Success.Main }; const linkStyles = { color: color.Success.Main };
export const UrlPreviewCard = as<'div', { url: string; ts: number }>( export const UrlPreviewCard = as<
({ url, ts, ...props }, ref) => { 'div',
{ url: string; ts: number; renderViewer: (props: RenderViewerProps) => ReactNode }
>(({ url, ts, renderViewer, ...props }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const [viewer, setViewer] = useState(false);
const [previewStatus, loadPreview] = useAsyncCallback( const [previewStatus, loadPreview] = useAsyncCallback(
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]) useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
); );
@@ -30,7 +34,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
if (previewStatus.status === AsyncStatus.Error) return null; if (previewStatus.status === AsyncStatus.Error) return null;
const renderContent = (prev: IPreviewUrlResponse) => { const renderContent = (prev: IPreviewUrlResponse) => {
const imgUrl = mxcUrlToHttp( const thumbUrl = mxcUrlToHttp(
mx, mx,
prev['og:image'] || '', prev['og:image'] || '',
useAuthentication, useAuthentication,
@@ -40,9 +44,29 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
false false
); );
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
return ( return (
<> <>
{imgUrl && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />} {thumbUrl && (
<UrlPreviewImg
src={thumbUrl}
alt={prev['og:title']}
title={prev['og:title']}
onClick={() => setViewer(true)}
/>
)}
{imgUrl && (
<ImageOverlay
src={imgUrl}
alt={prev['og:title']}
viewer={viewer}
requestClose={() => {
setViewer(false);
}}
renderViewer={renderViewer}
/>
)}
<UrlPreviewContent> <UrlPreviewContent>
<Text <Text
style={linkStyles} style={linkStyles}
@@ -79,8 +103,7 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
)} )}
</UrlPreview> </UrlPreview>
); );
} });
);
export const UrlPreviewHolder = as<'div'>(({ children, ...props }, ref) => { export const UrlPreviewHolder = as<'div'>(({ children, ...props }, ref) => {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);