Commenting sidebar

A thread list panel beside the canvas: every page's comments, filterable, with a host-supplied toggle button.

import {
	CanvasComments,
	CanvasCommentsSidebar,
	CommentAuthor,
	CommentingContext,
	commentsSidebarOpen,
	commentToolOverrides,
	commentTools,
	getCommentThreads,
	putCommentRecords,
	toggleCommentsSidebar,
} from '@tldraw/commenting'
import { getLicenseKey } from '@tldraw/dotcom-shared'
import { useMemo } from 'react'
import {
	commentSchemaRecords,
	createComment,
	createCommentThread,
	createShapeId,
	createTLSchema,
	createTLStore,
	Editor,
	TLComponents,
	Tldraw,
	TldrawUiButton,
	TldrawUiButtonLabel,
	toRichText,
	useEditor,
	useValue,
} from 'tldraw'
import '@tldraw/commenting/commenting.css'
import 'tldraw/tldraw.css'

// A tiny local user directory so the list shows names instead of ids.
const AUTHORS: Record<string, CommentAuthor> = {
	me: { name: 'You', color: '#EC5E41' },
	ada: { name: 'Ada Lovelace', color: '#0E9F6E' },
	grace: { name: 'Grace Hopper', color: '#4465E9' },
}
const resolveAuthor = (id: string): CommentAuthor => AUTHORS[id] ?? { name: id }

// The canvas layer and the sidebar read the same context — who you are, and how author ids become
// names — so it's built once and spread into each rather than repeated on both.
const commenting: CommentingContext = { currentUserId: 'me', resolveAuthor }

// Two pages with a thread each (so the sidebar's every-page default and page labels have
// something to show) plus a resolved thread, hidden until "show resolved" is toggled on.
function seed(editor: Editor) {
	// Hot reload and remounts re-run onMount — never seed twice.
	if (getCommentThreads(editor).length > 0) return

	const pageOne = editor.getCurrentPageId()
	const boxOne = createShapeId('sidebar-box-one')
	editor.createShape({ id: boxOne, type: 'geo', x: 180, y: 140, props: { w: 320, h: 220 } })

	editor.createPage({ name: 'Page 2' })
	const pageTwo = editor.getPages()[1].id
	editor.setCurrentPage(pageTwo)
	const boxTwo = createShapeId('sidebar-box-two')
	editor.createShape({
		id: boxTwo,
		type: 'geo',
		x: 240,
		y: 200,
		props: { w: 280, h: 200, geo: 'ellipse' },
	})
	editor.setCurrentPage(pageOne)

	const now = Date.now()
	const records = [
		{
			pageId: pageOne,
			shapeId: boxOne,
			author: 'ada',
			text: 'Is this the final position for this box?',
			minutesAgo: 45,
		},
		{
			pageId: pageTwo,
			shapeId: boxTwo,
			author: 'grace',
			text: 'The ellipse reads better than the rectangle did.',
			minutesAgo: 20,
		},
	].flatMap(({ pageId, shapeId, author, text, minutesAgo }) => {
		const thread = createCommentThread({
			pageId,
			anchor: { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: false },
			createdBy: author,
		})
		const comment = createComment({
			threadId: thread.id,
			pageId,
			authorId: author,
			body: toRichText(text),
		})
		const createdAt = now - minutesAgo * 60_000
		return [
			{ ...thread, createdAt },
			{ ...comment, createdAt },
		]
	})

	const resolvedThread = createCommentThread({
		pageId: pageOne,
		anchor: { type: 'point', x: 620, y: 420 },
		createdBy: 'me',
	})
	const resolvedComment = createComment({
		threadId: resolvedThread.id,
		pageId: pageOne,
		authorId: 'me',
		body: toRichText('Fixed the spacing here.'),
	})
	records.push(
		{
			...resolvedThread,
			createdAt: now - 90 * 60_000,
			resolved: { at: now - 60 * 60_000, by: 'me' },
		},
		{ ...resolvedComment, createdAt: now - 90 * 60_000 }
	)

	putCommentRecords(editor, records)
	commentsSidebarOpen.set(editor, true)
}

// The host supplies the sidebar's open/close affordance. Here it's a button in the share-panel
// slot — the same top-right spot tldraw.com uses. (The comment tool closes the sidebar while
// it's active, so the button is how it comes back.)
function SidebarToggle() {
	const editor = useEditor()
	const open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])
	return (
		<div style={{ pointerEvents: 'all', display: 'flex', padding: 4 }}>
			<TldrawUiButton type="normal" onClick={() => toggleCommentsSidebar(editor)}>
				<TldrawUiButtonLabel>{open ? 'Hide comments' : 'Comments'}</TldrawUiButtonLabel>
			</TldrawUiButton>
		</div>
	)
}

export default function CommentingSidebarExample() {
	// Comments are stored as records in the editor's own store — registering the comment schema
	// records is all it takes to persist and sync them alongside shapes.
	const store = useMemo(
		() => createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
		[]
	)

	const components = useMemo<TLComponents>(
		() => ({
			InFrontOfTheCanvas: () => (
				<>
					<CanvasComments {...commenting} />
					<CanvasCommentsSidebar {...commenting} />
				</>
			),
			SharePanel: SidebarToggle,
		}),
		[]
	)

	return (
		<div className="tldraw__editor">
			<Tldraw
				licenseKey={getLicenseKey()}
				store={store}
				tools={commentTools}
				overrides={[commentToolOverrides]}
				components={components}
				onMount={seed}
			/>
		</div>
	)
}
Is this page helpful?
Prev
Multiplayer sync
Next
Comment clustering