/**
* 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 ``;
}).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 };
}