# Welcome to Tiptap Comments

Tiptap Comments allows you to annotate and discuss your Tiptap documents via Threads.

Feel free to try it out in this demo by selecting a slice of content and clicking on the **Add comment** button.

## Comments features

- Add inline, document, or sidebar comments
- Comments on text, nodes, custom nodes, or across a selection of nodes
- Rich text support within comments (e.g., bold, emojis)
- Resolve, edit, or delete comments
- Offline commenting support
- Handle overlapping comments
- Mention users directly within comments
- Webhooks to trigger custom notifications services when users are mentioned
- Programmatically manage comments using the Comments API

```javascript
import './styles.scss'

import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCaret } from '@tiptap/extension-collaboration-caret'
import Image from '@tiptap/extension-image'
import Placeholder from '@tiptap/extension-placeholder'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { CommentsKit, hoverOffThread, hoverThread } from '@tiptap-pro/extension-comments'
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import { useCallback, useRef, useState } from 'react'
import { v4 as uuid } from 'uuid'
import * as Y from 'yjs'

import { fromBase64String } from '../demo-setup.ts'
import { initialContent } from '../initialContent.ts'
import { ThreadsList } from './components/ThreadsList.jsx'
import { ThreadsProvider } from './context.jsx'
import { NodeViewExtension } from './extensions.jsx'
import { useThreads } from './hooks/useThreads.jsx'
import { useUser } from './hooks/useUser.jsx'

const doc = new Y.Doc()

const isDev = import.meta.env.MODE === 'development'
const id = isDev ? 'dev' : uuid()

const provider = new TiptapCollabProvider({
  appId: '7j9y6m10',
  name: `tiptap-comments-demo/${id}`,
  document: doc,
})

// apply initial content
const initialBinary = fromBase64String(initialContent)

Y.applyUpdate(provider.document, initialBinary)

export default () => {
  const [showUnresolved, setShowUnresolved] = useState(true)
  const [selectedThread, setSelectedThread] = useState(null)
  const threadsRef = useRef([])
  const [selection, setSelection] = useState(null)

const user = useUser()

const editor = useEditor({
    onSelectionUpdate: ({ editor: currentEditor }) => setSelection(currentEditor.state.selection),
    extensions: [
      StarterKit.configure({
        undoRedo: false,
      }),
      Image,
      Collaboration.configure({
        document: doc,
      }),
      CollaborationCaret.configure({
        provider,
        user: {
          name: user.name,
          color: user.color,
        },
      }),
      CommentsKit.configure({
        provider,
        onClickThread: threadId => {
          const isResolved = threadsRef.current.find(t => t.id === threadId)?.resolvedAt

if (!threadId || isResolved) {
            setSelectedThread(null)
            editor.chain().unselectThread().run()
            return
          }

setSelectedThread(threadId)
          editor.chain().selectThread({ id: threadId, updateSelection: false }).run()
        },
      }),
      Placeholder.configure({
        placeholder: 'Write a text to add comments …',
      }),
      NodeViewExtension,
    ],
  })

const { threads = [], createThread } = useThreads(provider, editor, user)

threadsRef.current = threads

const selectThreadInEditor = useCallback(
    threadId => {
      editor.chain().selectThread({ id: threadId }).run()
    },
    [editor],
  )

const deleteThread = useCallback(
    threadId => {
      provider.deleteThread(threadId)
      editor.commands.removeThread({ id: threadId })
    },
    [editor],
  )

const resolveThread = useCallback(
    threadId => {
      editor.commands.resolveThread({ id: threadId })
    },
    [editor],
  )

const unresolveThread = useCallback(
    threadId => {
      editor.commands.unresolveThread({ id: threadId })
    },
    [editor],
  )

const updateComment = useCallback(
    (threadId, commentId, content, metaData) => {
      editor.commands.updateComment({
        threadId,
        id: commentId,
        content,
        data: metaData,
      })
    },
    [editor],
  )

const onHoverThread = useCallback(
    threadId => {
      hoverThread(editor, [threadId])
    },
    [editor],
  )

const onLeaveThread = useCallback(() => {
    hoverOffThread(editor)
  }, [editor])

if (!editor) {
    return null
  }

const filteredThreads = threads.filter(t => (showUnresolved ? !t.resolvedAt : !!t.resolvedAt))

return (
    <ThreadsProvider
      onClickThread={selectThreadInEditor}
      onDeleteThread={deleteThread}
      onHoverThread={onHoverThread}
      onLeaveThread={onLeaveThread}
      onResolveThread={resolveThread}
      onUpdateComment={updateComment}
      onUnresolveThread={unresolveThread}
      selectedThreads={editor.storage.comments.focusedThreads}
      selectedThread={selectedThread}
      setSelectedThread={setSelectedThread}
      threads={threads}
    >
      <div className="col-group" data-viewmode={showUnresolved ? 'open' : 'resolved'}>
        <div className="main">
          <div className="control-group">
            <div className="button-group">
              <button type="button" onClick={createThread} disabled={!selection || selection.empty}>
                Add comment
              </button>
              <button
                type="button"
                onClick={() => editor.chain().focus().setImage({ src: 'https://placehold.co/800x500' }).run()}
              >
                Add image
              </button>
              <button type="button" onClick={() => editor.chain().focus().insertNodeView().run()}>
                Add node view
              </button>
            </div>
          </div>
          <EditorContent editor={editor} />
        </div>
        <div className="sidebar">
          <div className="sidebar-options">
            <div className="option-group">
              <div className="label-large">Comments</div>
              <div className="switch-group">
                <label>
                  <input
                    type="radio"
                    name="thread-state"
                    onChange={() => setShowUnresolved(true)}
                    checked={showUnresolved}
                  />
                  Open
                </label>
                <label>
                  <input
                    type="radio"
                    name="thread-state"
                    onChange={() => setShowUnresolved(false)}
                    checked={!showUnresolved}
                  />
                  Resolved
                </label>
              </div>
            </div>
            <ThreadsList provider={provider} threads={filteredThreads} />
          </div>
        </div>
      </div>
    </ThreadsProvider>
  )
}
```
