refactor(rinbox): replace forwarding banner with popup modal

Convert inline forwarding banner to a click-triggered modal overlay.
Fix API field name bug (data.target → data.forwardsTo). Add email
input for no-email state with sovereignty messaging. Remove dismiss
logic in favor of modal open/close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-20 12:15:53 -07:00
parent 42a84cb72e
commit ad9e54dbe9
1 changed files with 185 additions and 93 deletions

View File

@ -39,7 +39,7 @@ class FolkInboxClient extends HTMLElement {
private _fwdStatus: 'loading' | 'unavailable' | 'no-email' | 'ready' | 'enabled' | 'error' = 'loading';
private _fwdAddress = '';
private _fwdTarget = '';
private _fwdDismissed = false;
private _fwdModalOpen = false;
private _fwdBusy = false;
private _fwdError = '';
private demoApprovals: any[] = [
@ -295,11 +295,6 @@ class FolkInboxClient extends HTMLElement {
}
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 {
@ -311,12 +306,12 @@ class FolkInboxClient extends HTMLElement {
if (data.enabled) {
this._fwdStatus = 'enabled';
this._fwdAddress = data.address || '';
this._fwdTarget = data.target || '';
} else if (data.available && data.target) {
this._fwdTarget = data.forwardsTo || '';
} else if (data.available && data.forwardsTo) {
this._fwdStatus = 'ready';
this._fwdAddress = data.address || '';
this._fwdTarget = data.target || '';
} else if (!data.target) {
this._fwdTarget = data.forwardsTo || '';
} else if (!data.forwardsTo) {
this._fwdStatus = 'no-email';
this._fwdAddress = data.address || '';
} else {
@ -372,77 +367,151 @@ class FolkInboxClient extends HTMLElement {
this.render();
}
private _dismissForwardBanner() {
this._fwdDismissed = true;
sessionStorage.setItem('rinbox_fwd_dismissed', '1');
private async _saveEmailAndEnable() {
if (this._fwdBusy) return;
const input = this.shadow.getElementById('fwd-email-input') as HTMLInputElement | null;
const email = input?.value.trim();
if (!email) return;
this._fwdBusy = true;
this._fwdError = '';
this.render();
const token = getAccessToken();
try {
// Save email to profile
const profileResp = await fetch('https://auth.rspace.online/api/user/profile', {
method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ profileEmail: email }),
});
if (!profileResp.ok) {
const err = await profileResp.json().catch(() => ({}));
this._fwdError = (err as any).error || 'Failed to save email';
this._fwdStatus = 'error';
this._fwdBusy = false;
this.render();
return;
}
// Enable forwarding
const fwdResp = await fetch('https://auth.rspace.online/api/account/email-forward/enable', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
});
if (fwdResp.ok) {
this._fwdStatus = 'enabled';
this._fwdTarget = email;
} else {
const err = await fwdResp.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 renderForwardBanner(): string {
if (this._fwdDismissed || this._fwdStatus === 'loading' || this._fwdStatus === 'unavailable') return '';
if (this._fwdStatus === 'ready') {
return `
<div class="fwd-banner fwd-ready">
<div class="fwd-banner-content">
<div class="fwd-banner-title">Forward rInbox to your email</div>
<div class="fwd-banner-route">
<code>${this.escapeHtml(this._fwdAddress)}</code>
<span class="fwd-arrow">&rarr;</span>
<code>${this.escapeHtml(this._fwdTarget)}</code>
</div>
</div>
<div class="fwd-banner-actions">
<button class="fwd-btn-enable" data-action="fwd-enable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Enabling...' : 'Enable Forwarding'}</button>
<button class="fwd-btn-dismiss" data-action="fwd-dismiss">Dismiss</button>
</div>
</div>`;
}
private renderForwardTrigger(): string {
if (this._fwdStatus === 'loading' || this._fwdStatus === 'unavailable') return '';
if (this._fwdStatus === 'enabled') {
return `
<div class="fwd-banner fwd-enabled">
<div class="fwd-banner-content">
<div class="fwd-banner-title">Forwarding active</div>
<div class="fwd-banner-route">
<div class="fwd-trigger fwd-trigger-enabled" data-action="fwd-open">
<span class="fwd-trigger-dot fwd-dot-active"></span>
<span>Forwarding active: <code>${this.escapeHtml(this._fwdAddress)}</code> &rarr; <code>${this.escapeHtml(this._fwdTarget)}</code></span>
<span class="fwd-trigger-link">Manage</span>
</div>`;
}
return `
<div class="fwd-trigger" data-action="fwd-open">
<span class="fwd-trigger-icon">&#128233;</span>
<span class="fwd-trigger-link">Forward rInbox to your email &rarr;</span>
</div>`;
}
private renderForwardModal(): string {
if (!this._fwdModalOpen) return '';
let content = '';
if (this._fwdStatus === 'ready') {
content = `
<div class="fwd-modal-status">
<div class="fwd-modal-route">
<div class="fwd-modal-route-line">
<code>${this.escapeHtml(this._fwdAddress)}</code>
<span class="fwd-arrow">&rarr;</span>
<code>${this.escapeHtml(this._fwdTarget)}</code>
</div>
</div>
<div class="fwd-banner-actions">
<button class="fwd-btn-disable" data-action="fwd-disable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Disabling...' : 'Disable'}</button>
<p class="fwd-modal-desc">Mail sent to your rInbox address will be forwarded to your personal email.</p>
<div class="fwd-sovereignty">
&#128274; Your email is stored locally with your EncryptID keys sovereign and encrypted on your device.
</div>
</div>
<div class="fwd-modal-actions">
<button class="fwd-btn-enable" data-action="fwd-enable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Enabling...' : 'Enable Forwarding'}</button>
<button class="fwd-btn-cancel" data-action="fwd-close">Cancel</button>
</div>`;
} else if (this._fwdStatus === 'enabled') {
content = `
<div class="fwd-modal-status">
<div class="fwd-modal-status-badge fwd-status-active">
<span class="fwd-trigger-dot fwd-dot-active"></span>
Forwarding Active
</div>
<div class="fwd-modal-route">
<div class="fwd-modal-route-line">
<code>${this.escapeHtml(this._fwdAddress)}</code>
<span class="fwd-arrow">&rarr;</span>
<code>${this.escapeHtml(this._fwdTarget)}</code>
</div>
</div>
<div class="fwd-sovereignty">
&#128274; Your email is stored locally with your EncryptID keys sovereign and encrypted on your device.
</div>
</div>
<div class="fwd-modal-actions">
<button class="fwd-btn-disable" data-action="fwd-disable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Disabling...' : 'Disable Forwarding'}</button>
<button class="fwd-btn-cancel" data-action="fwd-close">Close</button>
</div>`;
} else if (this._fwdStatus === 'no-email') {
content = `
<div class="fwd-modal-status">
<p class="fwd-modal-desc">Connect an email address to receive forwarded mail from your rInbox.</p>
<div class="fwd-sovereignty">
&#128274; Your email is stored locally with your EncryptID keys never on a central server. Only you control your contact info.
</div>
<div class="compose-field" style="margin-top:1rem">
<label>Your email address</label>
<input id="fwd-email-input" type="email" placeholder="you@example.com" />
</div>
</div>
<div class="fwd-modal-actions">
<button class="fwd-btn-enable" data-action="fwd-save-enable" ${this._fwdBusy ? 'disabled' : ''}>${this._fwdBusy ? 'Saving...' : 'Save &amp; Enable'}</button>
<button class="fwd-btn-cancel" data-action="fwd-close">Cancel</button>
</div>`;
} else if (this._fwdStatus === 'error') {
content = `
<div class="fwd-modal-status">
<div class="fwd-modal-error">${this.escapeHtml(this._fwdError)}</div>
</div>
<div class="fwd-modal-actions">
<button class="fwd-btn-enable" data-action="fwd-enable">Retry</button>
<button class="fwd-btn-cancel" data-action="fwd-close">Close</button>
</div>`;
}
if (this._fwdStatus === 'no-email') {
return `
<div class="fwd-banner fwd-no-email">
<div class="fwd-banner-content">
<div class="fwd-banner-title">Forward rInbox to your email</div>
<div class="fwd-banner-desc">Add a verified email to your profile to enable forwarding.</div>
</div>
<div class="fwd-banner-actions">
<a class="fwd-btn-profile" href="https://auth.rspace.online/profile" target="_blank" rel="noopener">Go to Profile</a>
<button class="fwd-btn-dismiss" data-action="fwd-dismiss">Dismiss</button>
</div>
</div>`;
}
if (this._fwdStatus === 'error') {
return `
<div class="fwd-banner fwd-error">
<div class="fwd-banner-content">
<div class="fwd-banner-title">${this.escapeHtml(this._fwdError)}</div>
</div>
<div class="fwd-banner-actions">
<button class="fwd-btn-dismiss" data-action="fwd-dismiss">Dismiss</button>
</div>
</div>`;
}
return '';
return `
<div class="fwd-overlay" data-action="fwd-close-overlay">
<div class="fwd-modal">
<button class="help-close" data-action="fwd-close">&times;</button>
<div class="fwd-modal-title">&#128233; Email Forwarding</div>
${content}
</div>
</div>`;
}
private timeAgo(dateStr: string): string {
@ -663,31 +732,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; }
/* Forwarding trigger */
.fwd-trigger { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; margin-bottom: 12px; cursor: pointer; border-radius: 8px; font-size: 0.8rem; color: var(--rs-text-secondary); transition: background 0.15s; }
.fwd-trigger:hover { background: var(--rs-bg-hover); }
.fwd-trigger-enabled { color: #4ade80; }
.fwd-trigger-enabled code { font-size: 0.75rem; color: #4ade80; background: rgba(34,197,94,0.1); padding: 2px 6px; border-radius: 4px; }
.fwd-trigger-icon { font-size: 1rem; }
.fwd-trigger-link { color: #818cf8; font-weight: 500; }
.fwd-trigger-enabled .fwd-trigger-link { color: rgba(74,222,128,0.7); font-weight: 400; margin-left: auto; }
.fwd-trigger-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; }
.fwd-dot-active { background: #4ade80; box-shadow: 0 0 6px rgba(74,222,128,0.5); }
/* Forwarding modal overlay */
.fwd-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.fwd-modal { background: var(--rs-bg-surface-sunken); border: 1px solid var(--rs-border-strong); border-radius: 16px; max-width: 480px; width: 100%; padding: 2rem; position: relative; }
.fwd-modal-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 1.25rem; color: var(--rs-text-primary); }
.fwd-modal-status { margin-bottom: 1.25rem; }
.fwd-modal-status-badge { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.75rem; border-radius: 6px; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.75rem; }
.fwd-status-active { background: rgba(34,197,94,0.1); color: #4ade80; }
.fwd-modal-route { margin: 0.75rem 0; }
.fwd-modal-route-line { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.fwd-modal-route-line code { font-size: 0.8rem; color: #818cf8; background: rgba(99,102,241,0.1); padding: 3px 8px; border-radius: 4px; }
.fwd-arrow { color: var(--rs-text-muted); font-size: 0.85rem; }
.fwd-modal-desc { font-size: 0.85rem; color: var(--rs-text-secondary); line-height: 1.5; margin: 0.5rem 0; }
.fwd-sovereignty { font-size: 0.8rem; color: var(--rs-text-muted); line-height: 1.5; padding: 0.75rem; background: rgba(99,102,241,0.06); border: 1px solid rgba(99,102,241,0.15); border-radius: 8px; margin-top: 0.75rem; }
.fwd-modal-error { font-size: 0.85rem; color: #f87171; padding: 0.75rem; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2); border-radius: 8px; }
.fwd-modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
.fwd-btn-enable { padding: 8px 18px; border-radius: 8px; border: none; background: linear-gradient(135deg, #6366f1, #0891b2); color: white; cursor: pointer; font-size: 0.85rem; 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 { padding: 8px 18px; border-radius: 8px; border: 1px solid rgba(239,68,68,0.3); background: transparent; color: #f87171; cursor: pointer; font-size: 0.85rem; font-weight: 500; transition: all 0.15s; }
.fwd-btn-disable:hover { border-color: rgba(239,68,68,0.5); background: rgba(239,68,68,0.08); }
.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; }
.fwd-btn-cancel { padding: 8px 18px; border-radius: 8px; border: 1px solid var(--rs-border-strong); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.85rem; transition: all 0.15s; }
.fwd-btn-cancel:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); }
@media (max-width: 768px) {
.mailbox-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
@ -695,8 +773,7 @@ class FolkInboxClient extends HTMLElement {
.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; }
.fwd-modal { padding: 1.25rem; }
.rapp-nav { gap: 0.25rem; }
.nav-btn { padding: 4px 8px; font-size: 12px; }
.inbox-header { padding: 0.625rem 0.75rem; gap: 0.5rem; }
@ -715,9 +792,10 @@ class FolkInboxClient extends HTMLElement {
<div class="container">
${this.renderNav()}
${this.showingSampleData ? '<div class="sample-banner">Showing sample data — create a mailbox to get started</div>' : ''}
${this.view === 'mailboxes' ? this.renderForwardBanner() : ''}
${this.view === 'mailboxes' ? this.renderForwardTrigger() : ''}
${this.renderView()}
${this.helpOpen ? this.renderHelp() : ""}
${this.renderForwardModal()}
</div>
`;
this.bindEvents();
@ -1337,10 +1415,24 @@ class FolkInboxClient extends HTMLElement {
helpClose.addEventListener("click", () => { this.helpOpen = false; this.render(); });
}
// Forwarding banner actions
// Forwarding trigger + modal actions
this.shadow.querySelector("[data-action='fwd-open']")?.addEventListener("click", () => {
this._fwdModalOpen = true; this.render();
});
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());
this.shadow.querySelector("[data-action='fwd-save-enable']")?.addEventListener("click", () => this._saveEmailAndEnable());
this.shadow.querySelectorAll("[data-action='fwd-close']").forEach(btn => {
btn.addEventListener("click", () => { this._fwdModalOpen = false; this.render(); });
});
const fwdOverlay = this.shadow.querySelector("[data-action='fwd-close-overlay']");
if (fwdOverlay) {
fwdOverlay.addEventListener("click", (e) => {
if ((e.target as HTMLElement).dataset.action === "fwd-close-overlay") {
this._fwdModalOpen = false; this.render();
}
});
}
// Mailbox click
this.shadow.querySelectorAll("[data-mailbox]").forEach((card) => {