diff --git a/element-web b/element-web new file mode 160000 index 0000000000..dd89cee328 --- /dev/null +++ b/element-web @@ -0,0 +1 @@ +Subproject commit dd89cee3288b68f0b70438a29d18afd17b3ea681 diff --git a/matrix-react-sdk b/matrix-react-sdk new file mode 160000 index 0000000000..b67a2303f9 --- /dev/null +++ b/matrix-react-sdk @@ -0,0 +1 @@ +Subproject commit b67a2303f912a8cab5caf089f3ea710850aa252c diff --git a/package.json b/package.json index 7316dccea2..140e50dad1 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@atlaskit/pragmatic-drag-and-drop": "1.1.6", "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0", "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3", + "@element-hq/element-call-embedded": "0.16.1", "@fontsource/inter": "4.5.14", "@tanstack/react-query": "5.24.1", "@tanstack/react-query-devtools": "5.24.1", @@ -54,7 +55,8 @@ "jotai": "2.6.0", "linkify-react": "4.1.3", "linkifyjs": "4.1.3", - "matrix-js-sdk": "38.2.0", + "matrix-js-sdk": "34.11.0", + "matrix-widget-api": "1.9.0", "millify": "6.1.0", "pdfjs-dist": "4.2.67", "prismjs": "1.30.0", diff --git a/src/app/components/widget/WidgetContainer.css.ts b/src/app/components/widget/WidgetContainer.css.ts new file mode 100644 index 0000000000..9681b8b4d7 --- /dev/null +++ b/src/app/components/widget/WidgetContainer.css.ts @@ -0,0 +1,61 @@ +import { style } from '@vanilla-extract/css'; +import { color, config } from 'folds'; + +export const WidgetOverlay = style({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 1000, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: config.space.S400, +}); + +export const WidgetContainer = style({ + position: 'relative', + width: '100%', + height: '100%', + maxWidth: '1400px', + maxHeight: '900px', + backgroundColor: color.Background.Container, + borderRadius: config.radii.R400, + overflow: 'hidden', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', + display: 'flex', + flexDirection: 'column', +}); + +export const WidgetHeader = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: config.space.S200, + borderBottom: `1px solid ${color.Background.ContainerLine}`, + backgroundColor: color.Background.Container, +}); + +export const WidgetTitle = style({ + fontWeight: 600, + fontSize: '14px', + color: color.Background.OnContainer, +}); + +export const WidgetIframe = style({ + flex: 1, + border: 'none', + width: '100%', + height: '100%', +}); + +export const LoadingContainer = style({ + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + gap: config.space.S300, +}); diff --git a/src/app/components/widget/WidgetContainer.tsx b/src/app/components/widget/WidgetContainer.tsx new file mode 100644 index 0000000000..c75c907b80 --- /dev/null +++ b/src/app/components/widget/WidgetContainer.tsx @@ -0,0 +1,96 @@ +/** + * Widget Container Component + * Displays widgets (like Element Call) in an iframe overlay + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { Box, IconButton, Icon, Icons, Text, Spinner } from 'folds'; +import { IApp } from '../../../types/widget'; +import * as css from './WidgetContainer.css'; + +export interface WidgetContainerProps { + widget: IApp; + onClose: () => void; + onLoad?: (iframe: HTMLIFrameElement) => void; +} + +export function WidgetContainer({ widget, onClose, onLoad }: WidgetContainerProps) { + const iframeRef = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [onClose]); + + // Initialize widget API as soon as iframe is mounted + // This prevents race conditions where the widget sends capabilities request before we're listening + useEffect(() => { + if (iframeRef.current && onLoad) { + onLoad(iframeRef.current); + } + }, [onLoad]); + + const handleIframeLoad = () => { + setIsLoading(false); + setError(null); + }; + + const handleIframeError = () => { + setIsLoading(false); + setError('Failed to load widget'); + }; + + const handleOverlayClick = (e: React.MouseEvent) => { + // Close if clicking the overlay background (not the container) + if (e.target === e.currentTarget) { + onClose(); + } + }; + + return ( +
+
+
+ {widget.name || 'Widget'} + + + +
+ + {isLoading && ( +
+ + Loading {widget.name}... +
+ )} + + {error && ( +
+ + {error} +
+ )} + +