/** * folk-inbox-client — Collaborative email client. * * Shows mailbox list, thread inbox, thread detail with comments, * and approval workflow interface. */ class FolkInboxClient extends HTMLElement { private shadow: ShadowRoot; private space = "demo"; private view: "mailboxes" | "threads" | "thread" | "approvals" = "mailboxes"; private mailboxes: any[] = []; private threads: any[] = []; private currentMailbox: any = null; private currentThread: any = null; private approvals: any[] = []; private filter: "all" | "open" | "snoozed" | "closed" = "all"; private demoThreads: Record = { team: [ { id: "t1", from_name: "Alice Chen", from_address: "alice@example.com", subject: "Sprint planning notes", status: "open", is_read: true, is_starred: true, comment_count: 3, received_at: new Date(Date.now() - 2 * 3600000).toISOString(), body_text: "Here are the sprint planning notes from today's session. We agreed on the following priorities for the next two weeks:\n\n1. Ship local-first sync for notes module\n2. Polish the calendar demo mode\n3. Review provider registry API\n\nLet me know if I missed anything.", comments: [{ username: "Bob Martinez", body: "Looks good! I'd add the inbox overhaul too.", created_at: new Date(Date.now() - 1.5 * 3600000).toISOString() }, { username: "Carol Wu", body: "Agreed, calendar polish is top priority.", created_at: new Date(Date.now() - 1 * 3600000).toISOString() }, { username: "Alice Chen", body: "Updated the list. Thanks!", created_at: new Date(Date.now() - 0.5 * 3600000).toISOString() }] }, { id: "t2", from_name: "Bob Martinez", from_address: "bob@example.com", subject: "Deploy checklist for v2.1", status: "open", is_read: false, is_starred: false, comment_count: 1, received_at: new Date(Date.now() - 5 * 3600000).toISOString(), body_text: "Here is the deploy checklist for v2.1. Please review before we cut the release.\n\n- [ ] Run full test suite\n- [ ] Update changelog\n- [ ] Tag release in Gitea\n- [ ] Deploy to staging\n- [ ] Smoke test all modules", comments: [{ username: "Alice Chen", body: "I can handle the changelog update.", created_at: new Date(Date.now() - 4 * 3600000).toISOString() }] }, { id: "t3", from_name: "Carol Wu", from_address: "carol@example.com", subject: "Design system color tokens", status: "snoozed", is_read: true, is_starred: false, comment_count: 0, received_at: new Date(Date.now() - 24 * 3600000).toISOString(), body_text: "I've been working on standardizing our color tokens across all modules. The current approach of inline hex values is getting unwieldy. Proposal attached.", comments: [] }, { id: "t4", from_name: "Dave Park", from_address: "dave@example.com", subject: "Q1 Retrospective summary", status: "closed", is_read: true, is_starred: false, comment_count: 5, received_at: new Date(Date.now() - 72 * 3600000).toISOString(), body_text: "Summary of our Q1 retrospective:\n\nWhat went well: Local-first architecture, community engagement, rapid prototyping.\nWhat to improve: Documentation, test coverage, onboarding flow.", comments: [{ username: "Alice Chen", body: "Great summary, Dave.", created_at: new Date(Date.now() - 70 * 3600000).toISOString() }, { username: "Bob Martinez", body: "+1 on improving docs.", created_at: new Date(Date.now() - 69 * 3600000).toISOString() }, { username: "Carol Wu", body: "I can lead the onboarding redesign.", created_at: new Date(Date.now() - 68 * 3600000).toISOString() }, { username: "Dave Park", body: "Sounds good, let's schedule a kickoff.", created_at: new Date(Date.now() - 67 * 3600000).toISOString() }, { username: "Alice Chen", body: "Added to next sprint.", created_at: new Date(Date.now() - 66 * 3600000).toISOString() }] }, ], support: [ { id: "t5", from_name: "New User", from_address: "newuser@example.com", subject: "How do I create a space?", status: "open", is_read: false, is_starred: false, comment_count: 0, received_at: new Date(Date.now() - 1 * 3600000).toISOString(), body_text: "Hi, I just signed up and I'm not sure how to create my own space. The docs mention a space switcher but I can't find it. Could you point me in the right direction?", comments: [] }, { id: "t6", from_name: "Partner Org", from_address: "partner@example.org", subject: "Integration API access request", status: "open", is_read: true, is_starred: true, comment_count: 2, received_at: new Date(Date.now() - 8 * 3600000).toISOString(), body_text: "We'd like to integrate our platform with rSpace modules via the API. Could you provide API documentation and access credentials for our staging environment?", comments: [{ username: "Team Bot", body: "Request logged. Assigning to API team.", created_at: new Date(Date.now() - 7 * 3600000).toISOString() }, { username: "Bob Martinez", body: "I'll send over the API docs today.", created_at: new Date(Date.now() - 6 * 3600000).toISOString() }] }, { id: "t7", from_name: "Community Member", from_address: "member@example.com", subject: "Feature request: dark mode", status: "closed", is_read: true, is_starred: false, comment_count: 4, received_at: new Date(Date.now() - 96 * 3600000).toISOString(), body_text: "Would love to see a proper dark mode toggle. The current theme is close but some panels still have bright backgrounds.", comments: [{ username: "Carol Wu", body: "This is on our roadmap! Targeting next release.", created_at: new Date(Date.now() - 90 * 3600000).toISOString() }, { username: "Community Member", body: "Awesome, looking forward to it.", created_at: new Date(Date.now() - 88 * 3600000).toISOString() }, { username: "Carol Wu", body: "Dark mode shipped in v2.0!", created_at: new Date(Date.now() - 48 * 3600000).toISOString() }, { username: "Community Member", body: "Looks great, thanks!", created_at: new Date(Date.now() - 46 * 3600000).toISOString() }] }, ], }; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "demo"; if (this.space === "demo") { this.loadDemoData(); return; } this.loadMailboxes(); } private loadDemoData() { this.mailboxes = [ { slug: "team", name: "Team Inbox", email: "team@rspace.online", description: "Shared workspace inbox for internal team communications" }, { slug: "support", name: "Support", email: "support@rspace.online", description: "Community support requests with multi-sig approval workflows" }, ]; this.render(); } private async loadMailboxes() { try { const base = window.location.pathname.replace(/\/$/, ""); const resp = await fetch(`${base}/api/mailboxes`); if (resp.ok) { const data = await resp.json(); this.mailboxes = data.mailboxes || []; } } catch { /* ignore */ } this.render(); } private async loadThreads(slug: string) { if (this.space === "demo") { this.threads = this.demoThreads[slug] || []; if (this.filter !== "all") this.threads = this.threads.filter(t => t.status === this.filter); this.render(); return; } try { const base = window.location.pathname.replace(/\/$/, ""); const status = this.filter === "all" ? "" : `?status=${this.filter}`; const resp = await fetch(`${base}/api/mailboxes/${slug}/threads${status}`); if (resp.ok) { const data = await resp.json(); this.threads = data.threads || []; } } catch { /* ignore */ } this.render(); } private async loadThread(id: string) { if (this.space === "demo") { const all = [...(this.demoThreads.team || []), ...(this.demoThreads.support || [])]; this.currentThread = all.find(t => t.id === id) || null; if (this.currentThread) { this.currentThread.comments = this.currentThread.comments || [{ username: "Team Bot", body: "Thread noted.", created_at: new Date().toISOString() }]; } this.render(); return; } try { const base = window.location.pathname.replace(/\/$/, ""); const resp = await fetch(`${base}/api/threads/${id}`); if (resp.ok) { this.currentThread = await resp.json(); } } catch { /* ignore */ } this.render(); } private async loadApprovals() { if (this.space === "demo") { this.approvals = []; this.render(); return; } try { const base = window.location.pathname.replace(/\/$/, ""); const q = this.currentMailbox ? `?mailbox=${this.currentMailbox.slug}` : ""; const resp = await fetch(`${base}/api/approvals${q}`); if (resp.ok) { const data = await resp.json(); this.approvals = data.approvals || []; } } catch { /* ignore */ } this.render(); } private timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } private render() { this.shadow.innerHTML = `
${this.renderNav()} ${this.renderView()}
`; this.bindEvents(); } private renderNav(): string { const items = [ { id: "mailboxes", label: "Mailboxes" }, { id: "approvals", label: "Approvals" }, ]; if (this.currentMailbox) { items.unshift({ id: "threads", label: this.currentMailbox.name }); } return `
${this.view !== "mailboxes" ? `` : ""} ${items.map((i) => ``).join("")}
`; } private renderView(): string { switch (this.view) { case "mailboxes": return this.renderMailboxes(); case "threads": return this.renderThreads(); case "thread": return this.renderThreadDetail(); case "approvals": return this.renderApprovals(); default: return ""; } } private renderMailboxes(): string { if (this.mailboxes.length === 0) { return `

📨

No mailboxes yet

Create a shared mailbox to get started

`; } return this.mailboxes.map((m) => `
${m.name}
${m.email}
${m.description || "Shared mailbox"}
`).join(""); } private renderThreads(): string { const filters = ["all", "open", "snoozed", "closed"]; return `
${filters.map((f) => ``).join("")}
${this.threads.length === 0 ? `
No threads
` : this.threads.map((t) => `
${t.is_read ? "" : ``} ${t.from_name || t.from_address || "Unknown"} ${t.subject} ${t.comment_count > 0 ? `(${t.comment_count})` : ""} ${t.is_starred ? `` : ""} ${t.status} ${this.timeAgo(t.received_at)}
`).join("")}
`; } private renderThreadDetail(): string { if (!this.currentThread) return `
Loading...
`; const t = this.currentThread; const comments = t.comments || []; return `
${t.subject}
From: ${t.from_name || ""} <${t.from_address || "unknown"}> · ${this.timeAgo(t.received_at)} · ${t.status}
${t.body_text || t.body_html || "(no content)"}
Comments (${comments.length})
${comments.map((cm: any) => `
${cm.username || "Anonymous"} ${this.timeAgo(cm.created_at)}
${cm.body}
`).join("")} ${comments.length === 0 ? `
No comments yet
` : ""}
`; } private renderApprovals(): string { if (this.approvals.length === 0) { return `

No pending approvals

`; } return this.approvals.map((a) => `
${a.subject}
${a.status}
${a.signature_count || 0} / ${a.required_signatures} signatures · ${this.timeAgo(a.created_at)}
${a.status === "PENDING" ? `
` : ""}
`).join(""); } private bindEvents() { // Navigation this.shadow.querySelectorAll("[data-nav]").forEach((btn) => { btn.addEventListener("click", () => { const nav = (btn as HTMLElement).dataset.nav as any; if (nav === "approvals") { this.view = "approvals"; this.loadApprovals(); } else if (nav === "mailboxes") { this.view = "mailboxes"; this.currentMailbox = null; this.render(); } else if (nav === "threads") { this.view = "threads"; this.render(); } }); }); // Back const backBtn = this.shadow.querySelector("[data-action='back']"); if (backBtn) { backBtn.addEventListener("click", () => { if (this.view === "thread") { this.view = "threads"; this.render(); } else if (this.view === "threads" || this.view === "approvals") { this.view = "mailboxes"; this.currentMailbox = null; this.render(); } }); } // Mailbox click this.shadow.querySelectorAll("[data-mailbox]").forEach((card) => { card.addEventListener("click", () => { const slug = (card as HTMLElement).dataset.mailbox!; this.currentMailbox = this.mailboxes.find((m) => m.slug === slug); this.view = "threads"; this.loadThreads(slug); }); }); // Thread click this.shadow.querySelectorAll("[data-thread]").forEach((row) => { row.addEventListener("click", () => { const id = (row as HTMLElement).dataset.thread!; this.view = "thread"; this.loadThread(id); }); }); // Filter this.shadow.querySelectorAll("[data-filter]").forEach((btn) => { btn.addEventListener("click", () => { this.filter = (btn as HTMLElement).dataset.filter as any; if (this.currentMailbox) this.loadThreads(this.currentMailbox.slug); }); }); // Approval actions this.shadow.querySelectorAll("[data-approve]").forEach((btn) => { btn.addEventListener("click", async () => { if (this.space === "demo") { alert("Approvals are disabled in demo mode."); return; } const id = (btn as HTMLElement).dataset.approve!; const base = window.location.pathname.replace(/\/$/, ""); await fetch(`${base}/api/approvals/${id}/sign`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vote: "APPROVE" }), }); this.loadApprovals(); }); }); this.shadow.querySelectorAll("[data-reject]").forEach((btn) => { btn.addEventListener("click", async () => { if (this.space === "demo") { alert("Approvals are disabled in demo mode."); return; } const id = (btn as HTMLElement).dataset.reject!; const base = window.location.pathname.replace(/\/$/, ""); await fetch(`${base}/api/approvals/${id}/sign`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vote: "REJECT" }), }); this.loadApprovals(); }); }); } } customElements.define("folk-inbox-client", FolkInboxClient);