diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DrawBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DrawBlock.tsx index 853c44f0..0afb39ce 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DrawBlock.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/DrawBlock.tsx @@ -1,50 +1,126 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import { defaultProps, insertOrUpdateBlock } from '@blocknote/core'; +import { + ReactRendererProps, + defaultProps, + insertOrUpdateBlock, +} from '@blocknote/core'; import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react'; import { TFunction } from 'i18next'; -import React, { useEffect, useState } from 'react'; -import { css } from 'styled-components'; -import { Tldraw } from 'tldraw'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { + DefaultColorStyle, + Editor as TLDEditor, + TLGeoShape, + TLShapePartial, + Tldraw, + createShapeId, + toRichText, + useEditor, +} from 'tldraw'; import 'tldraw/tldraw.css'; -import { Box, BoxButton, Icon } from '@/components'; +import { Box, Icon } from '@/components'; +import { useProviderStore } from '@/features/docs/doc-management/stores/useProviderStore'; import { DocsBlockNoteEditor } from '../../types'; +/** + * Collaborative Draw block spec. + * + * We leverage BlockNote's `propSchema` to persist two key props: + * - `roomId`: A stable identifier so all participants load the same provider / yjs room. + * - `drawingData`: A frozen TLDraw snapshot when the provider is *not* connected (e.g. offline) or for history replay. + * + * TLDraw's multiplayer features are provided by our own `useProviderStore`, which memo‑izes a y‑websocket provider for every `roomId`. + */ export const DrawBlock = createReactBlockSpec( { type: 'draw', propSchema: { textAlignment: defaultProps.textAlignment, backgroundColor: defaultProps.backgroundColor, + /** + * Persisted TLDraw snapshot – serialised via the built‑in `store.getSnapshot()`. + * When a new collaborator first loads the document we hydrate the store from here + * before connecting to the yjs provider so local history is preserved. + */ + drawingData: { + default: null as ReturnType | null, + }, + /** + * A stable identifier for the collaborative room. We automatically generate one + * when the block is first inserted (see the slash‑menu helper). + */ + roomId: { + default: '', + }, }, content: 'inline', }, { - render: ({ block, editor, contentRef }) => { - //const [openEmojiPicker, setOpenEmojiPicker] = useState(false); + render: ({ block, editor }: ReactRendererProps<'draw'>) => { + const { roomId, drawingData } = block.props; - // // Temporary: sets a yellow background color to a draw block when added by - // // the user, while keeping the colors menu on the drag handler usable for - // // this custom block. - // useEffect(() => { - // if ( - // !block.content.length && - // block.props.backgroundColor === 'default' - // ) { - // editor.updateBlock(block, { props: { backgroundColor: 'yellow' } }); - // } - // }, [block, editor]); + /** + * Pull (or create) a provider & TLStore for this `roomId`. Internally this wires up + * a y‑websocket provider and binds it to TLDraw's store for real‑time sync. + */ + const { store, isSynced } = useProviderStore((state) => + state.getOrCreateTLDrawStore(roomId), + ); + + // Hold a local reference so we can access the editor instance inside callbacks. + const tldrawEditorRef = useRef(null); + const [isHydrated, setIsHydrated] = useState(false); + + /** + * When TLDraw mounts we hydrate the store *once* with any persisted snapshot. This means + * a fresh collaborator immediately sees the last known state even before the provider + * finishes syncing. + */ + const handleMount = useCallback( + (tldrEditor: TLDEditor) => { + tldrawEditorRef.current = tldrEditor; + + if (!isHydrated && drawingData) { + // Load the snapshot into the store. We intentionally do this before provider sync. + tldrEditor.store.loadSnapshot(drawingData); + setIsHydrated(true); + } + + // Observe local changes and push them back into the BlockNote block props so they are + // versioned inside the doc history ("undo" across different block types still works!) + const dispose = tldrEditor.store.listen(() => { + const snapshot = tldrEditor.store.getSnapshot(); + editor.updateBlock(block.id, { + props: { + ...block.props, + drawingData: snapshot, + }, + }); + }); + + return () => dispose(); + }, + [block.id, block.props, drawingData, editor, isHydrated], + ); return ( - + ); }, }, ); +/** + * Slash‑menu helper → inserts a new collaborative draw block with a unique `roomId`. + */ export const getDrawReactSlashMenuItems = ( editor: DocsBlockNoteEditor, t: TFunction<'translation', undefined>, @@ -55,15 +131,22 @@ export const getDrawReactSlashMenuItems = ( onItemClick: () => { insertOrUpdateBlock(editor, { type: 'draw', + props: { + roomId: `drawing-${Date.now()}`, + drawingData: null, + }, }); }, aliases: ['draw'], group, icon: , - subtext: t('Add a draw block'), + subtext: t('Add a collaborative canvas'), }, ]; +/** + * Formatting‑toolbar item so users can transform an existing block into a Draw block. + */ export const getDrawFormattingToolbarItems = ( t: TFunction<'translation', undefined>, ): BlockTypeSelectItem => ({