fix: GIF CSP + edit history HTML rendering + unhandled rejection cleanup

- nginx (LXC 106, live): added https://*.giphy.com to connect-src CSP —
  browser was blocking fetch() to media2.giphy.com CDN with CSP violation
- EditHistoryModal: render formatted_body as sanitized HTML (via
  html-react-parser + sanitizeCustomHtml) with linkification for plain
  text, matching how messages render in the timeline
- useAsyncCallback + ThumbnailContent + ImageContent + VideoContent +
  ClientConfigLoader: use .catch(() => undefined) instead of void to
  silence unhandled promise rejections from fire-and-forget useEffect
  calls — errors already captured in AsyncState.Error for UI display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:34:46 -04:00
parent 7bafefa5e7
commit 7b01cfebf5
6 changed files with 25 additions and 18 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ export function ClientConfigLoader({ fallback, error, children }: ClientConfigLo
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
useEffect(() => {
void load().catch(() => undefined);
load().catch(() => undefined);
}, [load]);
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
@@ -113,7 +113,7 @@ export const ImageContent = as<'div', ImageContentProps>(
};
useEffect(() => {
if (autoPlay) void loadSrc().catch(() => undefined);
if (autoPlay) loadSrc().catch(() => undefined);
}, [autoPlay, loadSrc]);
return (
@@ -37,7 +37,7 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
);
useEffect(() => {
void loadThumbSrc().catch(() => undefined);
loadThumbSrc().catch(() => undefined);
}, [loadThumbSrc]);
return thumbSrcState.status === AsyncStatus.Success ? renderImage(thumbSrcState.data) : null;
@@ -106,7 +106,7 @@ export const VideoContent = as<'div', VideoContentProps>(
};
useEffect(() => {
if (autoPlay) void loadSrc().catch(() => undefined);
if (autoPlay) loadSrc().catch(() => undefined);
}, [autoPlay, loadSrc]);
return (
@@ -1,4 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import React, { ReactNode, useCallback, useEffect } from 'react';
import parse from 'html-react-parser';
import Linkify from 'linkify-react';
import FocusTrap from 'focus-trap-react';
import {
Box,
@@ -18,6 +20,8 @@ import {
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { stopPropagation } from '../../../utils/keyboard';
import { sanitizeCustomHtml } from '../../../utils/sanitize';
import { LINKIFY_OPTS } from '../../../plugins/react-custom-html-parser';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { timeDayMonYear, timeHourMinute } from '../../../utils/time';
import { useSetting } from '../../../state/hooks/settings';
@@ -52,17 +56,24 @@ function isRawEditEvent(raw: unknown): raw is RawEditEvent {
return typeof r.event_id === 'string' && typeof r.origin_server_ts === 'number';
}
function getVersionBody(evt: MatrixEvent): string {
function getVersionContent(evt: MatrixEvent): ReactNode {
const content = evt.getContent();
const newContent = content['m.new_content'] as Record<string, unknown> | undefined;
const source = newContent ?? content;
const format = source.format;
const formattedBody = source.formatted_body;
if (typeof formattedBody === 'string') {
return formattedBody.replace(/<[^>]+>/g, '').trim() || '(no text)';
if (
format === 'org.matrix.custom.html' &&
typeof formattedBody === 'string' &&
formattedBody.trim()
) {
return parse(sanitizeCustomHtml(formattedBody));
}
const body = source.body;
return typeof body === 'string' ? body : '(no text)';
const text = typeof body === 'string' ? body : '(no text)';
return <Linkify options={LINKIFY_OPTS}>{text}</Linkify>;
}
export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProps) {
@@ -106,7 +117,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
);
useEffect(() => {
void fetchHistory().catch(() => undefined);
fetchHistory().catch(() => undefined);
}, [fetchHistory]);
const formatTs = (ts: number): string => {
@@ -115,11 +126,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
return `${date} at ${time}`;
};
const originalBody = (() => {
const content = mEvent.getContent();
const body = content.body;
return typeof body === 'string' ? body : '(no text)';
})();
const originalContent = getVersionContent(mEvent);
const originalTs = mEvent.getTs();
@@ -189,7 +196,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
</Text>
</Box>
<Text size="T300" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{originalBody}
{originalContent}
</Text>
</Box>
@@ -209,7 +216,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
size="T300"
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
>
{getVersionBody(editEvt)}
{getVersionContent(editEvt)}
</Text>
</Box>
))}
+1 -1
View File
@@ -114,7 +114,7 @@ export const useAsyncCallbackValue = <TData, TError>(
const [state, load] = useAsyncCallback<TData, TError, []>(asyncCallback);
useEffect(() => {
void load().catch(() => undefined);
load().catch(() => undefined);
}, [load]);
return [state, load];