/** * Comments Sidebar UI — threaded comments rendering, reply/resolve/reopen * actions, Yjs sync, and new-comment input wiring. * * Extracted from main.ts for decomposition. */ import type * as Y from 'yjs'; import type { Editor } from '@tiptap/core'; import { type CommentThread, createThread, addReply, resolveThread, reopenThread, sortThreads, generateCommentId, } from '../lib/comment-threads.js'; import { escapeHtml } from '../lib/ai-chat.js'; // ── Types ─────────────────────────────────────────────────── export interface CommentsSidebarDeps { editor: Editor; ydoc: Y.Doc; userName: string; } export interface CommentsSidebarResult { loadCommentsFromYjs: () => void; } // ── Comments Sidebar UI ───────────────────────────────────── export function wireCommentsSidebar(deps: CommentsSidebarDeps): CommentsSidebarResult { const { editor, ydoc, userName } = deps; const commentsSidebar = document.getElementById('comments-sidebar') as HTMLElement; const commentsListEl = document.getElementById('comments-list') as HTMLElement; const commentsSidebarClose = document.getElementById('comments-sidebar-close') as HTMLElement; const commentNewInput = document.getElementById('comment-new-input') as HTMLInputElement; const btnComments = document.getElementById('btn-comments') as HTMLElement; let commentThreads: CommentThread[] = []; function renderCommentThreads(): void { const sorted = sortThreads(commentThreads, 'unresolved'); if (sorted.length === 0) { commentsListEl.innerHTML = '
No comments yet. Select text and click the comment button to start a thread.
'; return; } commentsListEl.innerHTML = sorted.map(t => { const allComments = [t.root, ...t.replies]; return `
${allComments.map(c => `
${escapeHtml(c.author)} ${new Date(c.createdAt).toLocaleString()}
${escapeHtml(c.text)}
`).join('')}
${t.resolved ? `` : ``}
`; }).join(''); commentsListEl.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', () => { const action = btn.getAttribute('data-action'); const threadId = btn.getAttribute('data-id')!; const idx = commentThreads.findIndex(t => t.id === threadId); if (idx === -1) return; if (action === 'resolve') { commentThreads[idx] = resolveThread(commentThreads[idx], userName); renderCommentThreads(); syncCommentsToYjs(); } else if (action === 'reopen') { commentThreads[idx] = reopenThread(commentThreads[idx]); renderCommentThreads(); syncCommentsToYjs(); } else if (action === 'reply') { (async () => { const { modalPrompt } = await import('../lib/modal-dialog.js'); const text = await modalPrompt({ title: 'Reply', placeholder: 'Type your reply…', okLabel: 'Reply', }); if (text) { commentThreads[idx] = addReply(commentThreads[idx], userName, text); renderCommentThreads(); syncCommentsToYjs(); } })(); } }); }); } function syncCommentsToYjs(): void { ydoc.getMap('meta').set('commentThreads', JSON.stringify(commentThreads)); } function loadCommentsFromYjs(): void { const raw = ydoc.getMap('meta').get('commentThreads') as string | undefined; if (raw) { try { commentThreads = JSON.parse(raw); } catch { commentThreads = []; } } renderCommentThreads(); } // Sync comments on yjs changes ydoc.getMap('meta').observe(() => { loadCommentsFromYjs(); }); btnComments.addEventListener('click', () => { const showing = commentsSidebar.style.display !== 'none'; commentsSidebar.style.display = showing ? 'none' : ''; if (!showing) renderCommentThreads(); }); commentsSidebarClose.addEventListener('click', () => { commentsSidebar.style.display = 'none'; }); // Add comment from sidebar input on Enter (requires text selection in editor) commentNewInput.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; const text = commentNewInput.value.trim(); if (!text) return; const { from, to } = editor.state.selection; if (from === to) { commentNewInput.placeholder = 'Select text first...'; return; } const anchorId = generateCommentId(); editor.chain().focus().setComment({ commentId: anchorId, author: userName, timestamp: new Date().toISOString(), text, }).run(); const thread = createThread(anchorId, userName, text); commentThreads.push(thread); renderCommentThreads(); syncCommentsToYjs(); commentNewInput.value = ''; }); return { loadCommentsFromYjs }; }