diff --git a/server/spaces.ts b/server/spaces.ts index b57291e..7ceb088 100644 --- a/server/spaces.ts +++ b/server/spaces.ts @@ -241,6 +241,8 @@ spaces.get("/", async (c) => { seenSlugs.add(slug); let vis = data.meta.visibility || "private"; + // Demo space is always public + if (slug === "demo") vis = "public"; // Check both claims.sub (raw userId) and did:key: format for // compatibility — auto-provisioned spaces store ownerDID as did:key: const callerDid = claims ? ((claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`) : ""; @@ -261,9 +263,7 @@ spaces.get("/", async (c) => { // For unauthenticated: only show demo if (!claims && slug !== "demo") continue; - // For authenticated: skip public spaces the user has no role in - // (demo is shown separately, other public spaces are noise) - if (claims && isPublicSpace && !isOwner && !isMember && slug !== "demo") continue; + // Public spaces are shown in the PUBLIC section for all authenticated users // Determine relationship const relationship = isOwner @@ -290,6 +290,7 @@ spaces.get("/", async (c) => { accessible, relationship, pendingRequest: pendingRequest || undefined, + isPersonal: !!(username && slug === username), }); } } diff --git a/shared/components/rstack-space-switcher.ts b/shared/components/rstack-space-switcher.ts index f4cec69..a09f656 100644 --- a/shared/components/rstack-space-switcher.ts +++ b/shared/components/rstack-space-switcher.ts @@ -21,6 +21,7 @@ interface SpaceInfo { accessible?: boolean; relationship?: "owner" | "member" | "demo" | "other"; pendingRequest?: boolean; + isPersonal?: boolean; } export class RStackSpaceSwitcher extends HTMLElement { @@ -141,10 +142,9 @@ export class RStackSpaceSwitcher extends HTMLElement { return { cls: "vis-public", label: "👁" }; } - /** Format display name based on visibility type */ + /** Format display name — only rename the user's personal space */ #displayName(s: SpaceInfo): string { - const v = s.visibility || "public"; - if (v === "private") { + if (s.isPersonal || s.slug === getUsername()?.toLowerCase()) { const username = getUsername(); return username ? `${username}'s Space` : "My Space"; } @@ -168,130 +168,60 @@ export class RStackSpaceSwitcher extends HTMLElement { .join(""); } - #demoSpaceHTML(current: string, moduleId: string): string { - const isActive = current === "demo"; - return ` - - 🎮 - Demo Space - 👁 - `; - } - #renderMenu(menu: HTMLElement, current: string) { const auth = isAuthenticated(); const moduleId = this.#getCurrentModule(); - if (this.#spaces.length === 0) { - let cta = ""; - if (!auth) { - cta = this.#yourSpaceCTAhtml("Sign in to create →"); - } else { - const username = getUsername(); - const label = username ? `Create ${username}'s Space →` : "Create My Space →"; - cta = this.#yourSpaceCTAhtml(label); - } - menu.innerHTML = ` - ${this.#demoSpaceHTML(current, moduleId)} -
- ${cta} -
- - - ${this.#createFormHTML()} - `; - this.#attachYourSpaceCTA(menu); - this.#attachCreatePopout(menu); - return; - } + // Split spaces by visibility + const privateSpaces = this.#spaces.filter((s) => s.visibility === "private"); + const permissionedSpaces = this.#spaces.filter((s) => s.visibility === "permissioned"); + const publicSpaces = this.#spaces.filter((s) => s.visibility === "public"); - // Split spaces by visibility and role - const mySpaces = this.#spaces.filter((s) => s.role); - const discoverSpaces = this.#spaces.filter((s) => s.accessible === false); - - const privateSpaces = mySpaces.filter((s) => s.visibility === "private"); - const permissionedSpaces = mySpaces.filter((s) => s.visibility === "permissioned"); - const publicSpaces = mySpaces.filter((s) => s.visibility === "public"); - - const hasOwnedSpace = mySpaces.some((s) => s.relationship === "owner"); + const hasOwnedSpace = this.#spaces.some((s) => s.relationship === "owner"); let html = ""; - // ── Demo Space — always first ── - html += this.#demoSpaceHTML(current, moduleId); - html += `
`; - - // ── Create personal space CTA — only if user has no owned spaces ── + // ── PRIVATE section ── + html += `
Private
`; + if (privateSpaces.length > 0) { + html += this.#renderSpaceGroup(privateSpaces, current, moduleId); + } if (!auth) { html += this.#yourSpaceCTAhtml("Sign in to create →"); - html += `
`; } else if (!hasOwnedSpace) { const username = getUsername(); const label = username ? `Create ${username}'s Space →` : "Create My Space →"; html += this.#yourSpaceCTAhtml(label); - html += `
`; } - // ── Private spaces (red) — top ── - if (privateSpaces.length > 0) { - html += `
Private
`; - html += this.#renderSpaceGroup(privateSpaces, current, moduleId); - } - - // ── Permissioned spaces (yellow) — middle ── + // ── PERMISSIONED section ── if (permissionedSpaces.length > 0) { - if (privateSpaces.length > 0) html += `
`; + html += `
`; html += `
Permissioned
`; html += this.#renderSpaceGroup(permissionedSpaces, current, moduleId); } - // ── Public spaces (green) — bottom ── + // ── PUBLIC section ── + html += `
`; + html += `
Public
`; if (publicSpaces.length > 0) { - if (privateSpaces.length > 0 || permissionedSpaces.length > 0) html += `
`; - html += `
Public
`; html += this.#renderSpaceGroup(publicSpaces, current, moduleId); + } else { + html += ``; } - // ── Discover (permissioned spaces the user can request access to) ── - if (auth && discoverSpaces.length > 0) { - html += `
`; - html += `
Discover
`; - html += discoverSpaces - .map((s) => { - const vis = this.#visibilityInfo(s); - const pending = s.pendingRequest; - return ` -
- ${s.icon || "🌐"} - ${this.#displayName(s)} - ${vis.label} - ${pending - ? `Requested` - : `` - } -
`; - }) - .join(""); - } + // ── *Discover section ── + html += `
`; + html += `
✦ Discover
`; + html += `Explore spaces →`; + // ── Create new space ── html += `
`; html += ``; html += this.#createFormHTML(); menu.innerHTML = html; - // Attach Request Access button listeners - menu.querySelectorAll(".item-request-btn").forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - const el = btn as HTMLElement; - this.#showRequestAccessModal(el.dataset.slug!, el.dataset.name!); - }); - }); - // Intercept space link clicks — dispatch space-switch event for client-side switching menu.querySelectorAll("a.item[href]").forEach((link) => { link.addEventListener("click", (e) => { @@ -1176,6 +1106,9 @@ const STYLES = ` .section-label--private { color: #f87171; opacity: 0.85; } .section-label--permissioned { color: #fbbf24; opacity: 0.85; } .section-label--public { color: #34d399; opacity: 0.85; } +.section-label--discover { color: #a78bfa; opacity: 0.85; } +.item--discover-link { font-size: 0.85rem; color: #a78bfa; text-decoration: none; } +.item--discover-link:hover { background: rgba(167,139,250,0.08); } .item { display: flex; align-items: center; gap: 10px;