diff --git a/modules/rinbox/components/folk-inbox-client.ts b/modules/rinbox/components/folk-inbox-client.ts index 6070f39..d3c2f0e 100644 --- a/modules/rinbox/components/folk-inbox-client.ts +++ b/modules/rinbox/components/folk-inbox-client.ts @@ -10,6 +10,7 @@ import { mailboxSchema, type MailboxDoc } from "../schemas"; import type { DocumentId } from "../../../shared/local-first/document"; import { TourEngine } from "../../../shared/tour-engine"; import { ViewHistory } from "../../../shared/view-history.js"; +import { getAccessToken, getUsername } from "../../../lib/rspace-header"; type ComposeMode = 'new' | 'reply' | 'reply-all' | 'forward'; @@ -35,6 +36,12 @@ class FolkInboxClient extends HTMLElement { private _usernameCache = new Map(); private _currentUsername: string | null = null; private _history = new ViewHistory<"mailboxes" | "threads" | "thread" | "approvals" | "personal" | "agents">("mailboxes"); + private _fwdStatus: 'loading' | 'unavailable' | 'no-email' | 'ready' | 'enabled' | 'error' = 'loading'; + private _fwdAddress = ''; + private _fwdTarget = ''; + private _fwdDismissed = false; + private _fwdBusy = false; + private _fwdError = ''; private demoApprovals: any[] = [ { id: "a1", subject: "Re: Q1 Budget approval for rSpace infrastructure", status: "PENDING", @@ -106,6 +113,7 @@ class FolkInboxClient extends HTMLElement { connectedCallback() { this.space = this.getAttribute("space") || "demo"; this._loadUsername(); + this._checkForwardStatus(); if (this.space === "demo") { this.loadDemoData(); } else { this.subscribeOffline(); this.loadMailboxes(); } // Auto-start tour on first visit @@ -119,14 +127,9 @@ class FolkInboxClient extends HTMLElement { this._offlineUnsubs = []; } - /** Extract username from EncryptID JWT in localStorage */ + /** Extract username from EncryptID session */ private _loadUsername() { - try { - const token = localStorage.getItem('encryptid-token'); - if (!token) return; - const payload = JSON.parse(atob(token.split('.')[1])); - if (payload.username) this._currentUsername = payload.username; - } catch { /* no token or invalid */ } + this._currentUsername = getUsername(); } /** Resolve a DID to a display name */ @@ -253,7 +256,7 @@ class FolkInboxClient extends HTMLElement { } try { const base = window.location.pathname.replace(/\/$/, ""); - const token = localStorage.getItem('encryptid-token'); + const token = getAccessToken(); const resp = await fetch(`${base}/api/personal-inboxes`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); @@ -286,11 +289,162 @@ class FolkInboxClient extends HTMLElement { private getAuthHeaders(): Record { const headers: Record = { "Content-Type": "application/json" }; - const token = localStorage.getItem('encryptid-token'); + const token = getAccessToken(); if (token) headers["Authorization"] = `Bearer ${token}`; return headers; } + private async _checkForwardStatus() { + if (sessionStorage.getItem('rinbox_fwd_dismissed')) { + this._fwdDismissed = true; + this._fwdStatus = 'unavailable'; + return; + } + const token = getAccessToken(); + if (!token) { this._fwdStatus = 'unavailable'; return; } + try { + const resp = await fetch('https://auth.rspace.online/api/account/email-forward', { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!resp.ok) { this._fwdStatus = 'unavailable'; return; } + const data = await resp.json(); + if (data.enabled) { + this._fwdStatus = 'enabled'; + this._fwdAddress = data.address || ''; + this._fwdTarget = data.target || ''; + } else if (data.available && data.target) { + this._fwdStatus = 'ready'; + this._fwdAddress = data.address || ''; + this._fwdTarget = data.target || ''; + } else if (!data.target) { + this._fwdStatus = 'no-email'; + this._fwdAddress = data.address || ''; + } else { + this._fwdStatus = 'unavailable'; + } + } catch { + this._fwdStatus = 'unavailable'; + } + this.render(); + } + + private async _enableForwarding() { + if (this._fwdBusy) return; + this._fwdBusy = true; + this._fwdError = ''; + this.render(); + const token = getAccessToken(); + try { + const resp = await fetch('https://auth.rspace.online/api/account/email-forward/enable', { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + }); + if (resp.ok) { + this._fwdStatus = 'enabled'; + } else { + const err = await resp.json().catch(() => ({})); + this._fwdError = (err as any).error || 'Failed to enable forwarding'; + this._fwdStatus = 'error'; + } + } catch { + this._fwdError = 'Network error — try again'; + this._fwdStatus = 'error'; + } + this._fwdBusy = false; + this.render(); + } + + private async _disableForwarding() { + if (this._fwdBusy) return; + this._fwdBusy = true; + this.render(); + const token = getAccessToken(); + try { + const resp = await fetch('https://auth.rspace.online/api/account/email-forward/disable', { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + }); + if (resp.ok) { + this._fwdStatus = 'ready'; + } + } catch { /* ignore */ } + this._fwdBusy = false; + this.render(); + } + + private _dismissForwardBanner() { + this._fwdDismissed = true; + sessionStorage.setItem('rinbox_fwd_dismissed', '1'); + this.render(); + } + + private renderForwardBanner(): string { + if (this._fwdDismissed || this._fwdStatus === 'loading' || this._fwdStatus === 'unavailable') return ''; + + if (this._fwdStatus === 'ready') { + return ` +
+
+
Forward rInbox to your email
+
+ ${this.escapeHtml(this._fwdAddress)} + + ${this.escapeHtml(this._fwdTarget)} +
+
+
+ + +
+
`; + } + + if (this._fwdStatus === 'enabled') { + return ` +
+
+
Forwarding active
+
+ ${this.escapeHtml(this._fwdAddress)} + + ${this.escapeHtml(this._fwdTarget)} +
+
+
+ +
+
`; + } + + if (this._fwdStatus === 'no-email') { + return ` +
+
+
Forward rInbox to your email
+
Add a verified email to your profile to enable forwarding.
+
+
+ Go to Profile + +
+
`; + } + + if (this._fwdStatus === 'error') { + return ` +
+
+
${this.escapeHtml(this._fwdError)}
+
+
+ +
+
`; + } + + return ''; + } + private timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); @@ -509,12 +663,40 @@ class FolkInboxClient extends HTMLElement { .sample-banner { padding: 8px 16px; background: rgba(99,102,241,0.12); border: 1px solid rgba(99,102,241,0.25); border-radius: 8px; color: #a5b4fc; font-size: 13px; text-align: center; margin-bottom: 12px; } + /* Forwarding banner */ + .fwd-banner { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; border-radius: 10px; margin-bottom: 12px; border: 1px solid; } + .fwd-banner.fwd-ready { background: rgba(99,102,241,0.08); border-color: rgba(99,102,241,0.25); } + .fwd-banner.fwd-enabled { background: rgba(34,197,94,0.08); border-color: rgba(34,197,94,0.25); } + .fwd-banner.fwd-no-email { background: rgba(251,191,36,0.08); border-color: rgba(251,191,36,0.25); } + .fwd-banner.fwd-error { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.25); } + .fwd-banner-content { flex: 1; min-width: 0; } + .fwd-banner-title { font-size: 0.85rem; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 2px; } + .fwd-banner-desc { font-size: 0.8rem; color: var(--rs-text-secondary); } + .fwd-banner-route { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; } + .fwd-banner-route code { font-size: 0.75rem; color: #818cf8; background: rgba(99,102,241,0.1); padding: 2px 6px; border-radius: 4px; } + .fwd-enabled .fwd-banner-route code { color: #4ade80; background: rgba(34,197,94,0.1); } + .fwd-arrow { color: var(--rs-text-muted); font-size: 0.8rem; } + .fwd-banner-actions { display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0; } + .fwd-btn-enable { padding: 6px 14px; border-radius: 6px; border: none; background: linear-gradient(135deg, #6366f1, #0891b2); color: white; cursor: pointer; font-size: 0.8rem; font-weight: 600; transition: opacity 0.15s; } + .fwd-btn-enable:hover { opacity: 0.9; } + .fwd-btn-enable:disabled { opacity: 0.5; cursor: not-allowed; } + .fwd-btn-disable { padding: 6px 12px; border-radius: 6px; border: 1px solid rgba(34,197,94,0.3); background: transparent; color: #4ade80; cursor: pointer; font-size: 0.75rem; transition: all 0.15s; } + .fwd-btn-disable:hover { border-color: rgba(34,197,94,0.6); background: rgba(34,197,94,0.1); } + .fwd-btn-disable:disabled { opacity: 0.5; cursor: not-allowed; } + .fwd-btn-dismiss { padding: 6px 10px; border-radius: 6px; border: none; background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 0.75rem; transition: color 0.15s; } + .fwd-btn-dismiss:hover { color: var(--rs-text-secondary); } + .fwd-btn-profile { padding: 6px 14px; border-radius: 6px; border: 1px solid rgba(251,191,36,0.3); background: rgba(251,191,36,0.1); color: #fbbf24; text-decoration: none; font-size: 0.8rem; font-weight: 500; transition: all 0.15s; } + .fwd-btn-profile:hover { border-color: rgba(251,191,36,0.5); background: rgba(251,191,36,0.15); } + .fwd-error .fwd-banner-title { color: #f87171; } + @media (max-width: 768px) { .mailbox-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } .help-grid { grid-template-columns: 1fr; } .thread-row { flex-wrap: wrap; gap: 4px; } } @media (max-width: 480px) { + .fwd-banner { flex-direction: column; align-items: stretch; gap: 0.5rem; } + .fwd-banner-actions { justify-content: flex-end; } .rapp-nav { gap: 0.25rem; } .nav-btn { padding: 4px 8px; font-size: 12px; } .inbox-header { padding: 0.625rem 0.75rem; gap: 0.5rem; } @@ -533,6 +715,7 @@ class FolkInboxClient extends HTMLElement {
${this.renderNav()} ${this.showingSampleData ? '
Showing sample data — create a mailbox to get started
' : ''} + ${this.view === 'mailboxes' ? this.renderForwardBanner() : ''} ${this.renderView()} ${this.helpOpen ? this.renderHelp() : ""}
@@ -1154,6 +1337,11 @@ class FolkInboxClient extends HTMLElement { helpClose.addEventListener("click", () => { this.helpOpen = false; this.render(); }); } + // Forwarding banner actions + this.shadow.querySelector("[data-action='fwd-enable']")?.addEventListener("click", () => this._enableForwarding()); + this.shadow.querySelector("[data-action='fwd-disable']")?.addEventListener("click", () => this._disableForwarding()); + this.shadow.querySelector("[data-action='fwd-dismiss']")?.addEventListener("click", () => this._dismissForwardBanner()); + // Mailbox click this.shadow.querySelectorAll("[data-mailbox]").forEach((card) => { card.addEventListener("click", () => {