import React, { useCallback, useMemo, useState } from 'react'; import { Box, Button, Input, Text, config } from 'folds'; import { MatrixClient } from 'matrix-js-sdk'; import { PaarrotUiActionContent, PaarrotUiButtonNode, PaarrotUiContent, PaarrotUiNode, } from '../../../types/matrix/room'; import { mxcUrlToHttp } from '../../utils/matrix'; import { isAllowedRoomAppImageSrc, scopeRoomAppCss } from './sanitizeRoomAppCss'; import * as css from './RoomAppView.css'; const SCOPE = '[data-room-app]'; type RoomAppSchemaProps = { mx: MatrixClient; content: PaarrotUiContent; uiEventId?: string; useAuthentication: boolean; onAction: (payload: PaarrotUiActionContent) => void | Promise; }; function fieldName(node: { id: string; name?: string }): string { return node.name || node.id; } function spacerSize(size?: number): 'sm' | 'md' | 'lg' { if (typeof size === 'number' && size <= 8) return 'sm'; if (typeof size === 'number' && size >= 24) return 'lg'; return 'md'; } export function RoomAppSchema({ mx, content, uiEventId, useAuthentication, onAction, }: RoomAppSchemaProps) { const [values, setValues] = useState>({}); const [sending, setSending] = useState(false); const scopedCss = useMemo( () => (content.css ? scopeRoomAppCss(content.css, SCOPE) : ''), [content.css] ); const setField = useCallback((name: string, value: string) => { setValues((prev) => ({ ...prev, [name]: value })); }, []); const handleButton = useCallback( async (node: PaarrotUiButtonNode) => { if (node.disabled || sending) return; setSending(true); try { await onAction({ action: node.action, component_id: node.id, value: node.value, values: { ...values }, ui_event_id: uiEventId, }); } finally { setSending(false); } }, [onAction, sending, uiEventId, values] ); const renderNode = (node: PaarrotUiNode): React.ReactNode => { switch (node.type) { case 'panel': return (
{node.children?.map((child) => renderNode(child))}
); case 'row': return (
{node.children?.map((child) => renderNode(child))}
); case 'text': return ( {node.text ?? ''} ); case 'button': { const variant = node.variant ?? 'Primary'; return ( ); } case 'input': { const name = fieldName(node); const current = values[name] ?? node.value ?? ''; return (
{node.label && ( {node.label} )} setField(name, evt.currentTarget.value)} />
); } case 'select': { const name = fieldName(node); const current = values[name] ?? node.value ?? ''; return (
{node.label && ( {node.label} )}
); } case 'image': { if (!isAllowedRoomAppImageSrc(node.src)) return null; const httpSrc = node.src.startsWith('mxc://') ? mxcUrlToHttp(mx, node.src, useAuthentication) ?? undefined : node.src; if (!httpSrc) return null; return ( {node.alt ); } case 'spacer': return
; default: return null; } }; return (
{scopedCss ? : null} {renderNode(content.root)}
); }