import React, { ReactNode, useEffect, useRef, useState } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { Box, Button, Text, config } from 'folds'; import { Editor, Transforms } from 'slate'; import { RoomInputPlaceholder } from './RoomInputPlaceholder'; import { copyToClipboard } from '../../utils/dom'; /** Two failures within this window show the notice instead of recovering again. */ const SILENT_RETRY_WINDOW_MS = 5000; type CapturedError = { message: string; details: string }; /** * Error boundary around a message composer. * * The composer's known failure mode is a transient render error right after an * autocomplete insert (slate-react's DOM selection sync racing the model), and * the draft is intact when it happens. So the first failure recovers silently: * the selection is cleared and the composer remounts with the same content. * Only a second failure within a few seconds shows the notice, which offers * "Reload composer" and "Copy details" (the error and component stack), so the * underlying bug can be diagnosed. Every failure is also logged to the console. */ export function ComposerErrorBoundary({ editor, children, }: { editor: Editor; children: ReactNode; }) { const lastSilentReset = useRef(0); const captured = useRef(undefined); const clearSelection = () => { try { Transforms.deselect(editor); } catch { /* editor already in a safe state */ } }; return ( { const message = error instanceof Error ? error.message : String(error); const details = [ `When: ${new Date().toISOString()}`, `Error: ${error instanceof Error ? `${error.name}: ${message}` : message}`, error instanceof Error && error.stack ? `Stack:\n${error.stack}` : '', info.componentStack ? `Component stack:${info.componentStack}` : '', `Browser: ${navigator.userAgent}`, ] .filter(Boolean) .join('\n'); captured.current = { message, details }; console.error('[composer] render error (draft kept):', error, info.componentStack); }} fallbackRender={(props) => ( )} > {children} ); } function ComposerFallback({ resetErrorBoundary, captured, lastSilentReset, }: FallbackProps & { captured?: CapturedError; lastSilentReset: React.MutableRefObject; }) { const silent = Date.now() - lastSilentReset.current > SILENT_RETRY_WINDOW_MS; const [copied, setCopied] = useState(false); useEffect(() => { if (!silent) return; lastSilentReset.current = Date.now(); resetErrorBoundary(); }, [silent, resetErrorBoundary, lastSilentReset]); if (silent) return null; return ( The message composer hit a snag. Your draft is safe — reload the composer to keep typing. {captured && ( )} ); }