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
}