diff --git a/modules/rnotes/components/comment-panel.ts b/modules/rnotes/components/comment-panel.ts index 341bb3d..a237e02 100644 --- a/modules/rnotes/components/comment-panel.ts +++ b/modules/rnotes/components/comment-panel.ts @@ -119,50 +119,128 @@ class NotesCommentPanel extends HTMLElement { return `${Math.floor(diff / 86400000)}d ago`; }; const formatDate = (ts: number) => new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); - const { authorId: currentUserId } = this.getSessionInfo(); + const { authorId: currentUserId, authorName: currentUserName } = this.getSessionInfo(); + const initials = (name: string) => name.split(/\s+/).map(w => w[0] || '').join('').slice(0, 2).toUpperCase() || '?'; + const avatarColor = (id: string) => { + let h = 0; + for (let i = 0; i < id.length; i++) h = id.charCodeAt(i) + ((h << 5) - h); + return `hsl(${Math.abs(h) % 360}, 55%, 55%)`; + }; this.shadow.innerHTML = `
@@ -171,19 +249,43 @@ class NotesCommentPanel extends HTMLElement { ${threads.map(thread => { const reactions = thread.reactions || {}; const reactionEntries = Object.entries(reactions).filter(([, users]) => users.length > 0); + const isActive = thread.id === this._activeThreadId; + const hasMessages = thread.messages.length > 0; + const firstMsg = thread.messages[0]; + const authorName = firstMsg?.authorName || currentUserName; + const authorId = firstMsg?.authorId || currentUserId; + return ` -
+
- ${esc(thread.messages[0]?.authorName || 'Anonymous')} - ${timeAgo(thread.createdAt)} -
- ${thread.messages.map(msg => ` -
-
${esc(msg.authorName)}
-
${esc(msg.text)}
+
${initials(authorName)}
+
+ ${esc(authorName)} + ${timeAgo(thread.createdAt)}
- `).join('')} - ${thread.messages.length === 0 ? '
Click to add a comment...
' : ''} +
+ ${hasMessages ? ` +
${esc(firstMsg.text)}
+ ${thread.messages.slice(1).map(msg => ` +
+
+
${initials(msg.authorName)}
+ ${esc(msg.authorName)} + ${timeAgo(msg.createdAt)} +
+
${esc(msg.text)}
+
+ `).join('')} + ` : ` +
+ +
+ + +
+
+ `} + ${hasMessages && reactionEntries.length > 0 ? `
${reactionEntries.map(([emoji, users]) => ` @@ -193,19 +295,28 @@ class NotesCommentPanel extends HTMLElement {
${REACTION_EMOJIS.map(e => ``).join('')}
+ ` : ''} + ${hasMessages && thread.reminderAt ? `
- ${thread.reminderAt - ? `⏰ ${formatDate(thread.reminderAt)}` - : `` - } - + ⏰ ${formatDate(thread.reminderAt)} +
+ ` : ''} + ${hasMessages ? `
- +
+ +
+ ` : ''}
- + ${hasMessages ? ` + + + + ` : ''} +
`; @@ -214,12 +325,21 @@ class NotesCommentPanel extends HTMLElement { `; this.wireEvents(); + + // Auto-focus new comment textarea + requestAnimationFrame(() => { + const newInput = this.shadow.querySelector('.new-comment-input') as HTMLTextAreaElement; + if (newInput) newInput.focus(); + }); } private wireEvents() { // Click thread to scroll editor to it this.shadow.querySelectorAll('.thread[data-thread]').forEach(el => { el.addEventListener('click', (e) => { + // Don't handle clicks on inputs/buttons/textareas + const target = e.target as HTMLElement; + if (target.closest('input, textarea, button')) return; const threadId = (el as HTMLElement).dataset.thread; if (!threadId || !this._editor) return; this._activeThreadId = threadId; @@ -236,6 +356,46 @@ class NotesCommentPanel extends HTMLElement { }); }); + // New comment submit (thread with no messages yet) + this.shadow.querySelectorAll('[data-submit-new]').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const threadId = (btn as HTMLElement).dataset.submitNew; + if (!threadId) return; + const textarea = this.shadow.querySelector(`textarea[data-new-thread="${threadId}"]`) as HTMLTextAreaElement; + const text = textarea?.value?.trim(); + if (!text) return; + this.addReply(threadId, text); + }); + }); + + // New comment cancel — delete the empty thread + this.shadow.querySelectorAll('[data-cancel-new]').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const threadId = (btn as HTMLElement).dataset.cancelNew; + if (threadId) this.deleteThread(threadId); + }); + }); + + // New comment textarea — Ctrl+Enter to submit, Escape to cancel + this.shadow.querySelectorAll('.new-comment-input').forEach(textarea => { + textarea.addEventListener('keydown', (e) => { + const ke = e as KeyboardEvent; + if (ke.key === 'Enter' && (ke.ctrlKey || ke.metaKey)) { + e.stopPropagation(); + const threadId = (textarea as HTMLTextAreaElement).dataset.newThread; + const text = (textarea as HTMLTextAreaElement).value.trim(); + if (threadId && text) this.addReply(threadId, text); + } else if (ke.key === 'Escape') { + e.stopPropagation(); + const threadId = (textarea as HTMLTextAreaElement).dataset.newThread; + if (threadId) this.deleteThread(threadId); + } + }); + textarea.addEventListener('click', (e) => e.stopPropagation()); + }); + // Reply this.shadow.querySelectorAll('[data-reply]').forEach(btn => { btn.addEventListener('click', (e) => { diff --git a/modules/rnotes/components/folk-notes-app.ts b/modules/rnotes/components/folk-notes-app.ts index 8397615..ba5d9e8 100644 --- a/modules/rnotes/components/folk-notes-app.ts +++ b/modules/rnotes/components/folk-notes-app.ts @@ -1216,14 +1216,17 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF const useYjs = !isDemo && isEditable; this.contentZone.innerHTML = ` -

- - ${isEditable ? this.renderToolbar() : ''} -