fix: GIF CSP + edit history HTML rendering + unhandled rejection cleanup
CI / Build & Quality Checks (push) Successful in 10m24s
CI / Build & Quality Checks (push) Successful in 10m24s
- 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:
@@ -21,7 +21,7 @@ export function ClientConfigLoader({ fallback, error, children }: ClientConfigLo
|
|||||||
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load().catch(() => undefined);
|
load().catch(() => undefined);
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const ImageContent = as<'div', ImageContentProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoPlay) void loadSrc().catch(() => undefined);
|
if (autoPlay) loadSrc().catch(() => undefined);
|
||||||
}, [autoPlay, loadSrc]);
|
}, [autoPlay, loadSrc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ThumbnailContent({ info, renderImage }: ThumbnailContentProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadThumbSrc().catch(() => undefined);
|
loadThumbSrc().catch(() => undefined);
|
||||||
}, [loadThumbSrc]);
|
}, [loadThumbSrc]);
|
||||||
|
|
||||||
return thumbSrcState.status === AsyncStatus.Success ? renderImage(thumbSrcState.data) : null;
|
return thumbSrcState.status === AsyncStatus.Success ? renderImage(thumbSrcState.data) : null;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const VideoContent = as<'div', VideoContentProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoPlay) void loadSrc().catch(() => undefined);
|
if (autoPlay) loadSrc().catch(() => undefined);
|
||||||
}, [autoPlay, loadSrc]);
|
}, [autoPlay, loadSrc]);
|
||||||
|
|
||||||
return (
|
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 FocusTrap from 'focus-trap-react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -18,6 +20,8 @@ import {
|
|||||||
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
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 { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { timeDayMonYear, timeHourMinute } from '../../../utils/time';
|
import { timeDayMonYear, timeHourMinute } from '../../../utils/time';
|
||||||
import { useSetting } from '../../../state/hooks/settings';
|
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';
|
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 content = evt.getContent();
|
||||||
const newContent = content['m.new_content'] as Record<string, unknown> | undefined;
|
const newContent = content['m.new_content'] as Record<string, unknown> | undefined;
|
||||||
const source = newContent ?? content;
|
const source = newContent ?? content;
|
||||||
|
|
||||||
|
const format = source.format;
|
||||||
const formattedBody = source.formatted_body;
|
const formattedBody = source.formatted_body;
|
||||||
if (typeof formattedBody === 'string') {
|
if (
|
||||||
return formattedBody.replace(/<[^>]+>/g, '').trim() || '(no text)';
|
format === 'org.matrix.custom.html' &&
|
||||||
|
typeof formattedBody === 'string' &&
|
||||||
|
formattedBody.trim()
|
||||||
|
) {
|
||||||
|
return parse(sanitizeCustomHtml(formattedBody));
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = source.body;
|
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) {
|
export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProps) {
|
||||||
@@ -106,7 +117,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchHistory().catch(() => undefined);
|
fetchHistory().catch(() => undefined);
|
||||||
}, [fetchHistory]);
|
}, [fetchHistory]);
|
||||||
|
|
||||||
const formatTs = (ts: number): string => {
|
const formatTs = (ts: number): string => {
|
||||||
@@ -115,11 +126,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
|
|||||||
return `${date} at ${time}`;
|
return `${date} at ${time}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalBody = (() => {
|
const originalContent = getVersionContent(mEvent);
|
||||||
const content = mEvent.getContent();
|
|
||||||
const body = content.body;
|
|
||||||
return typeof body === 'string' ? body : '(no text)';
|
|
||||||
})();
|
|
||||||
|
|
||||||
const originalTs = mEvent.getTs();
|
const originalTs = mEvent.getTs();
|
||||||
|
|
||||||
@@ -189,7 +196,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="T300" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
<Text size="T300" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
{originalBody}
|
{originalContent}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -209,7 +216,7 @@ export function EditHistoryModal({ room, mEvent, onClose }: EditHistoryModalProp
|
|||||||
size="T300"
|
size="T300"
|
||||||
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
||||||
>
|
>
|
||||||
{getVersionBody(editEvt)}
|
{getVersionContent(editEvt)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const useAsyncCallbackValue = <TData, TError>(
|
|||||||
const [state, load] = useAsyncCallback<TData, TError, []>(asyncCallback);
|
const [state, load] = useAsyncCallback<TData, TError, []>(asyncCallback);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load().catch(() => undefined);
|
load().catch(() => undefined);
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
return [state, load];
|
return [state, load];
|
||||||
|
|||||||
Reference in New Issue
Block a user