From 3d354909d63f46cc8912bb753689832e628c1ee3 Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Sat, 14 Mar 2026 17:22:18 +1100
Subject: [PATCH] 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
---
src/app/components/RenderMessageContent.tsx | 7 +-
.../components/url-preview/UrlPreview.css.tsx | 5 +
.../components/url-preview/UrlPreviewCard.tsx | 171 +++++++++---------
3 files changed, 93 insertions(+), 90 deletions(-)
diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx
index bc937427..4cfcb7dc 100644
--- a/src/app/components/RenderMessageContent.tsx
+++ b/src/app/components/RenderMessageContent.tsx
@@ -64,12 +64,7 @@ export function RenderMessageContent({
return (
{filteredUrls.map((url) => (
- }
- ts={ts}
- />
+
))}
);
diff --git a/src/app/components/url-preview/UrlPreview.css.tsx b/src/app/components/url-preview/UrlPreview.css.tsx
index 3192dc49..cd0b2528 100644
--- a/src/app/components/url-preview/UrlPreview.css.tsx
+++ b/src/app/components/url-preview/UrlPreview.css.tsx
@@ -23,6 +23,11 @@ export const UrlPreviewImg = style([
objectPosition: 'center',
flexShrink: 0,
overflow: 'hidden',
+ cursor: 'pointer',
+
+ ':hover': {
+ filter: 'brightness(0.8)',
+ },
},
]);
diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx
index f4efd33a..ee8967af 100644
--- a/src/app/components/url-preview/UrlPreviewCard.tsx
+++ b/src/app/components/url-preview/UrlPreviewCard.tsx
@@ -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 { 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 { useMatrixClient } from '../../hooks/useMatrixClient';
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
@@ -13,97 +13,100 @@ import * as css from './UrlPreviewCard.css';
import { tryDecodeURIComponent } from '../../utils/dom';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { ImageViewer } from '../image-viewer';
+import { onEnterOrSpace } from '../../utils/keyboard';
const linkStyles = { color: color.Success.Main };
-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
+export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
+ ({ url, ts, ...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])
);
- 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 && (
+ onEnterOrSpace(() => setViewer(true))(evt)}
+ onClick={() => setViewer(true)}
+ />
+ )}
+ {imgUrl && (
+ {
+ setViewer(false);
+ }}
+ renderViewer={(p) => }
+ />
+ )}
+
+
+ {typeof prev['og:site_name'] === 'string' && `${prev['og:site_name']} | `}
+ {tryDecodeURIComponent(url)}
+
+
+ {prev['og:title']}
+
+
+ {prev['og:description']}
+
+
+ >
+ );
+ };
return (
- <>
- {thumbUrl && (
- setViewer(true)}
- />
+
+ {previewStatus.status === AsyncStatus.Success ? (
+ renderContent(previewStatus.data)
+ ) : (
+
+
+
)}
- {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);