import { track } from '@tldraw/state-react' import { TLInstancePresence } from '@tldraw/tlschema' import { useEffect, useRef, useState } from 'react' import { Editor } from '../editor/Editor' import { useEditorComponents } from '../hooks/EditorComponentsContext' import { useEditor } from '../hooks/useEditor' import { usePeerIds } from '../hooks/usePeerIds' import { usePresence } from '../hooks/usePresence' import { CollaboratorState, getCollaboratorStateFromElapsedTime, shouldShowCollaborator, } from '../utils/collaboratorState' export const LiveCollaborators = track(function Collaborators() { const peerIds = usePeerIds() return peerIds.map((id) => ) }) const CollaboratorGuard = track(function CollaboratorGuard({ collaboratorId, }: { collaboratorId: string }) { const editor = useEditor() const presence = usePresence(collaboratorId) const collaboratorState = useCollaboratorState(editor, presence) if (!(presence && presence.currentPageId === editor.getCurrentPageId())) { // No need to render if we don't have a presence or if they're on a different page return null } if (!shouldShowCollaborator(editor, presence, collaboratorState)) { return null } return }) const Collaborator = track(function Collaborator({ latestPresence, }: { latestPresence: TLInstancePresence }) { const editor = useEditor() const { CollaboratorBrush, CollaboratorScribble, CollaboratorCursor, CollaboratorHint, CollaboratorShapeIndicator, } = useEditorComponents() const zoomLevel = editor.getZoomLevel() const viewportPageBounds = editor.getViewportPageBounds() const { userId, chatMessage, brush, scribbles, selectedShapeIds, userName, cursor, color } = latestPresence if (!cursor) return null // Add a little padding to the top-left of the viewport // so that the cursor doesn't get cut off const isCursorInViewport = !( cursor.x < viewportPageBounds.minX - 12 / zoomLevel || cursor.y < viewportPageBounds.minY - 16 / zoomLevel || cursor.x > viewportPageBounds.maxX - 12 / zoomLevel || cursor.y > viewportPageBounds.maxY - 16 / zoomLevel ) return ( <> {brush && CollaboratorBrush ? ( ) : null} {isCursorInViewport && CollaboratorCursor ? ( ) : CollaboratorHint ? ( ) : null} {CollaboratorScribble && scribbles.length ? ( <> {scribbles.map((scribble) => ( ))} ) : null} {CollaboratorShapeIndicator && selectedShapeIds .filter((id) => { // Skip hidden shapes if (editor.isShapeHidden(id)) return false // Only render SVG indicators for shapes that use legacy indicators // Canvas-based indicators are handled by CanvasShapeIndicators const shape = editor.getShape(id) if (!shape) return false const util = editor.getShapeUtil(shape) return util.useLegacyIndicator() }) .map((shapeId) => ( ))} ) }) function useCollaboratorState( editor: Editor, latestPresence: TLInstancePresence | null ): CollaboratorState { const rLastActivityTimestamp = useRef(latestPresence?.lastActivityTimestamp ?? -1) const [state, setState] = useState(() => getCollaboratorStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current) ) useEffect(() => { const interval = editor.timers.setInterval(() => { setState( getCollaboratorStateFromElapsedTime(editor, Date.now() - rLastActivityTimestamp.current) ) }, editor.options.collaboratorCheckIntervalMs) return () => clearInterval(interval) }, [editor]) if (latestPresence) { // We can do this on every render, it's free and cheaper than an effect // remember, there can be lots and lots of cursors moving around all the time rLastActivityTimestamp.current = latestPresence.lastActivityTimestamp ?? Infinity } return state }