forked from github/cinny
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:
45
src/app/components/ImageOverlay.tsx
Normal file
45
src/app/components/ImageOverlay.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,72 +16,94 @@ 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',
|
||||||
const mx = useMatrixClient();
|
{ url: string; ts: number; renderViewer: (props: RenderViewerProps) => ReactNode }
|
||||||
const useAuthentication = useMediaAuthentication();
|
>(({ url, ts, renderViewer, ...props }, ref) => {
|
||||||
const [previewStatus, loadPreview] = useAsyncCallback(
|
const mx = useMatrixClient();
|
||||||
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
|
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(() => {
|
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
|
||||||
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 && <UrlPreviewImg src={imgUrl} alt={prev['og:title']} title={prev['og:title']} />}
|
|
||||||
<UrlPreviewContent>
|
|
||||||
<Text
|
|
||||||
style={linkStyles}
|
|
||||||
truncate
|
|
||||||
as="a"
|
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
size="T200"
|
|
||||||
priority="300"
|
|
||||||
>
|
|
||||||
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
|
|
||||||
{tryDecodeURIComponent(url)}
|
|
||||||
</Text>
|
|
||||||
<Text truncate priority="400">
|
|
||||||
<b>{prev['og:title']}</b>
|
|
||||||
</Text>
|
|
||||||
<Text size="T200" priority="300">
|
|
||||||
<UrlPreviewDescription>{prev['og:description']}</UrlPreviewDescription>
|
|
||||||
</Text>
|
|
||||||
</UrlPreviewContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UrlPreview {...props} ref={ref}>
|
<>
|
||||||
{previewStatus.status === AsyncStatus.Success ? (
|
{thumbUrl && (
|
||||||
renderContent(previewStatus.data)
|
<UrlPreviewImg
|
||||||
) : (
|
src={thumbUrl}
|
||||||
<Box grow="Yes" alignItems="Center" justifyContent="Center">
|
alt={prev['og:title']}
|
||||||
<Spinner variant="Secondary" size="400" />
|
title={prev['og:title']}
|
||||||
</Box>
|
onClick={() => setViewer(true)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</UrlPreview>
|
{imgUrl && (
|
||||||
|
<ImageOverlay
|
||||||
|
src={imgUrl}
|
||||||
|
alt={prev['og:title']}
|
||||||
|
viewer={viewer}
|
||||||
|
requestClose={() => {
|
||||||
|
setViewer(false);
|
||||||
|
}}
|
||||||
|
renderViewer={renderViewer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<UrlPreviewContent>
|
||||||
|
<Text
|
||||||
|
style={linkStyles}
|
||||||
|
truncate
|
||||||
|
as="a"
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
size="T200"
|
||||||
|
priority="300"
|
||||||
|
>
|
||||||
|
{typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
|
||||||
|
{tryDecodeURIComponent(url)}
|
||||||
|
</Text>
|
||||||
|
<Text truncate priority="400">
|
||||||
|
<b>{prev['og:title']}</b>
|
||||||
|
</Text>
|
||||||
|
<Text size="T200" priority="300">
|
||||||
|
<UrlPreviewDescription>{prev['og:description']}</UrlPreviewDescription>
|
||||||
|
</Text>
|
||||||
|
</UrlPreviewContent>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<UrlPreview {...props} ref={ref}>
|
||||||
|
{previewStatus.status === AsyncStatus.Success ? (
|
||||||
|
renderContent(previewStatus.data)
|
||||||
|
) : (
|
||||||
|
<Box grow="Yes" alignItems="Center" justifyContent="Center">
|
||||||
|
<Spinner variant="Secondary" size="400" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</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);
|
||||||
|
|||||||
Reference in New Issue
Block a user