fix(embeds): decode leftover HTML entities in link-preview text (#187 DP17)
CI / Build & Quality Checks (push) Successful in 4m16s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
CI / Build & Quality Checks (push) Successful in 4m16s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 8s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
Steam double-encodes its meta tags (`&quot;`), so after Synapse's single decode the card showed "The "Perpetual Testing Initiative" …". The preview's og:title / og:description / og:site_name are now decoded once where the preview is fetched, so every card (about 20 read those fields directly) gets clean text. Rendered as React text only, so decoding can't inject markup; exactly one level is decoded. Verified on the Portal 2 store link: "The \"Perpetual Testing Initiative\" has been expanded…", no literal " left on the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e06600afe1
commit
cccd78fd43
@@ -17,6 +17,7 @@ import {
|
||||
import { ImageOverlay } from '../ImageOverlay';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { decodeHtmlEntities } from '../../utils/htmlEntities';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview';
|
||||
import {
|
||||
@@ -308,6 +309,23 @@ function isTenor(url: string): boolean {
|
||||
// Synapse's thumbnailer flattens animated images to a single still frame, so a
|
||||
// GIF served from /thumbnail renders but never plays. Detect GIF previews so the
|
||||
// card can point at /download (the original) instead.
|
||||
/** Text fields that are rendered as-is by the cards below. */
|
||||
const PREVIEW_TEXT_KEYS = ['og:title', 'og:description', 'og:site_name'] as const;
|
||||
|
||||
/**
|
||||
* Decode character references left in preview text once, before any card
|
||||
* reads it (e.g. Steam double-encodes, so `"` survived Synapse's decode
|
||||
* and showed literally). #187 DP17.
|
||||
*/
|
||||
const decodePreviewText = (prev: IPreviewUrlResponse): IPreviewUrlResponse => {
|
||||
const out: IPreviewUrlResponse = { ...prev };
|
||||
PREVIEW_TEXT_KEYS.forEach((key) => {
|
||||
const value = out[key];
|
||||
if (typeof value === 'string') out[key] = decodeHtmlEntities(value);
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
function isGifPreview(url: string, prev: IPreviewUrlResponse): boolean {
|
||||
if (prev['og:image:type'] === 'image/gif') return true;
|
||||
try {
|
||||
@@ -2281,8 +2299,8 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [viewer, setViewer] = useState(false);
|
||||
const [previewStatus, loadPreview] = useAsyncCallback(
|
||||
useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx]),
|
||||
const [previewStatus, loadPreview] = useAsyncCallback<IPreviewUrlResponse, Error, []>(
|
||||
useCallback(() => mx.getUrlPreview(url, ts).then(decodePreviewText), [url, ts, mx]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user