uchill/front_material/components/board/WhiteboardIframe.tsx

144 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useRef } from 'react';
interface WhiteboardIframeProps {
boardId: string;
username?: string;
token?: string;
/** Только ментор может очищать холст. */
isMentor?: boolean;
onToggleView?: () => void;
showingBoard?: boolean;
className?: string;
style?: React.CSSProperties;
}
/**
* Интерактивная доска Excalidraw + Yjs.
* Iframe создаётся императивно один раз — React не пересоздаёт его при переключении.
*/
export function WhiteboardIframe({
boardId,
username = 'Пользователь',
token: tokenProp,
isMentor = false,
showingBoard = true,
className = '',
style = {},
}: WhiteboardIframeProps) {
const containerRef = useRef<HTMLDivElement>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const createdRef = useRef(false);
useEffect(() => {
if (typeof window === 'undefined' || !containerRef.current || !boardId) return;
if (createdRef.current) return;
const token = tokenProp ?? localStorage.getItem('access_token') ?? '';
const apiBase = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8123/api';
const apiUrl = apiBase.replace(/\/api\/?$/, '') || 'http://127.0.0.1:8123';
// Вариант 1: отдельный URL доски (как LiveKit) — если app.uchill.online отдаёт один Next без nginx по путям
const excalidrawBaseUrl = (process.env.NEXT_PUBLIC_EXCALIDRAW_URL || '').trim().replace(/\/?$/, '');
const url: URL = excalidrawBaseUrl
? new URL(excalidrawBaseUrl.startsWith('http') ? excalidrawBaseUrl + '/' : `${window.location.origin}/${excalidrawBaseUrl}/`)
: (() => {
const path = process.env.NEXT_PUBLIC_EXCALIDRAW_PATH || '';
if (path) {
return new URL((path.startsWith('/') ? path : '/' + path) + '/', window.location.origin);
}
return new URL('/', `${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_EXCALIDRAW_PORT || '3001'}`);
})();
url.searchParams.set('boardId', boardId);
url.searchParams.set('apiUrl', apiUrl);
// Yjs WebSocket порт: 1236 (внешний порт yjs-whiteboard контейнера) или через nginx прокси
const yjsPort = process.env.NEXT_PUBLIC_YJS_PORT || '1236';
url.searchParams.set('yjsPort', yjsPort);
if (token) url.searchParams.set('token', token);
if (isMentor) url.searchParams.set('isMentor', '1');
const iframeSrc = excalidrawBaseUrl && excalidrawBaseUrl.startsWith('http')
? url.toString()
: `${url.origin}${url.pathname.replace(/\/?$/, '/')}?${url.searchParams.toString()}`;
const iframe = document.createElement('iframe');
iframe.src = iframeSrc;
iframe.style.cssText = 'width:100%;height:100%;border:none;display:block';
iframe.title = 'Интерактивная доска (Excalidraw)';
iframe.setAttribute('allow', 'camera; microphone; fullscreen');
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-modals');
const decodeName = (raw: string): string => {
if (!raw || typeof raw !== 'string') return raw;
try {
if (/%[0-9A-Fa-f]{2}/.test(raw)) return decodeURIComponent(raw);
return raw;
} catch {
return raw;
}
};
const sendUsername = () => {
if (iframe.contentWindow) {
try {
iframe.contentWindow.postMessage(
{ type: 'excalidraw-username', username: decodeName(username) },
url.origin
);
} catch (_) {}
}
};
iframe.onload = () => {
sendUsername();
setTimeout(sendUsername, 500);
};
containerRef.current.appendChild(iframe);
iframeRef.current = iframe;
createdRef.current = true;
setTimeout(sendUsername, 300);
return () => {
createdRef.current = false;
iframeRef.current = null;
iframe.remove();
};
}, [boardId, tokenProp, isMentor]);
useEffect(() => {
if (!iframeRef.current?.contentWindow || !createdRef.current) return;
const excalidrawBaseUrl = (process.env.NEXT_PUBLIC_EXCALIDRAW_URL || '').trim().replace(/\/?$/, '');
const targetOrigin = excalidrawBaseUrl.startsWith('http')
? new URL(excalidrawBaseUrl).origin
: (() => {
const path = process.env.NEXT_PUBLIC_EXCALIDRAW_PATH || '';
if (path) return window.location.origin;
return `${window.location.protocol}//${window.location.hostname}:${process.env.NEXT_PUBLIC_EXCALIDRAW_PORT || '3001'}`;
})();
const decodeName = (raw: string): string => {
if (!raw || typeof raw !== 'string') return raw;
try {
if (/%[0-9A-Fa-f]{2}/.test(raw)) return decodeURIComponent(raw);
return raw;
} catch {
return raw;
}
};
try {
iframeRef.current.contentWindow.postMessage(
{ type: 'excalidraw-username', username: decodeName(username) },
targetOrigin
);
} catch (_) {}
}, [username]);
return (
<div
ref={containerRef}
className={className}
style={{ width: '100%', height: '100%', position: 'relative', ...style }}
/>
);
}