import React, { FormEventHandler, useState } from 'react'; import { Box, Button, Header, Icon, IconButton, Icons, Input, Scroll, Spinner, Text, Tooltip, TooltipProvider, color, config, } from 'folds'; import { Room } from 'matrix-js-sdk'; import classNames from 'classnames'; import * as css from './WidgetsPanel.css'; import { ContainerColor } from '../../../styles/ContainerColor.css'; import { RoomWidgetView } from './RoomWidgetView'; import { useRoomWidgets } from './useRoomWidgets'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { usePowerLevelsContext } from '../../../hooks/usePowerLevels'; import { useRoomCreators } from '../../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../../hooks/useRoomPermissions'; import { StateEvent } from '../../../../types/matrix/room'; import { generateWidgetId, validateWidgetUrl, WidgetUrlError } from './widgetUtils'; const urlErrorMessage = (err: WidgetUrlError): string => { switch (err) { case 'empty': return 'Enter a widget URL.'; case 'not-https': return 'Widget URLs must use https.'; case 'same-origin': return 'That URL is not allowed (it is on this app’s own origin).'; default: return 'That is not a valid URL.'; } }; type WidgetsPanelProps = { room: Room; requestClose: () => void; }; export function WidgetsPanel({ room, requestClose }: WidgetsPanelProps) { const mx = useMatrixClient(); const widgets = useRoomWidgets(room); const powerLevels = usePowerLevelsContext(); const creators = useRoomCreators(room); const permissions = useRoomPermissions(creators, powerLevels); const canModify = permissions.stateEvent(StateEvent.Widget, mx.getSafeUserId()); const [viewingId, setViewingId] = useState(null); const [adding, setAdding] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(); const viewing = widgets.find((w) => w.id === viewingId) ?? null; const handleAdd: FormEventHandler = async (evt) => { evt.preventDefault(); const target = evt.target as HTMLFormElement; const nameInput = target.elements.namedItem('widgetName') as HTMLInputElement | null; const urlInput = target.elements.namedItem('widgetUrl') as HTMLInputElement | null; if (!urlInput) return; const urlErr = validateWidgetUrl(urlInput.value, window.location.origin); if (urlErr) { setError(urlErrorMessage(urlErr)); return; } setError(undefined); setSaving(true); const id = generateWidgetId(); const content = { id, type: 'm.custom', url: urlInput.value.trim(), name: nameInput?.value.trim() || 'Widget', creatorUserId: mx.getSafeUserId(), data: {}, }; try { // eslint-disable-next-line @typescript-eslint/no-explicit-any await mx.sendStateEvent(room.roomId, StateEvent.Widget as any, content as any, id); setAdding(false); } catch (e) { setError((e as Error).message); } finally { setSaving(false); } }; const handleRemove = (id: string) => { if (viewingId === id) setViewingId(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any mx.sendStateEvent(room.roomId, StateEvent.Widget as any, {} as any, id).catch(() => undefined); }; return (
Widgets {room.name} Close } > {(triggerRef) => ( )}
{viewing ? ( ) : ( {widgets.length === 0 && ( No widgets in this room yet. )} {widgets.map((widget) => ( setViewingId(widget.id)} style={{ cursor: 'pointer', minWidth: 0 }} > {widget.name || widget.templateUrl} {canModify && ( handleRemove(widget.id)} > )} ))} {canModify && (adding ? ( ) : ( ))} {error && ( {error} )} )}
); }