feat(collab): scope SVG cursors by active sub-document view
Remote cursor arrows and focus rings from peers viewing a different note in rDocs are now suppressed. A generic viewId concept on the collab overlay lets any rApp with sub-views opt in via a rspace-view-change CustomEvent. Peers on a different view appear dimmed in the people panel with a document icon hint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
97bf0504cb
commit
195b42eb3b
|
|
@ -690,6 +690,8 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
|||
this.doc = null;
|
||||
for (const unsub of this._offlineNotebookUnsubs) unsub();
|
||||
this._offlineNotebookUnsubs = [];
|
||||
// Clear viewId so overlay stops filtering cursors
|
||||
window.dispatchEvent(new CustomEvent('rspace-view-change', { detail: { viewId: null } }));
|
||||
}
|
||||
|
||||
/** Extract notebook + notes from Automerge doc into component state */
|
||||
|
|
@ -1477,6 +1479,10 @@ Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rF
|
|||
this.renderMeta();
|
||||
this.mountEditor(this.selectedNote);
|
||||
this.broadcastPresence();
|
||||
// Notify overlay which sub-document we're viewing (scopes SVG cursors)
|
||||
window.dispatchEvent(new CustomEvent('rspace-view-change', {
|
||||
detail: { viewId: this.selectedNote.id, viewLabel: this.selectedNote.title },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ interface PeerState {
|
|||
lastSeen: number;
|
||||
module?: string; // which rApp they're in
|
||||
context?: string; // human-readable view label (e.g. "My Notebook > Note Title")
|
||||
viewId?: string | null; // sub-document view (e.g. noteId) — cursors hidden when mismatched
|
||||
}
|
||||
|
||||
export class RStackCollabOverlay extends HTMLElement {
|
||||
|
|
@ -60,6 +61,7 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
#openActionsId: string | null = null; // which peer's actions dropdown is open
|
||||
#spaceMembers: { did: string; displayName: string; role: string }[] = [];
|
||||
#space: string | null = null;
|
||||
#viewId: string | null = null; // sub-document view filter (e.g. noteId)
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -102,12 +104,16 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
this.#tryConnect();
|
||||
}
|
||||
|
||||
// Listen for sub-document view changes (e.g. rDocs note navigation)
|
||||
window.addEventListener('rspace-view-change', this.#onViewChange);
|
||||
|
||||
// Click-outside closes panel (listen on document, check composedPath for shadow DOM)
|
||||
document.addEventListener('click', this.#onDocumentClick);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
document.removeEventListener('click', this.#onDocumentClick);
|
||||
window.removeEventListener('rspace-view-change', this.#onViewChange);
|
||||
if (!this.#externalPeers) {
|
||||
window.removeEventListener('rspace-doc-subscribe', this.#onDocSubscribe);
|
||||
this.#unsubAwareness?.();
|
||||
|
|
@ -171,6 +177,19 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
}
|
||||
};
|
||||
|
||||
#onViewChange = (e: Event) => {
|
||||
const { viewId } = (e as CustomEvent).detail ?? {};
|
||||
this.#viewId = viewId || null;
|
||||
// Re-broadcast so peers learn our current viewId
|
||||
this.#broadcastPresence(this.#lastCursor, undefined);
|
||||
// Re-render cursors immediately (hides/shows based on new viewId)
|
||||
if (!this.#badgeOnly) {
|
||||
this.#renderCursors();
|
||||
this.#renderFocusRings();
|
||||
}
|
||||
if (this.#panelOpen) this.#renderPanel();
|
||||
};
|
||||
|
||||
// ── Runtime connection ──
|
||||
|
||||
#runtimePollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
|
@ -368,6 +387,7 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
cursor: msg.cursor ?? existing?.cursor ?? null,
|
||||
selection: msg.selection ?? existing?.selection ?? null,
|
||||
lastSeen: Date.now(),
|
||||
viewId: msg.viewId ?? existing?.viewId ?? null,
|
||||
};
|
||||
this.#peers.set(msg.peer, peer);
|
||||
this.#ensureGcTimer();
|
||||
|
|
@ -391,6 +411,7 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
selection,
|
||||
username: this.#localUsername,
|
||||
color: this.#localColor,
|
||||
viewId: this.#viewId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -598,12 +619,13 @@ 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);
|
||||
fragments.push(`
|
||||
<div class="people-row">
|
||||
<div class="people-row${differentView ? ' different-view' : ''}">
|
||||
<span class="dot" style="background:${peer.color}"></span>
|
||||
<div class="name-block">
|
||||
<span class="name">${this.#escHtml(peer.username)}</span>
|
||||
${ctxStr ? `<span class="peer-context">${this.#escHtml(ctxStr)}</span>` : ''}
|
||||
${ctxStr ? `<span class="peer-context">${differentView ? '\u{1F4C4} ' : ''}${this.#escHtml(ctxStr)}</span>` : ''}
|
||||
</div>
|
||||
${isCanvas ? `<button class="actions-btn" data-pid="${this.#escHtml(peer.peerId)}">></button>` : ''}
|
||||
</div>
|
||||
|
|
@ -758,6 +780,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
|
||||
for (const peer of this.#peers.values()) {
|
||||
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;
|
||||
const age = now - peer.lastSeen;
|
||||
const opacity = age > 5000 ? 0.3 : 1;
|
||||
|
||||
|
|
@ -797,6 +821,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
|
||||
for (const peer of this.#peers.values()) {
|
||||
if (!peer.selection) continue;
|
||||
// Skip peers viewing a different sub-document
|
||||
if (this.#viewId && peer.viewId && this.#viewId !== peer.viewId) continue;
|
||||
const target = this.#findCollabEl(peer.selection);
|
||||
if (!target) continue;
|
||||
|
||||
|
|
@ -1035,6 +1061,10 @@ const OVERLAY_CSS = `
|
|||
color: var(--rs-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.people-row.different-view {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* ── Mode toggle (Online/Offline) ── */
|
||||
|
||||
.mode-toggle {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export interface AwarenessMessage {
|
|||
selection?: string;
|
||||
username?: string;
|
||||
color?: string;
|
||||
viewId?: string | null;
|
||||
}
|
||||
|
||||
/** Client sends full encrypted Automerge binary for server-side opaque storage. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue