79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
|
|
import React, { useEffect, useRef, useState } from 'react';
|
||
|
|
import { Box, Icon, Icons, Text, color } from 'folds';
|
||
|
|
import { Room } from 'matrix-js-sdk';
|
||
|
|
import { ClientWidgetApi, Widget } from 'matrix-widget-api';
|
||
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||
|
|
import { GeneralWidgetDriver } from './GeneralWidgetDriver';
|
||
|
|
import { isWidgetUrlSafe } from './widgetUtils';
|
||
|
|
|
||
|
|
type RoomWidgetViewProps = {
|
||
|
|
room: Room;
|
||
|
|
widget: Widget;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Hosts one room widget in a sandboxed iframe via ClientWidgetApi (so widgets
|
||
|
|
// that wait on the client handshake load), with a conservative capability driver.
|
||
|
|
// Re-mounts only when the widget id or its (template) URL changes — not on every
|
||
|
|
// unrelated room-state update — so viewing a widget doesn't reload constantly.
|
||
|
|
export function RoomWidgetView({ room, widget }: RoomWidgetViewProps) {
|
||
|
|
const mx = useMatrixClient();
|
||
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
|
const widgetRef = useRef(widget);
|
||
|
|
widgetRef.current = widget;
|
||
|
|
const [blocked, setBlocked] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const container = containerRef.current;
|
||
|
|
if (!container) return undefined;
|
||
|
|
const current = widgetRef.current;
|
||
|
|
|
||
|
|
const completeUrl = current.getCompleteUrl({
|
||
|
|
currentUserId: mx.getSafeUserId(),
|
||
|
|
widgetRoomId: room.roomId,
|
||
|
|
deviceId: mx.getDeviceId() ?? undefined,
|
||
|
|
baseUrl: mx.baseUrl,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Security: never render a same-origin widget with allow-same-origin (a
|
||
|
|
// same-origin frame could break out of the sandbox against our own origin).
|
||
|
|
if (!isWidgetUrlSafe(completeUrl, window.location.origin)) {
|
||
|
|
setBlocked(true);
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
setBlocked(false);
|
||
|
|
|
||
|
|
const iframe = document.createElement('iframe');
|
||
|
|
iframe.title = current.name || 'Widget';
|
||
|
|
iframe.sandbox.value =
|
||
|
|
'allow-forms allow-scripts allow-same-origin allow-popups allow-downloads';
|
||
|
|
iframe.allow = 'autoplay; clipboard-write;';
|
||
|
|
iframe.src = completeUrl;
|
||
|
|
iframe.style.width = '100%';
|
||
|
|
iframe.style.height = '100%';
|
||
|
|
iframe.style.border = 'none';
|
||
|
|
container.append(iframe);
|
||
|
|
|
||
|
|
const clientApi = new ClientWidgetApi(current, iframe, new GeneralWidgetDriver());
|
||
|
|
clientApi.setViewedRoomId(room.roomId);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
clientApi.stop();
|
||
|
|
iframe.remove();
|
||
|
|
};
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
}, [mx, room.roomId, widget.id, widget.templateUrl]);
|
||
|
|
|
||
|
|
if (blocked) {
|
||
|
|
return (
|
||
|
|
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="200">
|
||
|
|
<Icon size="400" src={Icons.Warning} style={{ color: color.Warning.Main }} />
|
||
|
|
<Text size="T300" align="Center">
|
||
|
|
This widget can't be loaded because its URL is on this app's own origin.
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return <Box ref={containerRef} grow="Yes" style={{ height: '100%', minHeight: 0 }} />;
|
||
|
|
}
|