diff --git a/src/app/components/ImageOverlay.tsx b/src/app/components/ImageOverlay.tsx
new file mode 100644
index 00000000..ea690924
--- /dev/null
+++ b/src/app/components/ImageOverlay.tsx
@@ -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) => (
+ }>
+
+ requestClose(),
+ clickOutsideDeactivates: true,
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ evt.stopPropagation()}
+ >
+ {renderViewer({
+ src,
+ alt,
+ requestClose,
+ })}
+
+
+
+
+ )
+);
diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx
index 4cfcb7dc..bc937427 100644
--- a/src/app/components/RenderMessageContent.tsx
+++ b/src/app/components/RenderMessageContent.tsx
@@ -64,7 +64,12 @@ export function RenderMessageContent({
return (
{filteredUrls.map((url) => (
-
+ }
+ ts={ts}
+ />
))}
);
diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx
index cbe85df2..f4efd33a 100644
--- a/src/app/components/url-preview/UrlPreviewCard.tsx
+++ b/src/app/components/url-preview/UrlPreviewCard.tsx
@@ -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 { Box, Icon, IconButton, Icons, Scroll, Spinner, Text, as, color, config } from 'folds';
+import { RenderViewerProps, ImageOverlay } from '../ImageOverlay';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -15,72 +16,94 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
const linkStyles = { color: color.Success.Main };
-export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
- ({ url, ts, ...props }, ref) => {
- const mx = useMatrixClient();
- const useAuthentication = useMediaAuthentication();
- const [previewStatus, loadPreview] = useAsyncCallback(
- useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
+export const UrlPreviewCard = as<
+ 'div',
+ { url: string; ts: number; renderViewer: (props: RenderViewerProps) => ReactNode }
+>(({ url, ts, renderViewer, ...props }, ref) => {
+ const mx = useMatrixClient();
+ const useAuthentication = useMediaAuthentication();
+ const [viewer, setViewer] = useState(false);
+ const [previewStatus, loadPreview] = useAsyncCallback(
+ useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
+ );
+
+ useEffect(() => {
+ loadPreview();
+ }, [loadPreview]);
+
+ if (previewStatus.status === AsyncStatus.Error) return null;
+
+ const renderContent = (prev: IPreviewUrlResponse) => {
+ const thumbUrl = mxcUrlToHttp(
+ mx,
+ prev['og:image'] || '',
+ useAuthentication,
+ 256,
+ 256,
+ 'scale',
+ false
);
- useEffect(() => {
- loadPreview();
- }, [loadPreview]);
-
- if (previewStatus.status === AsyncStatus.Error) return null;
-
- const renderContent = (prev: IPreviewUrlResponse) => {
- const imgUrl = mxcUrlToHttp(
- mx,
- prev['og:image'] || '',
- useAuthentication,
- 256,
- 256,
- 'scale',
- false
- );
-
- return (
- <>
- {imgUrl && }
-
-
- {typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
- {tryDecodeURIComponent(url)}
-
-
- {prev['og:title']}
-
-
- {prev['og:description']}
-
-
- >
- );
- };
+ const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
return (
-
- {previewStatus.status === AsyncStatus.Success ? (
- renderContent(previewStatus.data)
- ) : (
-
-
-
+ <>
+ {thumbUrl && (
+ setViewer(true)}
+ />
)}
-
+ {imgUrl && (
+ {
+ setViewer(false);
+ }}
+ renderViewer={renderViewer}
+ />
+ )}
+
+
+ {typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
+ {tryDecodeURIComponent(url)}
+
+
+ {prev['og:title']}
+
+
+ {prev['og:description']}
+
+
+ >
);
- }
-);
+ };
+
+ return (
+
+ {previewStatus.status === AsyncStatus.Success ? (
+ renderContent(previewStatus.data)
+ ) : (
+
+
+
+ )}
+
+ );
+});
export const UrlPreviewHolder = as<'div'>(({ children, ...props }, ref) => {
const scrollRef = useRef(null);