feat(collab): show all space members in People panel — offline with grey dots
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd6317fd06
commit
b5a54265ee
|
|
@ -56,6 +56,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
#connState: 'connected' | 'offline' | 'reconnecting' | 'connecting' = 'connecting';
|
||||
#externalPeers = false; // true for canvas — peers fed in via public API
|
||||
#openActionsId: string | null = null; // which peer's actions dropdown is open
|
||||
#spaceMembers: { did: string; displayName: string; role: string }[] = [];
|
||||
#space: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -64,6 +66,7 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
|
||||
connectedCallback() {
|
||||
this.#moduleId = this.getAttribute('module-id');
|
||||
this.#space = this.getAttribute('space');
|
||||
this.#badgeOnly = this.getAttribute('mode') === 'badge-only';
|
||||
this.#soloMode = localStorage.getItem('rspace_solo_mode') === '1';
|
||||
|
||||
|
|
@ -72,6 +75,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
this.#externalPeers = true;
|
||||
// Dispatch initial solo-mode event for canvas PresenceManager
|
||||
document.dispatchEvent(new CustomEvent('solo-mode-change', { detail: { solo: this.#soloMode } }));
|
||||
// Canvas: load members immediately (no runtime wait)
|
||||
this.#loadSpaceMembers();
|
||||
}
|
||||
|
||||
// Resolve local identity
|
||||
|
|
@ -186,6 +191,9 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
this.#localColor = this.#colorForPeer(this.#localPeerId!);
|
||||
this.#connState = 'connected';
|
||||
|
||||
// Load space members for offline display
|
||||
this.#loadSpaceMembers();
|
||||
|
||||
if (this.#docId) {
|
||||
this.#connectToDoc();
|
||||
}
|
||||
|
|
@ -228,6 +236,58 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
});
|
||||
}
|
||||
|
||||
async #loadSpaceMembers(retry = true) {
|
||||
let members: Record<string, { role: string; displayName?: string }> = {};
|
||||
|
||||
// Try CRDT doc first (same pattern as rstack-space-settings)
|
||||
const sync = (window as any).__communitySync;
|
||||
if (sync?.doc?.members) {
|
||||
members = sync.doc.members;
|
||||
} else if (this.#space) {
|
||||
// Fallback: fetch from API
|
||||
const host = window.location.hostname;
|
||||
const isSubdomain = host.split('.').length >= 3 && !host.startsWith('www.');
|
||||
const metaUrl = isSubdomain ? '/rspace/api/meta' : `/${this.#space}/rspace/api/meta`;
|
||||
try {
|
||||
const token = localStorage.getItem('encryptid_token');
|
||||
const res = await fetch(metaUrl, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
if (json.meta?.members) members = json.meta.members;
|
||||
}
|
||||
} catch { /* offline or error */ }
|
||||
}
|
||||
|
||||
const dids = Object.keys(members);
|
||||
if (!dids.length) {
|
||||
// CRDT doc may not be synced yet — retry once after 5s
|
||||
if (retry) setTimeout(() => this.#loadSpaceMembers(false), 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve display names via EncryptID
|
||||
let resolved: Record<string, { username: string; displayName: string }> = {};
|
||||
try {
|
||||
const res = await fetch('/api/users/resolve-dids', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dids }),
|
||||
});
|
||||
if (res.ok) resolved = await res.json();
|
||||
} catch { /* offline */ }
|
||||
|
||||
this.#spaceMembers = dids.map(did => ({
|
||||
did,
|
||||
displayName: resolved[did]?.displayName || resolved[did]?.username || members[did].displayName || did.slice(0, 12),
|
||||
role: members[did].role || 'member',
|
||||
}));
|
||||
|
||||
this.#renderBadge();
|
||||
if (this.#panelOpen) this.#renderPanel();
|
||||
}
|
||||
|
||||
#connectToDoc() {
|
||||
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||
if (!runtime || !this.#docId) return;
|
||||
|
|
@ -415,8 +475,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
<div class="collab-badge visible" id="badge"></div>
|
||||
<div class="people-panel" id="people-panel">
|
||||
<div class="people-panel-header">
|
||||
<h3>People Online</h3>
|
||||
<span class="panel-count" id="panel-count">1</span>
|
||||
<h3>People</h3>
|
||||
<span class="panel-count" id="panel-count">1 online</span>
|
||||
</div>
|
||||
<div class="people-list" id="people-list"></div>
|
||||
</div>
|
||||
|
|
@ -448,8 +508,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
const countEl = this.#shadow.getElementById('panel-count');
|
||||
if (!list) return;
|
||||
|
||||
const count = this.#peers.size + 1;
|
||||
if (countEl) countEl.textContent = this.#connState === 'connected' ? String(count) : '\u2014';
|
||||
const onlineCount = this.#peers.size + 1;
|
||||
if (countEl) countEl.textContent = this.#connState === 'connected' ? `${onlineCount} online` : '\u2014';
|
||||
|
||||
const fragments: string[] = [];
|
||||
|
||||
|
|
@ -506,6 +566,35 @@ export class RStackCollabOverlay extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
// Offline space members (not currently online)
|
||||
if (this.#spaceMembers.length > 0) {
|
||||
// Build set of online usernames (case-insensitive) for cross-referencing
|
||||
const onlineNames = new Set<string>();
|
||||
onlineNames.add(this.#localUsername.toLowerCase());
|
||||
for (const peer of this.#peers.values()) {
|
||||
onlineNames.add(peer.username.toLowerCase());
|
||||
}
|
||||
|
||||
const offlineMembers = this.#spaceMembers.filter(
|
||||
m => !onlineNames.has(m.displayName.toLowerCase())
|
||||
);
|
||||
|
||||
if (offlineMembers.length > 0) {
|
||||
fragments.push(`<div class="section-divider">Offline</div>`);
|
||||
for (const m of offlineMembers) {
|
||||
fragments.push(`
|
||||
<div class="people-row offline">
|
||||
<span class="dot"></span>
|
||||
<div class="name-block">
|
||||
<span class="name">${this.#escHtml(m.displayName)}</span>
|
||||
<span class="peer-context">${this.#escHtml(m.role)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.innerHTML = fragments.join('');
|
||||
|
||||
// Wire up event delegation on the list
|
||||
|
|
@ -868,6 +957,26 @@ const OVERLAY_CSS = `
|
|||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* ── Offline members ── */
|
||||
|
||||
.section-divider {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
padding: 8px 10px 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.people-row.offline .dot {
|
||||
background: #6b7280 !important;
|
||||
}
|
||||
|
||||
.people-row.offline .name {
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
}
|
||||
|
||||
/* ── Mode toggle (Online/Offline) ── */
|
||||
|
||||
.mode-toggle {
|
||||
|
|
|
|||
Loading…
Reference in New Issue