Merge branch 'dev'
This commit is contained in:
commit
8dd1d53297
|
|
@ -56,6 +56,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
||||||
#connState: 'connected' | 'offline' | 'reconnecting' | 'connecting' = 'connecting';
|
#connState: 'connected' | 'offline' | 'reconnecting' | 'connecting' = 'connecting';
|
||||||
#externalPeers = false; // true for canvas — peers fed in via public API
|
#externalPeers = false; // true for canvas — peers fed in via public API
|
||||||
#openActionsId: string | null = null; // which peer's actions dropdown is open
|
#openActionsId: string | null = null; // which peer's actions dropdown is open
|
||||||
|
#spaceMembers: { did: string; displayName: string; role: string }[] = [];
|
||||||
|
#space: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -64,6 +66,7 @@ export class RStackCollabOverlay extends HTMLElement {
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.#moduleId = this.getAttribute('module-id');
|
this.#moduleId = this.getAttribute('module-id');
|
||||||
|
this.#space = this.getAttribute('space');
|
||||||
this.#badgeOnly = this.getAttribute('mode') === 'badge-only';
|
this.#badgeOnly = this.getAttribute('mode') === 'badge-only';
|
||||||
this.#soloMode = localStorage.getItem('rspace_solo_mode') === '1';
|
this.#soloMode = localStorage.getItem('rspace_solo_mode') === '1';
|
||||||
|
|
||||||
|
|
@ -72,6 +75,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
||||||
this.#externalPeers = true;
|
this.#externalPeers = true;
|
||||||
// Dispatch initial solo-mode event for canvas PresenceManager
|
// Dispatch initial solo-mode event for canvas PresenceManager
|
||||||
document.dispatchEvent(new CustomEvent('solo-mode-change', { detail: { solo: this.#soloMode } }));
|
document.dispatchEvent(new CustomEvent('solo-mode-change', { detail: { solo: this.#soloMode } }));
|
||||||
|
// Canvas: load members immediately (no runtime wait)
|
||||||
|
this.#loadSpaceMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve local identity
|
// Resolve local identity
|
||||||
|
|
@ -186,6 +191,9 @@ export class RStackCollabOverlay extends HTMLElement {
|
||||||
this.#localColor = this.#colorForPeer(this.#localPeerId!);
|
this.#localColor = this.#colorForPeer(this.#localPeerId!);
|
||||||
this.#connState = 'connected';
|
this.#connState = 'connected';
|
||||||
|
|
||||||
|
// Load space members for offline display
|
||||||
|
this.#loadSpaceMembers();
|
||||||
|
|
||||||
if (this.#docId) {
|
if (this.#docId) {
|
||||||
this.#connectToDoc();
|
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() {
|
#connectToDoc() {
|
||||||
const runtime = (window as any).__rspaceOfflineRuntime;
|
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||||
if (!runtime || !this.#docId) return;
|
if (!runtime || !this.#docId) return;
|
||||||
|
|
@ -415,8 +475,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
||||||
<div class="collab-badge visible" id="badge"></div>
|
<div class="collab-badge visible" id="badge"></div>
|
||||||
<div class="people-panel" id="people-panel">
|
<div class="people-panel" id="people-panel">
|
||||||
<div class="people-panel-header">
|
<div class="people-panel-header">
|
||||||
<h3>People Online</h3>
|
<h3>People</h3>
|
||||||
<span class="panel-count" id="panel-count">1</span>
|
<span class="panel-count" id="panel-count">1 online</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="people-list" id="people-list"></div>
|
<div class="people-list" id="people-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -448,8 +508,8 @@ export class RStackCollabOverlay extends HTMLElement {
|
||||||
const countEl = this.#shadow.getElementById('panel-count');
|
const countEl = this.#shadow.getElementById('panel-count');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
|
||||||
const count = this.#peers.size + 1;
|
const onlineCount = this.#peers.size + 1;
|
||||||
if (countEl) countEl.textContent = this.#connState === 'connected' ? String(count) : '\u2014';
|
if (countEl) countEl.textContent = this.#connState === 'connected' ? `${onlineCount} online` : '\u2014';
|
||||||
|
|
||||||
const fragments: string[] = [];
|
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('');
|
list.innerHTML = fragments.join('');
|
||||||
|
|
||||||
// Wire up event delegation on the list
|
// Wire up event delegation on the list
|
||||||
|
|
@ -868,6 +957,26 @@ const OVERLAY_CSS = `
|
||||||
font-weight: normal;
|
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 (Online/Offline) ── */
|
||||||
|
|
||||||
.mode-toggle {
|
.mode-toggle {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue