Social Annotations in the Atmosphere
15
fork

Configure Feed

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

refactor: remove unfinished comments feature from annotation cards

Remove ~200 lines of unused code including comment forms, thread
collapse, and associated event listeners.

+1 -329
+1 -313
packages/core/src/components/annotation-card.ts
··· 1 1 import { Annotation } from '../types'; 2 - import { Comment } from '../pds/index'; 3 2 import { formatRelativeTime } from '../utils/date'; 4 3 import { escapeHtml } from '../utils/sanitize'; 5 4 ··· 68 67 height: 20px; 69 68 border-radius: 50%; 70 69 object-fit: cover; 71 - border: 1px dashed #d0d0d0; 72 70 } 73 71 74 72 .author-link { ··· 86 84 text-decoration: none; 87 85 font-weight: 500; 88 86 } 89 - 90 - /* Comments */ 91 - .comments-section { 92 - margin-top: 12px; 93 - padding-top: 12px; 94 - border-top: 1px dashed #e0e0e0; 95 - } 96 - 97 - .comments-header { 98 - display: flex; 99 - gap: 8px; 100 - margin-bottom: 8px; 101 - align-items: center; 102 - } 103 - 104 - button { 105 - background: transparent; 106 - border: 1px solid #d0d0d0; 107 - color: #666; 108 - font-size: 12px; 109 - padding: 4px 8px; 110 - border-radius: 2px; 111 - cursor: pointer; 112 - } 113 - 114 - button:hover { 115 - background: #f5f5f5; 116 - color: #333; 117 - } 118 - 119 - button.primary { 120 - background: #2d5016; 121 - color: white; 122 - border-color: #1f3810; 123 - } 124 - 125 - button.primary:hover { 126 - background: #1f3810; 127 - } 128 - 129 - .comments-list { 130 - display: flex; 131 - flex-direction: column; 132 - gap: 8px; 133 - margin-top: 8px; 134 - } 135 - 136 - .comment { 137 - padding: 8px; 138 - background: #fafafa; 139 - border: 1px solid #e0e0e0; 140 - border-radius: 2px; 141 - } 142 - 143 - .comment-content { 144 - display: flex; 145 - flex-direction: column; 146 - gap: 4px; 147 - } 148 - 149 - .comment-text { 150 - font-size: 13px; 151 - line-height: 1.4; 152 - color: #333; 153 - } 154 - 155 - .comment-meta { 156 - display: flex; 157 - gap: 8px; 158 - align-items: center; 159 - font-size: 11px; 160 - color: #999; 161 - } 162 - 163 - .reply-btn { 164 - border: none; 165 - color: #2d5016; 166 - padding: 0; 167 - background: transparent; 168 - } 169 - 170 - .reply-btn:hover { 171 - text-decoration: underline; 172 - background: transparent; 173 - } 174 - 175 - .comment-thread { 176 - margin-top: 8px; 177 - padding-left: 12px; 178 - position: relative; 179 - } 180 - 181 - .thread-toggle-btn { 182 - position: absolute; 183 - left: -4px; 184 - top: 0; 185 - border: none; 186 - width: 16px; 187 - height: 16px; 188 - padding: 0; 189 - display: flex; 190 - align-items: center; 191 - justify-content: center; 192 - } 193 - 194 - .comment-form, .reply-form { 195 - margin-top: 8px; 196 - padding: 8px; 197 - background: #fff; 198 - border: 1px dashed #d0d0d0; 199 - border-radius: 2px; 200 - } 201 - 202 - textarea { 203 - width: 100%; 204 - min-height: 60px; 205 - padding: 6px; 206 - border: 1px solid #d0d0d0; 207 - border-radius: 2px; 208 - font-family: inherit; 209 - font-size: 13px; 210 - margin-bottom: 6px; 211 - background: #fafafa; 212 - resize: vertical; 213 - } 214 - 215 - .form-actions { 216 - display: flex; 217 - gap: 6px; 218 - justify-content: flex-end; 219 - } 220 87 `; 221 88 222 89 export class SeamsAnnotationCard extends HTMLElement { 223 90 private _annotation: Annotation | null = null; 224 - private _comments: Comment[] = []; 225 - private _collapsedThreads: Set<string> = new Set(); // set of URIs 226 - private _activeReplyForms: Set<string> = new Set(); // set of URIs (subject or parent) 227 91 228 92 static get observedAttributes() { 229 - return ['annotation', 'comments']; 93 + return ['annotation']; 230 94 } 231 95 232 96 constructor() { ··· 247 111 return this._annotation; 248 112 } 249 113 250 - set comments(val: Comment[]) { 251 - this._comments = val; 252 - this.render(); 253 - } 254 - 255 - get comments() { 256 - return this._comments; 257 - } 258 - 259 - private toggleThread(uri: string) { 260 - if (this._collapsedThreads.has(uri)) { 261 - this._collapsedThreads.delete(uri); 262 - } else { 263 - this._collapsedThreads.add(uri); 264 - } 265 - this.render(); 266 - } 267 - 268 - private toggleReplyForm(uri: string) { 269 - if (this._activeReplyForms.has(uri)) { 270 - this._activeReplyForms.delete(uri); 271 - } else { 272 - this._activeReplyForms.add(uri); 273 - } 274 - this.render(); 275 - } 276 - 277 - private buildCommentThread(parentUri: string, isNested: boolean = false): string { 278 - const replies = this._comments.filter(c => c.reply?.parent === parentUri); 279 - if (replies.length === 0) return ''; 280 - 281 - const isCollapsed = this._collapsedThreads.has(parentUri); 282 - 283 - return ` 284 - <div class="comment-thread ${isNested ? 'nested' : ''}"> 285 - <button class="thread-toggle-btn" data-uri="${parentUri}"> 286 - ${isCollapsed ? '▸' : '▾'} 287 - </button> 288 - ${!isCollapsed ? ` 289 - <div class="thread-children"> 290 - ${replies.map(comment => this.renderComment(comment)).join('')} 291 - </div> 292 - ` : ` 293 - <small style="color: #999; font-style: italic; margin-left: 16px;"> 294 - ${replies.length} ${replies.length === 1 ? 'reply' : 'replies'} 295 - </small> 296 - `} 297 - </div> 298 - `; 299 - } 300 - 301 - private renderComment(comment: Comment): string { 302 - const hasReplies = this._comments.some(c => c.reply?.parent === comment.uri); 303 - const isReplyFormActive = comment.uri ? this._activeReplyForms.has(comment.uri) : false; 304 - 305 - return ` 306 - <div class="comment" data-uri="${comment.uri}"> 307 - <div class="comment-content"> 308 - <div class="comment-text">${escapeHtml(comment.plaintext)}</div> 309 - <div class="comment-meta"> 310 - <span>${formatRelativeTime(comment.createdAt)}</span> 311 - <button class="reply-btn" data-uri="${comment.uri}">Reply</button> 312 - </div> 313 - </div> 314 - ${isReplyFormActive ? ` 315 - <div class="reply-form" data-parent="${comment.uri}"> 316 - <textarea class="reply-input" placeholder="Write a reply..."></textarea> 317 - <div class="form-actions"> 318 - <button class="cancel-reply-btn" data-uri="${comment.uri}">Cancel</button> 319 - <button class="save-reply-btn primary" data-uri="${comment.uri}">Post</button> 320 - </div> 321 - </div> 322 - ` : ''} 323 - <!-- ${hasReplies && comment.uri ? this.buildCommentThread(comment.uri, true) : ''} /> 324 - </div> 325 - `; 326 - } 327 - 328 114 private render() { 329 115 if (!this.shadowRoot) return; 330 116 if (!this._annotation) { ··· 335 121 const ann = this._annotation; 336 122 const quote = ann.value.target.selector?.find((s: any) => s.$type === 'community.lexicon.annotation.annotation#textQuoteSelector'); 337 123 const text = quote?.exact || ''; 338 - const comments = this._comments.filter(c => c.subject === ann.uri && !c.reply); 339 124 340 - // Comments section state 341 - const isCommentsCollapsed = ann.uri ? this._collapsedThreads.has(ann.uri) : false; 342 - const isCommentFormActive = ann.uri ? this._activeReplyForms.has(ann.uri) : false; 343 - 344 - // Author info 345 125 const authorDid = ann.author?.did || 'unknown'; 346 126 const authorHandle = ann.author?.handle || (authorDid.includes(':') ? authorDid.split(':').pop() : authorDid); 347 - // Use dicebear fallback if no avatar 348 127 const avatarSrc = ann.author?.avatar || `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(authorHandle || authorDid)}`; 349 128 350 129 const html = ` ··· 367 146 </a> 368 147 ` : ''} 369 148 </div> 370 - 371 - <div class="comments-section"> 372 - <div class="comments-header"> 373 - <button class="toggle-comments-btn" data-uri="${ann.uri}"> 374 - ${isCommentsCollapsed ? '▸' : '▾'} ${comments.length} comment${comments.length !== 1 ? 's' : ''} 375 - </button> 376 - <button class="add-comment-btn" data-uri="${ann.uri}">Add comment</button> 377 - </div> 378 - 379 - ${!isCommentsCollapsed ? ` 380 - <div class="comments-list"> 381 - ${isCommentFormActive ? ` 382 - <div class="comment-form" data-subject="${ann.uri}"> 383 - <textarea class="comment-input" placeholder="Write a comment..."></textarea> 384 - <div class="form-actions"> 385 - <button class="cancel-comment-btn" data-uri="${ann.uri}">Cancel</button> 386 - <button class="save-comment-btn primary" data-uri="${ann.uri}">Post</button> 387 - </div> 388 - </div> 389 - ` : ''} 390 - ${comments.map(c => this.renderComment(c)).join('')} 391 - </div> 392 - ` : ''} 393 - </div> 394 149 </article> 395 150 `; 396 151 397 152 this.shadowRoot.innerHTML = html; 398 - this.attachEventListeners(); 399 153 } 400 - 401 - private attachEventListeners() { 402 - if (!this.shadowRoot) return; 403 - 404 - // Thread toggles (comments section and replies) 405 - this.shadowRoot.querySelectorAll('.toggle-comments-btn, .thread-toggle-btn').forEach(btn => { 406 - btn.addEventListener('click', (e) => { 407 - const uri = (e.currentTarget as HTMLElement).dataset.uri; 408 - if (uri) this.toggleThread(uri); 409 - }); 410 - }); 411 - 412 - // Show/Hide forms 413 - this.shadowRoot.querySelectorAll('.add-comment-btn, .reply-btn').forEach(btn => { 414 - btn.addEventListener('click', (e) => { 415 - const uri = (e.currentTarget as HTMLElement).dataset.uri; 416 - if (uri) this.toggleReplyForm(uri); 417 - }); 418 - }); 419 - 420 - this.shadowRoot.querySelectorAll('.cancel-comment-btn, .cancel-reply-btn').forEach(btn => { 421 - btn.addEventListener('click', (e) => { 422 - const uri = (e.currentTarget as HTMLElement).dataset.uri; 423 - if (uri) this.toggleReplyForm(uri); 424 - }); 425 - }); 426 - 427 - // Submit forms 428 - this.shadowRoot.querySelectorAll('.save-comment-btn').forEach(btn => { 429 - btn.addEventListener('click', (e) => { 430 - const uri = (e.currentTarget as HTMLElement).dataset.uri; 431 - const container = (e.currentTarget as HTMLElement).closest('.comment-form'); 432 - const textarea = container?.querySelector('textarea'); 433 - const text = textarea?.value.trim(); 434 - 435 - if (uri && text) { 436 - this.dispatchEvent(new CustomEvent('comment-submit', { 437 - detail: { subjectUri: uri, text }, 438 - bubbles: true, 439 - composed: true 440 - })); 441 - this.toggleReplyForm(uri); // Close form on submit (optimistic) 442 - } 443 - }); 444 - }); 445 - 446 - this.shadowRoot.querySelectorAll('.save-reply-btn').forEach(btn => { 447 - btn.addEventListener('click', (e) => { 448 - const uri = (e.currentTarget as HTMLElement).dataset.uri; // This is parent URI 449 - const container = (e.currentTarget as HTMLElement).closest('.reply-form'); 450 - const textarea = container?.querySelector('textarea'); 451 - const text = textarea?.value.trim(); 452 - 453 - if (uri && text) { 454 - this.dispatchEvent(new CustomEvent('reply-submit', { 455 - detail: { parentUri: uri, text }, 456 - bubbles: true, 457 - composed: true 458 - })); 459 - this.toggleReplyForm(uri); // Close form on submit 460 - } 461 - }); 462 - }); 463 - } 464 - 465 - 466 154 }
-16
packages/core/src/sidebar/index.ts
··· 425 425 this.pageAnnotations.forEach(ann => { 426 426 const card = document.createElement('seams-annotation-card') as SeamsAnnotationCard; 427 427 card.annotation = ann; 428 - card.comments = this.allComments; 429 428 annotationsContainer.appendChild(card); 430 429 }); 431 - 432 - // Event delegation for custom events from web components 433 - if (!annotationsContainer.hasAttribute('data-listening')) { 434 - annotationsContainer.setAttribute('data-listening', 'true'); 435 - 436 - annotationsContainer.addEventListener('comment-submit', (async (e: Event) => { 437 - const detail = (e as CustomEvent).detail; 438 - await this.handleCommentSubmit(detail.subjectUri, detail.text); 439 - }) as EventListener); 440 - 441 - annotationsContainer.addEventListener('reply-submit', (async (e: Event) => { 442 - const detail = (e as CustomEvent).detail; 443 - await this.handleReplySubmit(detail.parentUri, detail.text); 444 - }) as EventListener); 445 - } 446 430 } 447 431 448 432 private async handleCommentSubmit(subject: string, plaintext: string) {