fix: hover state on url preview image and make it keyboard friendly (#2777)

add hover state on url preview image and make it keyboard friendly
This commit is contained in:
Ajay Bura
2026-03-14 17:22:18 +11:00
committed by GitHub
parent 7570a84dfd
commit 3d354909d6
3 changed files with 93 additions and 90 deletions

View File

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

View File

@@ -23,6 +23,11 @@ export const UrlPreviewImg = style([
objectPosition: 'center', objectPosition: 'center',
flexShrink: 0, flexShrink: 0,
overflow: 'hidden', overflow: 'hidden',
cursor: 'pointer',
':hover': {
filter: 'brightness(0.8)',
},
}, },
]); ]);

View File

@@ -1,7 +1,7 @@
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import React, { 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 { 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';
@@ -13,97 +13,100 @@ import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom'; import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix'; import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { ImageViewer } from '../image-viewer';
import { onEnterOrSpace } from '../../utils/keyboard';
const linkStyles = { color: color.Success.Main }; const linkStyles = { color: color.Success.Main };
export const UrlPreviewCard = as< export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
'div', ({ url, ts, ...props }, ref) => {
{ url: string; ts: number; renderViewer: (props: RenderViewerProps) => ReactNode } const mx = useMatrixClient();
>(({ url, ts, renderViewer, ...props }, ref) => { const useAuthentication = useMediaAuthentication();
const mx = useMatrixClient(); const [viewer, setViewer] = useState(false);
const useAuthentication = useMediaAuthentication(); const [previewStatus, loadPreview] = useAsyncCallback(
const [viewer, setViewer] = useState(false); useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
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
); );
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication); 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
);
const imgUrl = mxcUrlToHttp(mx, prev['og:image'] || '', useAuthentication);
return (
<>
{thumbUrl && (
<UrlPreviewImg
src={thumbUrl}
alt={prev['og:title']}
title={prev['og:title']}
tabIndex={0}
onKeyDown={(evt) => onEnterOrSpace(() => setViewer(true))(evt)}
onClick={() => setViewer(true)}
/>
)}
{imgUrl && (
<ImageOverlay
src={imgUrl}
alt={prev['og:title']}
viewer={viewer}
requestClose={() => {
setViewer(false);
}}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
<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}>
{thumbUrl && ( {previewStatus.status === AsyncStatus.Success ? (
<UrlPreviewImg renderContent(previewStatus.data)
src={thumbUrl} ) : (
alt={prev['og:title']} <Box grow="Yes" alignItems="Center" justifyContent="Center">
title={prev['og:title']} <Spinner variant="Secondary" size="400" />
onClick={() => setViewer(true)} </Box>
/>
)} )}
{imgUrl && ( </UrlPreview>
<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);