Compare commits

...

2 Commits

Author SHA1 Message Date
Jeff Emmett db7e49ee92 Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m8s Details
2026-04-16 15:01:39 -04:00
Jeff Emmett 9daeb60895 fix(collab): dedup cursors by username + same-page-only visibility
- Deduplicate cursor rendering using #uniquePeers() (was showing
  multiple cursors per user from different tabs/sessions)
- Strict same-page filtering: cursors only visible when peers share
  the same effectiveViewId (viewId ?? moduleId)
- Users on different rApps no longer see each other's cursors
- Applied same fixes to focus rings and panel "different view" badge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:00:11 -04:00
1 changed files with 17 additions and 8 deletions

View File

@ -400,6 +400,11 @@ export class RStackCollabOverlay extends HTMLElement {
// ── Local broadcasting ──
/** Effective view identifier — viewId if set, else moduleId. Used to scope cursor visibility. */
get #effectiveViewId(): string | null {
return this.#viewId || this.#moduleId || null;
}
#broadcastPresence(cursor?: { x: number; y: number }, selection?: string) {
if (this.#soloMode) return; // suppress outgoing awareness in solo mode
@ -411,7 +416,7 @@ export class RStackCollabOverlay extends HTMLElement {
selection,
username: this.#localUsername,
color: this.#localColor,
viewId: this.#viewId,
viewId: this.#effectiveViewId,
});
}
@ -619,7 +624,8 @@ export class RStackCollabOverlay extends HTMLElement {
if (peer.module) ctxParts.push(peer.module);
if (peer.context) ctxParts.push(peer.context);
const ctxStr = ctxParts.join(' · ');
const differentView = !!(this.#viewId && peer.viewId && this.#viewId !== peer.viewId);
const localView = this.#effectiveViewId;
const differentView = !!((localView || peer.viewId) && localView !== peer.viewId);
fragments.push(`
<div class="people-row${differentView ? ' different-view' : ''}">
<span class="dot" style="background:${peer.color}"></span>
@ -778,10 +784,12 @@ export class RStackCollabOverlay extends HTMLElement {
const now = Date.now();
const fragments: string[] = [];
for (const peer of this.#peers.values()) {
// Deduplicate by username — only show the most recent cursor per user
const localView = this.#effectiveViewId;
for (const peer of this.#uniquePeers()) {
if (!peer.cursor) continue;
// Skip peers viewing a different sub-document (e.g. different note in rDocs)
if (this.#viewId && peer.viewId && this.#viewId !== peer.viewId) continue;
// Only show cursors for peers on the same page/view
if ((localView || peer.viewId) && localView !== peer.viewId) continue;
const age = now - peer.lastSeen;
const opacity = age > 5000 ? 0.3 : 1;
@ -819,10 +827,11 @@ export class RStackCollabOverlay extends HTMLElement {
// Remove all existing focus rings from the document
document.querySelectorAll('.rstack-collab-focus-ring').forEach(el => el.remove());
for (const peer of this.#peers.values()) {
const localView = this.#effectiveViewId;
for (const peer of this.#uniquePeers()) {
if (!peer.selection) continue;
// Skip peers viewing a different sub-document
if (this.#viewId && peer.viewId && this.#viewId !== peer.viewId) continue;
// Only show focus rings for peers on the same page/view
if ((localView || peer.viewId) && localView !== peer.viewId) continue;
const target = this.#findCollabEl(peer.selection);
if (!target) continue;