Ionosphere.tv
3
fork

Configure Feed

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

fix: merge overlapping comment byte ranges into clusters

Comments with different but overlapping byte ranges now group
together. Sorted by byte_start, then adjacent ranges that overlap
are merged into one cluster showing all reactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+41 -15
+41 -15
apps/ionosphere/src/app/components/TranscriptView.tsx
··· 379 379 return set; 380 380 }, [words, allComments]); 381 381 382 - // Group comments by byte range for margin indicators 383 - // Key: "byteStart-byteEnd", Value: { emoji counts, text comments } 382 + // Group comments by overlapping byte ranges into clusters. 383 + // Comments whose byte ranges overlap are merged into the same group. 384 384 const reactionGroups = useMemo(() => { 385 - if (allComments.length === 0) return new Map<string, { emojis: Map<string, number>; texts: CommentData[]; byteStart: number; byteEnd: number }>(); 386 - const groups = new Map<string, { emojis: Map<string, number>; texts: CommentData[]; byteStart: number; byteEnd: number }>(); 387 - for (const c of allComments) { 388 - if (c.byte_start === null || c.byte_end === null) continue; 389 - const key = `${c.byte_start}-${c.byte_end}`; 390 - if (!groups.has(key)) { 391 - groups.set(key, { emojis: new Map(), texts: [], byteStart: c.byte_start, byteEnd: c.byte_end }); 392 - } 393 - const group = groups.get(key)!; 394 - const isEmoji = c.text.length <= 2 && !/[a-zA-Z]/.test(c.text); 395 - if (isEmoji) { 396 - group.emojis.set(c.text, (group.emojis.get(c.text) || 0) + 1); 385 + type Group = { emojis: Map<string, number>; texts: CommentData[]; byteStart: number; byteEnd: number }; 386 + if (allComments.length === 0) return new Map<string, Group>(); 387 + 388 + // Collect anchored comments 389 + const anchored = allComments.filter( 390 + (c) => c.byte_start !== null && c.byte_end !== null 391 + ) as (CommentData & { byte_start: number; byte_end: number })[]; 392 + 393 + if (anchored.length === 0) return new Map<string, Group>(); 394 + 395 + // Sort by byte_start 396 + anchored.sort((a, b) => a.byte_start - b.byte_start); 397 + 398 + // Merge overlapping ranges into clusters 399 + const clusters: { byteStart: number; byteEnd: number; comments: typeof anchored }[] = []; 400 + for (const c of anchored) { 401 + const last = clusters[clusters.length - 1]; 402 + if (last && c.byte_start <= last.byteEnd) { 403 + // Overlaps — extend the cluster 404 + last.byteEnd = Math.max(last.byteEnd, c.byte_end); 405 + last.comments.push(c); 397 406 } else { 398 - group.texts.push(c); 407 + clusters.push({ byteStart: c.byte_start, byteEnd: c.byte_end, comments: [c] }); 399 408 } 409 + } 410 + 411 + // Build groups from clusters 412 + const groups = new Map<string, Group>(); 413 + for (const cluster of clusters) { 414 + const key = `${cluster.byteStart}-${cluster.byteEnd}`; 415 + const emojis = new Map<string, number>(); 416 + const texts: CommentData[] = []; 417 + for (const c of cluster.comments) { 418 + const isEmoji = c.text.length <= 2 && !/[a-zA-Z]/.test(c.text); 419 + if (isEmoji) { 420 + emojis.set(c.text, (emojis.get(c.text) || 0) + 1); 421 + } else { 422 + texts.push(c); 423 + } 424 + } 425 + groups.set(key, { emojis, texts, byteStart: cluster.byteStart, byteEnd: cluster.byteEnd }); 400 426 } 401 427 return groups; 402 428 }, [allComments]);