Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 156 lines 5.6 kB view raw
1/** 2 * Comments Sidebar UI — threaded comments rendering, reply/resolve/reopen 3 * actions, Yjs sync, and new-comment input wiring. 4 * 5 * Extracted from main.ts for decomposition. 6 */ 7 8import type * as Y from 'yjs'; 9import type { Editor } from '@tiptap/core'; 10import { 11 type CommentThread, 12 createThread, 13 addReply, 14 resolveThread, 15 reopenThread, 16 sortThreads, 17 generateCommentId, 18} from '../lib/comment-threads.js'; 19import { escapeHtml } from '../lib/ai-chat.js'; 20 21// ── Types ─────────────────────────────────────────────────── 22 23export interface CommentsSidebarDeps { 24 editor: Editor; 25 ydoc: Y.Doc; 26 userName: string; 27} 28 29export interface CommentsSidebarResult { 30 loadCommentsFromYjs: () => void; 31} 32 33// ── Comments Sidebar UI ───────────────────────────────────── 34 35export function wireCommentsSidebar(deps: CommentsSidebarDeps): CommentsSidebarResult { 36 const { editor, ydoc, userName } = deps; 37 38 const commentsSidebar = document.getElementById('comments-sidebar') as HTMLElement; 39 const commentsListEl = document.getElementById('comments-list') as HTMLElement; 40 const commentsSidebarClose = document.getElementById('comments-sidebar-close') as HTMLElement; 41 const commentNewInput = document.getElementById('comment-new-input') as HTMLInputElement; 42 const btnComments = document.getElementById('btn-comments') as HTMLElement; 43 44 let commentThreads: CommentThread[] = []; 45 46 function renderCommentThreads(): void { 47 const sorted = sortThreads(commentThreads, 'unresolved'); 48 if (sorted.length === 0) { 49 commentsListEl.innerHTML = '<div style="padding:var(--space-sm);color:var(--color-text-faint);font-size:0.8rem;">No comments yet. Select text and click the comment button to start a thread.</div>'; 50 return; 51 } 52 commentsListEl.innerHTML = sorted.map(t => { 53 const allComments = [t.root, ...t.replies]; 54 return `<div class="comment-thread ${t.resolved ? 'resolved' : ''}" data-thread-id="${t.id}"> 55 ${allComments.map(c => `<div> 56 <span class="comment-author">${escapeHtml(c.author)}</span> 57 <span class="comment-time">${new Date(c.createdAt).toLocaleString()}</span> 58 <div class="comment-body">${escapeHtml(c.text)}</div> 59 </div>`).join('')} 60 <div class="comment-actions"> 61 ${t.resolved 62 ? `<button data-action="reopen" data-id="${t.id}">Reopen</button>` 63 : `<button data-action="resolve" data-id="${t.id}">Resolve</button>`} 64 <button data-action="reply" data-id="${t.id}">Reply</button> 65 </div> 66 </div>`; 67 }).join(''); 68 69 commentsListEl.querySelectorAll('[data-action]').forEach(btn => { 70 btn.addEventListener('click', () => { 71 const action = btn.getAttribute('data-action'); 72 const threadId = btn.getAttribute('data-id')!; 73 const idx = commentThreads.findIndex(t => t.id === threadId); 74 if (idx === -1) return; 75 76 if (action === 'resolve') { 77 commentThreads[idx] = resolveThread(commentThreads[idx], userName); 78 renderCommentThreads(); 79 syncCommentsToYjs(); 80 } else if (action === 'reopen') { 81 commentThreads[idx] = reopenThread(commentThreads[idx]); 82 renderCommentThreads(); 83 syncCommentsToYjs(); 84 } else if (action === 'reply') { 85 (async () => { 86 const { modalPrompt } = await import('../lib/modal-dialog.js'); 87 const text = await modalPrompt({ 88 title: 'Reply', 89 placeholder: 'Type your reply…', 90 okLabel: 'Reply', 91 }); 92 if (text) { 93 commentThreads[idx] = addReply(commentThreads[idx], userName, text); 94 renderCommentThreads(); 95 syncCommentsToYjs(); 96 } 97 })(); 98 } 99 }); 100 }); 101 } 102 103 function syncCommentsToYjs(): void { 104 ydoc.getMap('meta').set('commentThreads', JSON.stringify(commentThreads)); 105 } 106 107 function loadCommentsFromYjs(): void { 108 const raw = ydoc.getMap('meta').get('commentThreads') as string | undefined; 109 if (raw) { 110 try { commentThreads = JSON.parse(raw); } catch { commentThreads = []; } 111 } 112 renderCommentThreads(); 113 } 114 115 // Sync comments on yjs changes 116 ydoc.getMap('meta').observe(() => { loadCommentsFromYjs(); }); 117 118 btnComments.addEventListener('click', () => { 119 const showing = commentsSidebar.style.display !== 'none'; 120 commentsSidebar.style.display = showing ? 'none' : ''; 121 if (!showing) renderCommentThreads(); 122 }); 123 124 commentsSidebarClose.addEventListener('click', () => { 125 commentsSidebar.style.display = 'none'; 126 }); 127 128 // Add comment from sidebar input on Enter (requires text selection in editor) 129 commentNewInput.addEventListener('keydown', (e) => { 130 if (e.key !== 'Enter') return; 131 const text = commentNewInput.value.trim(); 132 if (!text) return; 133 134 const { from, to } = editor.state.selection; 135 if (from === to) { 136 commentNewInput.placeholder = 'Select text first...'; 137 return; 138 } 139 140 const anchorId = generateCommentId(); 141 editor.chain().focus().setComment({ 142 commentId: anchorId, 143 author: userName, 144 timestamp: new Date().toISOString(), 145 text, 146 }).run(); 147 148 const thread = createThread(anchorId, userName, text); 149 commentThreads.push(thread); 150 renderCommentThreads(); 151 syncCommentsToYjs(); 152 commentNewInput.value = ''; 153 }); 154 155 return { loadCommentsFromYjs }; 156}