From fab970e439038293553d2e1ba43df51eddd62ee7 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 13:09:45 -0700 Subject: [PATCH 1/3] fix(encryptid): use CONFIG.smtp.from for OIDC verification emails The sendVerificationEmail function was hardcoding noreply@ridentity.online as the sender, but SMTP authenticates as noreply@rspace.online. Mailcow rejected the mismatch with 553 "Sender address rejected: not owned by user". Co-Authored-By: Claude Opus 4.6 --- src/encryptid/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 1e2c5ed..76f05bb 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -283,7 +283,7 @@ async function sendVerificationEmail(to: string, token: string, username: string } await smtpTransport.sendMail({ - from: 'EncryptID ', + from: CONFIG.smtp.from, to, subject: 'rIdentity — Verify your email address', text: [ From 233b7e3689671b6ca687f9f4c7f2526064442c73 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 13:41:08 -0700 Subject: [PATCH 2/3] feat(responsive): adaptive tablet/fold breakpoints, touch/pen parity, pointer events drag Add intermediate breakpoints (960px, 1024px, 900px, 640px, 600px) for tablets and fold devices across all 12 rApp components. Add touch-action: manipulation and -webkit-tap-highlight-color to eliminate 300ms tap delay. Fix undersized tap targets (<36px) in rtasks, rfiles, rinbox, and rcart. Replace HTML5 drag API with pointer events in rtasks kanban and rchoices ranking for touch/pen/mouse parity. Replace mouseenter/ mouseleave with pointerenter/pointerleave in rchoices spider chart with click toggle. Co-Authored-By: Claude Opus 4.6 --- modules/rcal/components/folk-calendar-view.ts | 13 ++- .../rcart/components/folk-group-buy-page.ts | 16 ++-- modules/rcart/components/folk-payment-page.ts | 8 +- .../rcart/components/folk-payment-request.ts | 7 +- .../components/folk-choices-dashboard.ts | 76 ++++++++++------- .../rfiles/components/folk-file-browser.ts | 5 +- .../rforum/components/folk-forum-dashboard.ts | 3 +- .../rinbox/components/folk-inbox-client.ts | 10 +-- modules/rnotes/components/folk-notes-app.ts | 6 +- modules/rtasks/components/folk-tasks-board.ts | 82 ++++++++++--------- .../rvote/components/folk-vote-dashboard.ts | 7 +- .../rwallet/components/folk-wallet-viewer.ts | 7 +- 12 files changed, 150 insertions(+), 90 deletions(-) diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index ea6492f..1ffe4e0 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -2298,8 +2298,9 @@ class FolkCalendarView extends HTMLElement { private getStyles(): string { return ` - :host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); padding: 0.5rem; } + :host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); padding: 0.5rem; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; } + button, a, input, select, textarea, [role="button"] { touch-action: manipulation; } .error { color: var(--rs-error); text-align: center; padding: 8px; } @@ -2606,6 +2607,16 @@ class FolkCalendarView extends HTMLElement { .kbd-hint { text-align: center; font-size: 10px; color: var(--rs-text-muted); margin-top: 12px; padding-top: 8px; border-top: 1px solid var(--rs-border-subtle); } .kbd-hint kbd { padding: 1px 4px; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 3px; font-family: inherit; font-size: 9px; } + /* ── Tablet/Adaptive ── */ + @media (max-width: 1024px) { + .main-layout--docked { grid-template-columns: 1fr auto 320px; } + } + @media (max-width: 900px) and (min-width: 769px) { + .main-layout--docked { grid-template-columns: 1fr; } + .zoom-bar--middle { display: none; } + .ev-label { display: none; } + .day { min-height: 64px; } + } /* ── Mobile ── */ @media (max-width: 768px) { :host { padding: 0.25rem; } diff --git a/modules/rcart/components/folk-group-buy-page.ts b/modules/rcart/components/folk-group-buy-page.ts index 7b7383d..f09ea68 100644 --- a/modules/rcart/components/folk-group-buy-page.ts +++ b/modules/rcart/components/folk-group-buy-page.ts @@ -436,8 +436,9 @@ class FolkGroupBuyPage extends HTMLElement { private getStyles(): string { return ` - :host { display: block; padding: 2rem 1.5rem; width: 100%; max-width: 960px; } + :host { display: block; padding: 2rem 1.5rem; width: 100%; max-width: 960px; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; } + button, a, input, select, textarea, [role="button"] { touch-action: manipulation; } .loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); } .error { background: rgba(239,68,68,0.1); border: 1px solid var(--rs-error); border-radius: 8px; padding: 1.5rem; color: #fca5a5; text-align: center; } @@ -585,7 +586,7 @@ class FolkGroupBuyPage extends HTMLElement { } .btn-pledge:hover { background: linear-gradient(135deg, #16a34a, #15803d); box-shadow: 0 4px 12px rgba(34,197,94,0.4); } .btn-pledge:disabled { opacity: 0.5; cursor: not-allowed; } - .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; } + .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; min-height: 36px; min-width: 36px; } .btn-lg { padding: 0.875rem; font-size: 1rem; width: 100%; } .pledge-disclaimer { color: var(--rs-text-muted); font-size: 0.6875rem; text-align: center; margin: 0; line-height: 1.4; } @@ -628,6 +629,11 @@ class FolkGroupBuyPage extends HTMLElement { .commons-stat-projected .commons-stat-value { color: #a855f7; } .commons-stat-arrow { color: var(--rs-text-muted); font-size: 1.25rem; } + @media (max-width: 960px) and (min-width: 769px) { + .main-grid { grid-template-columns: 1fr 280px; } + .hero-img { width: 160px; height: 160px; } + .tier-commons { display: none; } + } @media (max-width: 768px) { :host { padding: 1.25rem 1rem; } .hero { flex-direction: column; padding: 1.25rem; } @@ -638,6 +644,9 @@ class FolkGroupBuyPage extends HTMLElement { .commons-stats { flex-wrap: wrap; gap: 0.5rem; } .fill-visual { flex-direction: column; align-items: center; } .fill-visual__marker-label { display: none; } + .sim-controls { flex-direction: column; align-items: stretch; gap: 0.5rem; } + .sim-label { white-space: normal; } + .commons-controls { flex-direction: column; align-items: stretch; gap: 0.5rem; } } @media (max-width: 480px) { :host { padding: 1rem 0.75rem; } @@ -649,9 +658,6 @@ class FolkGroupBuyPage extends HTMLElement { .tier-row { padding: 0.625rem 0.75rem; gap: 0.5rem; } .tier-qty { font-size: 0.8125rem; min-width: 2.5rem; } .tier-price { font-size: 0.8125rem; } - .sim-controls { flex-direction: column; align-items: stretch; gap: 0.5rem; } - .sim-label { white-space: normal; } - .commons-controls { flex-direction: column; align-items: stretch; gap: 0.5rem; } .pledge-panel { padding: 1rem; } h3 { font-size: 0.9375rem; } } diff --git a/modules/rcart/components/folk-payment-page.ts b/modules/rcart/components/folk-payment-page.ts index 0d7a9ee..3e1be4d 100644 --- a/modules/rcart/components/folk-payment-page.ts +++ b/modules/rcart/components/folk-payment-page.ts @@ -628,8 +628,9 @@ class FolkPaymentPage extends HTMLElement { private getStyles(): string { return ` - :host { display: block; padding: 1.5rem; width: 100%; max-width: 560px; } + :host { display: block; padding: 1.5rem; width: 100%; max-width: 560px; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; } + button, a, input, select, textarea, [role="button"] { touch-action: manipulation; } .payment-page { } @@ -712,7 +713,7 @@ class FolkPaymentPage extends HTMLElement { .terminal-msg { text-align: center; padding: 2rem; color: var(--rs-text-muted); font-size: 0.875rem; } .transak-container { border-radius: 8px; overflow: hidden; border: 1px solid var(--rs-border); } - .transak-iframe { width: 100%; height: 600px; border: none; } + .transak-iframe { width: 100%; height: clamp(400px, 72vh, 600px); border: none; } .footer { margin-top: 2rem; border-top: 1px solid var(--rs-border); padding-top: 1.5rem; } .qr-section { display: flex; flex-direction: column; align-items: center; gap: 1rem; } @@ -723,6 +724,9 @@ class FolkPaymentPage extends HTMLElement { .loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); } .error { text-align: center; padding: 3rem; color: #f87171; } + @media (max-width: 600px) { + :host { padding: 1.25rem 1rem; } + } @media (max-width: 480px) { :host { padding: 1rem; } .header { flex-direction: column; align-items: flex-start; gap: 0.5rem; } diff --git a/modules/rcart/components/folk-payment-request.ts b/modules/rcart/components/folk-payment-request.ts index 8afc649..04f375d 100644 --- a/modules/rcart/components/folk-payment-request.ts +++ b/modules/rcart/components/folk-payment-request.ts @@ -561,8 +561,9 @@ class FolkPaymentRequest extends HTMLElement { private getStyles(): string { return ` - :host { display: block; padding: 1.5rem; width: 100%; max-width: 520px; } + :host { display: block; padding: 1.5rem; width: 100%; max-width: 520px; -webkit-tap-highlight-color: transparent; } * { box-sizing: border-box; } + button, a, input, select, textarea, [role="button"] { touch-action: manipulation; } .page-title { color: var(--rs-text-primary); font-size: 1.5rem; font-weight: 700; margin: 0 0 0.25rem; text-align: center; } .page-subtitle { color: var(--rs-text-secondary); font-size: 0.9375rem; text-align: center; margin: 0 0 2rem; } @@ -642,6 +643,10 @@ class FolkPaymentRequest extends HTMLElement { .share-input { flex: 1; padding: 0.5rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.75rem; font-family: monospace; } .action-row { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; } + @media (max-width: 600px) { + .toggle-btn { font-size: 0.75rem; } + .method-desc { display: none; } + } @media (max-width: 480px) { :host { padding: 1rem; } .page-title { font-size: 1.25rem; } diff --git a/modules/rchoices/components/folk-choices-dashboard.ts b/modules/rchoices/components/folk-choices-dashboard.ts index ea12111..ffc2696 100644 --- a/modules/rchoices/components/folk-choices-dashboard.ts +++ b/modules/rchoices/components/folk-choices-dashboard.ts @@ -94,7 +94,8 @@ class FolkChoicesDashboard extends HTMLElement { this.shadow.innerHTML = `
- ${campaignHTML} + ${contentHTML}
+ `; this.bindEvents(); @@ -282,17 +426,18 @@ export class FolkCampaignManager extends HTMLElement { // Tour button this.shadowRoot.getElementById('btn-tour')?.addEventListener('click', () => this.startTour()); - const modal = this.shadowRoot.getElementById('import-modal') as HTMLElement; - const openBtn = this.shadowRoot.getElementById('import-md-btn'); - const closeBtn = this.shadowRoot.getElementById('import-modal-close'); + // ── Import modal ── + const importModal = this.shadowRoot.getElementById('import-modal') as HTMLElement; + const importOpenBtn = this.shadowRoot.getElementById('import-md-btn'); + const importCloseBtn = this.shadowRoot.getElementById('import-modal-close'); const parseBtn = this.shadowRoot.getElementById('import-parse-btn'); const mdInput = this.shadowRoot.getElementById('import-md-textarea') as HTMLTextAreaElement; const platformSel = this.shadowRoot.getElementById('import-platform') as HTMLSelectElement; const importedEl = this.shadowRoot.getElementById('imported-posts'); - openBtn?.addEventListener('click', () => { modal.hidden = false; }); - closeBtn?.addEventListener('click', () => { modal.hidden = true; }); - modal?.addEventListener('click', (e) => { if (e.target === modal) modal.hidden = true; }); + importOpenBtn?.addEventListener('click', () => { importModal.hidden = false; }); + importCloseBtn?.addEventListener('click', () => { importModal.hidden = true; }); + importModal?.addEventListener('click', (e) => { if (e.target === importModal) importModal.hidden = true; }); parseBtn?.addEventListener('click', () => { const raw = mdInput.value; @@ -301,7 +446,6 @@ export class FolkCampaignManager extends HTMLElement { const platform = platformSel.value; const total = tweets.length; - // Build imported posts as campaign posts and save to Automerge const posts: CampaignPost[] = tweets.map((text, i) => ({ id: `imported-${Date.now()}-${i}`, platform, @@ -315,7 +459,6 @@ export class FolkCampaignManager extends HTMLElement { phaseLabel: 'Imported', })); - // Render imported posts inline let html = `

📥 Imported Posts (${total})

`; html += '
'; tweets.forEach((text, i) => { @@ -332,9 +475,8 @@ export class FolkCampaignManager extends HTMLElement { }); html += '
'; importedEl.innerHTML = html; - modal.hidden = true; + importModal.hidden = true; - // Save to Automerge if runtime available if (this._space !== 'demo') { const c = this._campaigns[0]; if (c) { @@ -343,6 +485,128 @@ export class FolkCampaignManager extends HTMLElement { } } }); + + // ── Generate modal ── + const genModal = this.shadowRoot.getElementById('generate-modal') as HTMLElement; + const genOpenBtn = this.shadowRoot.getElementById('generate-btn'); + const genCloseBtn = this.shadowRoot.getElementById('generate-modal-close'); + const genSubmit = this.shadowRoot.getElementById('gen-submit') as HTMLButtonElement; + const genBrief = this.shadowRoot.getElementById('gen-brief') as HTMLTextAreaElement; + const genError = this.shadowRoot.getElementById('gen-error') as HTMLElement; + + genOpenBtn?.addEventListener('click', () => { + genModal.hidden = false; + // Restore last brief if regenerating + if (this._lastBrief && genBrief) genBrief.value = this._lastBrief; + }); + genCloseBtn?.addEventListener('click', () => { genModal.hidden = true; }); + genModal?.addEventListener('click', (e) => { if (e.target === genModal) genModal.hidden = true; }); + + genSubmit?.addEventListener('click', () => this.handleGenerate()); + + // ── Preview mode buttons ── + this.shadowRoot.getElementById('preview-save')?.addEventListener('click', () => { + if (!this._generatedCampaign) return; + this.saveCampaignToDoc(this._generatedCampaign); + this._campaigns.unshift(this._generatedCampaign); + this._generatedCampaign = null; + this._previewMode = false; + this.render(); + }); + + this.shadowRoot.getElementById('preview-discard')?.addEventListener('click', () => { + this._generatedCampaign = null; + this._previewMode = false; + this.render(); + }); + + this.shadowRoot.getElementById('preview-regenerate')?.addEventListener('click', () => { + this._previewMode = false; + this._generatedCampaign = null; + this.render(); + // Re-open generate modal after render + requestAnimationFrame(() => { + const modal = this.shadowRoot?.getElementById('generate-modal') as HTMLElement; + if (modal) modal.hidden = false; + }); + }); + + // ── Thread badge toggles ── + this.shadowRoot.querySelectorAll('.thread-badge').forEach(badge => { + badge.addEventListener('click', () => { + const postId = (badge as HTMLElement).dataset.postId; + if (!postId) return; + const expansion = this.shadowRoot!.getElementById(`thread-${postId}`); + if (expansion) expansion.hidden = !expansion.hidden; + }); + }); + } + + private async handleGenerate() { + if (!this.shadowRoot || this._generating) return; + + const briefEl = this.shadowRoot.getElementById('gen-brief') as HTMLTextAreaElement; + const errorEl = this.shadowRoot.getElementById('gen-error') as HTMLElement; + const submitBtn = this.shadowRoot.getElementById('gen-submit') as HTMLButtonElement; + + const brief = briefEl?.value?.trim(); + if (!brief || brief.length < 10) { + if (errorEl) errorEl.innerHTML = 'Please enter at least 10 characters describing your event.'; + return; + } + + // Gather selected platforms + const platformChecks = this.shadowRoot.querySelectorAll('#gen-platforms input[type="checkbox"]'); + const platforms: string[] = []; + platformChecks.forEach((cb: Element) => { + if ((cb as HTMLInputElement).checked) platforms.push((cb as HTMLInputElement).value); + }); + if (platforms.length === 0) { + if (errorEl) errorEl.innerHTML = 'Select at least one platform.'; + return; + } + + const tone = (this.shadowRoot.getElementById('gen-tone') as HTMLSelectElement)?.value || 'professional'; + const style = (this.shadowRoot.getElementById('gen-style') as HTMLSelectElement)?.value || 'event-promo'; + + this._generating = true; + this._lastBrief = brief; + if (errorEl) errorEl.innerHTML = ''; + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = ' Generating...'; + } + + try { + const res = await fetch(`/${this._space}/rsocials/api/campaign/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ brief, platforms, tone, style }), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'Unknown error' })); + throw new Error(err.error || `HTTP ${res.status}`); + } + + const campaign: Campaign = await res.json(); + this._generatedCampaign = campaign; + this._previewMode = true; + + // Close modal and render preview + const genModal = this.shadowRoot.getElementById('generate-modal') as HTMLElement; + if (genModal) genModal.hidden = true; + + this.render(); + } catch (e: any) { + if (errorEl) errorEl.innerHTML = `${this.esc(e.message || 'Generation failed')}`; + } finally { + this._generating = false; + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = 'Generate Campaign'; + } + } } } diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index 8a28daf..f790da9 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -515,6 +515,127 @@ routes.put("/api/newsletter/campaigns/:id/status", async (c) => { return c.json(data, res.status as any); }); +// ── AI Campaign Generator ── + +routes.post("/api/campaign/generate", async (c) => { + const GEMINI_API_KEY = process.env.GEMINI_API_KEY || ""; + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { brief, platforms, style, tone } = await c.req.json(); + if (!brief || typeof brief !== "string" || brief.trim().length < 10) { + return c.json({ error: "brief is required (min 10 characters)" }, 400); + } + + const selectedPlatforms = (platforms && Array.isArray(platforms) && platforms.length > 0) + ? platforms : ["x", "linkedin", "instagram", "youtube", "threads", "bluesky", "newsletter"]; + const campaignStyle = style || "event-promo"; + const campaignTone = tone || "professional"; + + const { GoogleGenerativeAI } = await import("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); + const model = genAI.getGenerativeModel({ model: "gemini-2.5-pro" }); + + const systemPrompt = `You are an expert social media campaign strategist. Given an event brief, generate a complete multi-phase, multi-platform social media campaign. + +Style: ${campaignStyle} +Tone: ${campaignTone} +Target platforms: ${selectedPlatforms.join(", ")} + +Platform specifications: +- x: Max 280 chars per post. Support threads (threadPosts array). Use 2-4 hashtags, emojis encouraged. +- linkedin: Max 1300 chars. Professional tone. 3-5 hashtags. No emojis in professional mode. +- instagram: Carousel descriptions. 20-30 hashtags. Heavy emoji usage. +- youtube: Video title + description. SEO-focused with keywords. +- threads: Max 500 chars. Casual tone. Support threads (threadPosts array). +- bluesky: Max 300 chars. Conversational. Minimal hashtags (1-2). +- newsletter: HTML email body (emailHtml) with subject line (emailSubject). Include sections, CTA button. + +Event brief: +""" +${brief.trim()} +""" + +Return ONLY valid JSON (no markdown fences): +{ + "title": "Campaign title", + "description": "1-2 sentence campaign description", + "duration": "Human-readable date range (e.g. 'Mar 20-25, 2026 (6 days)')", + "platforms": [${selectedPlatforms.map((p: string) => `"${p}"`).join(", ")}], + "phases": [ + { "name": "phase-slug", "label": "Phase Label", "days": "Day -3 to -1" } + ], + "posts": [ + { + "platform": "x", + "postType": "thread", + "stepNumber": 1, + "content": "Main post content with emojis and formatting", + "scheduledAt": "2026-03-20T09:00:00", + "status": "draft", + "hashtags": ["Tag1", "Tag2"], + "phase": 1, + "phaseLabel": "Phase Label", + "threadPosts": ["First tweet of thread", "Second tweet", "Third tweet"], + "emailSubject": null, + "emailHtml": null + } + ] +} + +Rules: +- Generate 3-5 phases based on event type (pre-launch, launch, amplification, follow-up, etc.) +- Create posts for EACH selected platform in each relevant phase (not every platform needs every phase) +- For X and Threads posts marked as "thread" postType, include threadPosts array with individual posts +- For newsletter posts, include emailSubject and emailHtml with proper HTML (inline styles, CTA button) +- Use realistic future scheduledAt dates based on the brief +- stepNumber should increment across ALL posts (global ordering) +- Each post's content must respect the platform's character limits +- Include relevant emojis naturally in post content +- Hashtags should be relevant, no # prefix in the array +- Make content engaging, not generic — reference specific details from the brief`; + + try { + const result = await model.generateContent(systemPrompt); + const text = result.response.text(); + const jsonStr = text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim(); + const generated = JSON.parse(jsonStr); + + // Add IDs and timestamps + const now = Date.now(); + const campaignId = `gen-${now}`; + const campaign = { + id: campaignId, + title: generated.title || "Generated Campaign", + description: generated.description || "", + duration: generated.duration || "", + platforms: generated.platforms || selectedPlatforms, + phases: generated.phases || [], + posts: (generated.posts || []).map((p: any, i: number) => ({ + id: `${campaignId}-post-${i}`, + platform: p.platform || "x", + postType: p.postType || "text", + stepNumber: p.stepNumber || i + 1, + content: p.content || "", + scheduledAt: p.scheduledAt || new Date().toISOString(), + status: p.status || "draft", + hashtags: p.hashtags || [], + phase: p.phase || 1, + phaseLabel: p.phaseLabel || "", + ...(p.threadPosts ? { threadPosts: p.threadPosts } : {}), + ...(p.emailSubject ? { emailSubject: p.emailSubject } : {}), + ...(p.emailHtml ? { emailHtml: p.emailHtml } : {}), + })), + createdAt: now, + updatedAt: now, + }; + + return c.json(campaign); + } catch (e: any) { + console.error("[rSocials] Campaign generation error:", e.message); + return c.json({ error: "Failed to generate campaign: " + (e.message || "unknown error") }, 502); + } +}); + // ── Campaign Workflow CRUD API ── routes.get("/api/campaign-workflows", (c) => { diff --git a/modules/rsocials/schemas.ts b/modules/rsocials/schemas.ts index 646f89b..7e0bc51 100644 --- a/modules/rsocials/schemas.ts +++ b/modules/rsocials/schemas.ts @@ -36,6 +36,9 @@ export interface CampaignPost { hashtags: string[]; phase: number; phaseLabel: string; + threadPosts?: string[]; + emailSubject?: string; + emailHtml?: string; } export interface Campaign { @@ -378,12 +381,12 @@ export interface SocialsDoc { export const socialsSchema: DocSchema = { module: 'socials', collection: 'data', - version: 3, + version: 4, init: (): SocialsDoc => ({ meta: { module: 'socials', collection: 'data', - version: 3, + version: 4, spaceSlug: '', createdAt: Date.now(), }, @@ -397,7 +400,7 @@ export const socialsSchema: DocSchema = { if (!doc.campaignFlows) (doc as any).campaignFlows = {}; if (!doc.activeFlowId) (doc as any).activeFlowId = ''; if (!doc.campaignWorkflows) (doc as any).campaignWorkflows = {}; - if (doc.meta) doc.meta.version = 3; + if (doc.meta) doc.meta.version = 4; return doc; }, };