feat(rsocials): threadable drafts — vertical tweet chain + [+] in overlay
Post detail overlay now renders content as a tweet chain:
- Each tweet is a spine-connected row (dot + connecting line) with
tweet number, per-platform char counter, and inline textarea in
edit mode
- [+ Add tweet] button at the bottom appends a new empty tweet to
the chain; × per tweet removes it (hidden when only one tweet)
- Save writes threadPosts[] when len > 1, else plain content (and
clears threadPosts), so single drafts stay flat
- View mode shows tweets stacked vertically reading top-to-bottom,
edit mode keeps the same layout with editable boxes
- Per-platform limits (x/twitter 280, bluesky 300, threads 500, li
3000, ig 2200, yt 5000) drive live char counters; over-limit
goes red bold
- Grid cards gain a 🧵 N badge when threadPosts.length > 1
- Fix FLIP animation replaying on every re-render (now guarded with
an instance flag, plays once per open)
Bump folk-thread-gallery cache to v=5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3f3e67cdb
commit
29332a5023
|
|
@ -20,18 +20,25 @@ interface DraftPostCard {
|
|||
status: string;
|
||||
hashtags: string[];
|
||||
threadId?: string;
|
||||
threadPosts?: string[];
|
||||
}
|
||||
|
||||
type StatusFilter = 'draft' | 'scheduled' | 'published';
|
||||
const STATUS_STORAGE_KEY = 'rsocials:gallery:status-filter';
|
||||
|
||||
interface EditDraft {
|
||||
content: string;
|
||||
tweets: string[]; // thread-style: 1 element for single post, N for chains
|
||||
scheduledAt: string;
|
||||
hashtags: string;
|
||||
status: 'draft' | 'scheduled' | 'published';
|
||||
}
|
||||
|
||||
// Per-platform character limits (null = no practical limit shown in UI).
|
||||
const PLATFORM_LIMITS: Record<string, number | null> = {
|
||||
x: 280, twitter: 280, bluesky: 300, threads: 500,
|
||||
linkedin: 3000, instagram: 2200, youtube: 5000, newsletter: null,
|
||||
};
|
||||
|
||||
export class FolkThreadGallery extends HTMLElement {
|
||||
private _space = 'demo';
|
||||
private _threads: ThreadData[] = [];
|
||||
|
|
@ -152,6 +159,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
status: post.status,
|
||||
hashtags: post.hashtags || [],
|
||||
threadId: post.threadId,
|
||||
threadPosts: post.threadPosts ? [...post.threadPosts] : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -230,10 +238,13 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
const statusClass = `badge badge--${p.status}`;
|
||||
const statusLabel = p.status.charAt(0).toUpperCase() + p.status.slice(1);
|
||||
const cardClass = `card card--${p.status}`;
|
||||
const tweetCount = p.threadPosts?.length ?? 0;
|
||||
const threadBadge = tweetCount > 1 ? `<span class="badge badge--thread">🧵 ${tweetCount}</span>` : '';
|
||||
return `<button type="button" class="${cardClass}" data-post-id="${this.esc(p.id)}">
|
||||
<div class="card__badges">
|
||||
<span class="${statusClass}">${statusLabel}</span>
|
||||
<span class="badge badge--campaign">${this.esc(p.campaignTitle)}</span>
|
||||
${threadBadge}
|
||||
</div>
|
||||
<h3 class="card__title">${this.platformIcon(p.platform)} ${this.esc(p.platform)} Post</h3>
|
||||
<p class="card__preview">${preview}</p>
|
||||
|
|
@ -308,6 +319,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
.badge--scheduled { background: rgba(59,130,246,0.15); color: #60a5fa; }
|
||||
.badge--published { background: rgba(34,197,94,0.15); color: #22c55e; }
|
||||
.badge--campaign { background: rgba(99,102,241,0.15); color: #a5b4fc; }
|
||||
.badge--thread { background: rgba(20,184,166,0.15); color: #5eead4; }
|
||||
.filter-bar {
|
||||
display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem;
|
||||
justify-content: center;
|
||||
|
|
@ -467,6 +479,96 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
font-size: 0.9rem;
|
||||
}
|
||||
.edit-saving { font-size: 0.75rem; color: #94a3b8; margin-right: auto; align-self: center; }
|
||||
.edit-field-row { display: grid; grid-template-columns: 1fr 1.4fr; gap: 0.75rem; }
|
||||
@media (max-width: 480px) { .edit-field-row { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ── Tweet chain (downward) ── */
|
||||
.tweet-chain { display: flex; flex-direction: column; gap: 0; }
|
||||
.tweet-chain--view { margin-bottom: 0.5rem; }
|
||||
.tweet-row {
|
||||
display: grid; grid-template-columns: 32px 1fr; gap: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
.tweet-spine {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.tweet-dot {
|
||||
width: 32px; height: 32px; border-radius: 50%;
|
||||
background: rgba(20, 184, 166, 0.15);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 0.95rem; line-height: 1; flex-shrink: 0;
|
||||
}
|
||||
.tweet-line {
|
||||
flex: 1; width: 2px; background: var(--rs-input-border, #334155);
|
||||
margin-top: 4px; border-radius: 1px;
|
||||
}
|
||||
.tweet-body {
|
||||
display: flex; flex-direction: column; gap: 0.35rem;
|
||||
padding-bottom: 0.25rem; min-width: 0;
|
||||
}
|
||||
.tweet-head {
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
.tweet-num {
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
}
|
||||
.tweet-count {
|
||||
color: #94a3b8; font-variant-numeric: tabular-nums;
|
||||
margin-left: auto;
|
||||
}
|
||||
.tweet-count--over { color: #ef4444; font-weight: 700; }
|
||||
.tweet-content {
|
||||
font-size: 0.95rem; line-height: 1.55;
|
||||
color: var(--rs-text-primary, #f1f5f9);
|
||||
white-space: pre-wrap; word-wrap: break-word;
|
||||
background: rgba(255,255,255,0.025);
|
||||
padding: 0.75rem 0.9rem; border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
.tweet-textarea {
|
||||
background: var(--rs-bg-surface-raised, #0f172a);
|
||||
border: 1px solid var(--rs-input-border, #334155);
|
||||
border-radius: 10px;
|
||||
color: var(--rs-text-primary, #f1f5f9);
|
||||
padding: 0.7rem 0.85rem; font-size: 0.93rem; font-family: inherit;
|
||||
line-height: 1.55; resize: vertical; min-height: 72px;
|
||||
}
|
||||
.tweet-textarea:focus {
|
||||
outline: none; border-color: #14b8a6;
|
||||
box-shadow: 0 0 0 3px rgba(20,184,166,0.15);
|
||||
}
|
||||
.tweet-remove {
|
||||
background: transparent; border: none;
|
||||
color: #94a3b8; cursor: pointer;
|
||||
width: 22px; height: 22px; border-radius: 4px;
|
||||
line-height: 1; font-size: 1rem;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
.tweet-remove:hover { background: rgba(239,68,68,0.12); color: #fca5a5; }
|
||||
.tweet-add {
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
background: transparent;
|
||||
border: 1.5px dashed var(--rs-input-border, #334155);
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
border-radius: 10px;
|
||||
padding: 0.6rem 0.9rem; cursor: pointer;
|
||||
font-family: inherit; font-size: 0.85rem; font-weight: 600;
|
||||
margin-left: 44px; /* align with tweet body */
|
||||
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||
}
|
||||
.tweet-add:hover {
|
||||
background: rgba(20,184,166,0.08);
|
||||
border-color: #14b8a6; color: #5eead4;
|
||||
}
|
||||
.tweet-add__plus {
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: rgba(20, 184, 166, 0.2); color: #5eead4;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 1rem; line-height: 1; font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
<div class="gallery">
|
||||
<div class="header">
|
||||
|
|
@ -530,27 +632,36 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
|
||||
let body = '';
|
||||
let footer = '';
|
||||
const limit = PLATFORM_LIMITS[post.platform.toLowerCase()] ?? null;
|
||||
|
||||
if (this._editMode && this._editDraft) {
|
||||
const d = this._editDraft;
|
||||
// datetime-local needs YYYY-MM-DDTHH:mm in local time
|
||||
const dtLocal = d.scheduledAt ? toDatetimeLocal(d.scheduledAt) : '';
|
||||
const tweetChainHtml = d.tweets.map((t, i) => this.renderEditTweet(t, i, d.tweets.length, limit, post.platform)).join('');
|
||||
body = `<div class="overlay-body">
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-content">Content</label>
|
||||
<textarea id="f-content" class="edit-textarea" data-field="content">${this.esc(d.content)}</textarea>
|
||||
<label class="edit-label">Tweet chain</label>
|
||||
<div class="tweet-chain">
|
||||
${tweetChainHtml}
|
||||
<button type="button" class="tweet-add" data-action="add-tweet" aria-label="Add tweet">
|
||||
<span class="tweet-add__plus">+</span>
|
||||
<span class="tweet-add__label">Add tweet to chain</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-status">Status</label>
|
||||
<select id="f-status" class="edit-select" data-field="status">
|
||||
<option value="draft"${d.status === 'draft' ? ' selected' : ''}>Draft</option>
|
||||
<option value="scheduled"${d.status === 'scheduled' ? ' selected' : ''}>Scheduled</option>
|
||||
<option value="published"${d.status === 'published' ? ' selected' : ''}>Published</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-sched">Scheduled at</label>
|
||||
<input id="f-sched" type="datetime-local" class="edit-input" data-field="scheduledAt" value="${this.esc(dtLocal)}" />
|
||||
<div class="edit-field-row">
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-status">Status</label>
|
||||
<select id="f-status" class="edit-select" data-field="status">
|
||||
<option value="draft"${d.status === 'draft' ? ' selected' : ''}>Draft</option>
|
||||
<option value="scheduled"${d.status === 'scheduled' ? ' selected' : ''}>Scheduled</option>
|
||||
<option value="published"${d.status === 'published' ? ' selected' : ''}>Published</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-sched">Scheduled at</label>
|
||||
<input id="f-sched" type="datetime-local" class="edit-input" data-field="scheduledAt" value="${this.esc(dtLocal)}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-tags">Hashtags (space separated)</label>
|
||||
|
|
@ -563,8 +674,16 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
<button type="button" class="overlay-btn overlay-btn--primary" data-action="save">Save</button>
|
||||
</div>`;
|
||||
} else {
|
||||
const viewTweets = post.threadPosts && post.threadPosts.length > 0
|
||||
? post.threadPosts
|
||||
: [post.content || ''];
|
||||
const tweetChainView = viewTweets.map((t, i) => this.renderViewTweet(t, i, viewTweets.length, limit, post.platform)).join('');
|
||||
const isThread = viewTweets.length > 1;
|
||||
|
||||
body = `<div class="overlay-body">
|
||||
<div class="overlay-content">${this.esc(post.content) || '<em style="color:#64748b">Empty post</em>'}</div>
|
||||
<div class="tweet-chain tweet-chain--view">
|
||||
${tweetChainView}
|
||||
</div>
|
||||
<div class="overlay-meta-grid">
|
||||
<div class="overlay-meta-label">Schedule</div>
|
||||
<div class="overlay-meta-value">${this.esc(schedDate)}</div>
|
||||
|
|
@ -572,8 +691,8 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
<div class="overlay-hashtags">${post.hashtags.length ? post.hashtags.map(h => `<span>${this.esc(h)}</span>`).join('') : '<span style="background:transparent;color:#64748b">—</span>'}</div>
|
||||
<div class="overlay-meta-label">Campaign</div>
|
||||
<div class="overlay-meta-value">${this.esc(post.campaignTitle)}</div>
|
||||
<div class="overlay-meta-label">Characters</div>
|
||||
<div class="overlay-meta-value">${post.content.length}</div>
|
||||
<div class="overlay-meta-label">${isThread ? 'Tweets' : 'Characters'}</div>
|
||||
<div class="overlay-meta-value">${isThread ? `${viewTweets.length} tweets · ${viewTweets.reduce((s, t) => s + t.length, 0)} chars total` : String(viewTweets[0].length)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
const openHref = post.threadId
|
||||
|
|
@ -594,6 +713,44 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
private renderViewTweet(content: string, idx: number, total: number, limit: number | null, platform: string): string {
|
||||
const charsLeft = limit !== null ? limit - content.length : null;
|
||||
const overLimit = charsLeft !== null && charsLeft < 0;
|
||||
const empty = content.trim().length === 0;
|
||||
return `<article class="tweet-row tweet-row--view">
|
||||
<div class="tweet-spine">
|
||||
<div class="tweet-dot">${this.platformIcon(platform)}</div>
|
||||
${idx < total - 1 ? '<div class="tweet-line"></div>' : ''}
|
||||
</div>
|
||||
<div class="tweet-body">
|
||||
<div class="tweet-head">
|
||||
<span class="tweet-num">${total > 1 ? `${idx + 1}/${total}` : 'Tweet'}</span>
|
||||
${limit !== null ? `<span class="tweet-count${overLimit ? ' tweet-count--over' : ''}">${charsLeft}</span>` : ''}
|
||||
</div>
|
||||
<div class="tweet-content">${empty ? '<em style="color:#64748b">Empty</em>' : this.esc(content)}</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
private renderEditTweet(content: string, idx: number, total: number, limit: number | null, platform: string): string {
|
||||
const charsLeft = limit !== null ? limit - content.length : null;
|
||||
const overLimit = charsLeft !== null && charsLeft < 0;
|
||||
return `<article class="tweet-row tweet-row--edit">
|
||||
<div class="tweet-spine">
|
||||
<div class="tweet-dot">${this.platformIcon(platform)}</div>
|
||||
${idx < total - 1 ? '<div class="tweet-line"></div>' : ''}
|
||||
</div>
|
||||
<div class="tweet-body">
|
||||
<div class="tweet-head">
|
||||
<span class="tweet-num">${total > 1 ? `${idx + 1}/${total}` : 'Tweet'}</span>
|
||||
${limit !== null ? `<span class="tweet-count${overLimit ? ' tweet-count--over' : ''}">${charsLeft}</span>` : ''}
|
||||
${total > 1 ? `<button type="button" class="tweet-remove" data-action="remove-tweet" data-idx="${idx}" aria-label="Remove tweet ${idx + 1}" title="Remove tweet">×</button>` : ''}
|
||||
</div>
|
||||
<textarea class="tweet-textarea" data-tweet-idx="${idx}" rows="3">${this.esc(content)}</textarea>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
private wireOverlay(): void {
|
||||
if (!this.shadowRoot) return;
|
||||
const backdrop = this.shadowRoot.querySelector('.overlay-backdrop') as HTMLElement | null;
|
||||
|
|
@ -613,10 +770,16 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
else if (action === 'edit') this.enterEditMode();
|
||||
else if (action === 'cancel-edit') this.exitEditMode();
|
||||
else if (action === 'save') this.saveEdit();
|
||||
else if (action === 'add-tweet') this.addTweet();
|
||||
else if (action === 'remove-tweet') {
|
||||
const idx = parseInt(actionEl.dataset.idx || '', 10);
|
||||
if (!isNaN(idx)) this.removeTweet(idx);
|
||||
}
|
||||
});
|
||||
|
||||
// Bind input listeners in edit mode so the draft stays fresh
|
||||
if (this._editMode) {
|
||||
// Meta fields (status/sched/hashtags)
|
||||
backdrop.querySelectorAll<HTMLElement>('[data-field]').forEach(el => {
|
||||
el.addEventListener('input', (e) => {
|
||||
const input = e.currentTarget as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
|
|
@ -625,14 +788,23 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
(this._editDraft as any)[field] = input.value;
|
||||
});
|
||||
});
|
||||
// Tweet textareas — update draft + live character count without full re-render
|
||||
backdrop.querySelectorAll<HTMLTextAreaElement>('textarea[data-tweet-idx]').forEach(ta => {
|
||||
ta.addEventListener('input', () => {
|
||||
const idx = parseInt(ta.dataset.tweetIdx || '', 10);
|
||||
if (isNaN(idx) || !this._editDraft) return;
|
||||
this._editDraft.tweets[idx] = ta.value;
|
||||
this.updateTweetCounter(ta, ta.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// FLIP animation entry (only on first mount of this overlay)
|
||||
const card = backdrop.querySelector('#overlay-card') as HTMLElement | null;
|
||||
if (card && !card.dataset.animated) {
|
||||
card.dataset.animated = '1';
|
||||
// FLIP animation entry — only on first render per open
|
||||
if (!this._flipPlayed) {
|
||||
this._flipPlayed = true;
|
||||
const card = backdrop.querySelector('#overlay-card') as HTMLElement | null;
|
||||
const origin = this._flipOrigin;
|
||||
if (origin) {
|
||||
if (card && origin) {
|
||||
const target = card.getBoundingClientRect();
|
||||
const dx = origin.left + origin.width / 2 - (target.left + target.width / 2);
|
||||
const dy = origin.top + origin.height / 2 - (target.top + target.height / 2);
|
||||
|
|
@ -649,9 +821,11 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
// ── Overlay state transitions ──
|
||||
|
||||
private _flipOrigin: DOMRect | null = null;
|
||||
private _flipPlayed = false;
|
||||
|
||||
private expandPost(id: string, sourceEl?: HTMLElement): void {
|
||||
this._flipOrigin = sourceEl?.getBoundingClientRect() ?? null;
|
||||
this._flipPlayed = false;
|
||||
this._expandedPostId = id;
|
||||
this._editMode = false;
|
||||
this._editDraft = null;
|
||||
|
|
@ -664,6 +838,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
this._editMode = false;
|
||||
this._editDraft = null;
|
||||
this._flipOrigin = null;
|
||||
this._flipPlayed = false;
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
|
@ -672,17 +847,21 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
if (this._isDemoFallback) return; // no runtime = read-only
|
||||
const post = this._allPosts.find(p => p.id === this._expandedPostId);
|
||||
if (!post) return;
|
||||
// Seed tweets from post.threadPosts if present, else single-tweet array from content.
|
||||
const tweets = post.threadPosts && post.threadPosts.length > 0
|
||||
? [...post.threadPosts]
|
||||
: [post.content || ''];
|
||||
this._editDraft = {
|
||||
content: post.content,
|
||||
tweets,
|
||||
scheduledAt: post.scheduledAt,
|
||||
hashtags: (post.hashtags || []).join(' '),
|
||||
status: post.status as 'draft' | 'scheduled' | 'published',
|
||||
};
|
||||
this._editMode = true;
|
||||
this.render();
|
||||
// Focus content textarea
|
||||
// Focus first tweet
|
||||
requestAnimationFrame(() => {
|
||||
const ta = this.shadowRoot?.getElementById('f-content') as HTMLTextAreaElement | null;
|
||||
const ta = this.shadowRoot?.querySelector('[data-tweet-idx="0"]') as HTMLTextAreaElement | null;
|
||||
ta?.focus();
|
||||
});
|
||||
}
|
||||
|
|
@ -694,6 +873,36 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
this.render();
|
||||
}
|
||||
|
||||
private addTweet(): void {
|
||||
if (!this._editDraft) return;
|
||||
this._editDraft.tweets.push('');
|
||||
this.render();
|
||||
// Focus the new tweet
|
||||
requestAnimationFrame(() => {
|
||||
const tweets = this.shadowRoot?.querySelectorAll<HTMLTextAreaElement>('textarea[data-tweet-idx]');
|
||||
tweets?.[tweets.length - 1]?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private removeTweet(idx: number): void {
|
||||
if (!this._editDraft || this._editDraft.tweets.length <= 1) return;
|
||||
this._editDraft.tweets.splice(idx, 1);
|
||||
this.render();
|
||||
}
|
||||
|
||||
private updateTweetCounter(ta: HTMLTextAreaElement, value: string): void {
|
||||
const post = this._allPosts.find(p => p.id === this._expandedPostId);
|
||||
if (!post) return;
|
||||
const limit = PLATFORM_LIMITS[post.platform.toLowerCase()] ?? null;
|
||||
if (limit === null) return;
|
||||
const row = ta.closest('.tweet-row');
|
||||
const counter = row?.querySelector('.tweet-count') as HTMLElement | null;
|
||||
if (!counter) return;
|
||||
const left = limit - value.length;
|
||||
counter.textContent = String(left);
|
||||
counter.classList.toggle('tweet-count--over', left < 0);
|
||||
}
|
||||
|
||||
private async saveEdit(): Promise<void> {
|
||||
if (!this._editDraft || !this._expandedPostId) return;
|
||||
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||
|
|
@ -717,12 +926,19 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
|
||||
try {
|
||||
const docId = socialsDocId(this._space) as DocumentId;
|
||||
// Normalize: strip empty trailing tweets; keep at least one.
|
||||
const tweets = draft.tweets.map(t => t.trim()).filter((t, i, arr) => t.length > 0 || i === 0);
|
||||
if (tweets.length === 0) tweets.push('');
|
||||
const isThread = tweets.length > 1;
|
||||
|
||||
await runtime.changeDoc(docId, 'edit post', (d: SocialsDoc) => {
|
||||
if (!d.campaigns) return;
|
||||
for (const campaign of Object.values(d.campaigns)) {
|
||||
const post = (campaign.posts || []).find((p: CampaignPost) => p.id === postId);
|
||||
if (post) {
|
||||
post.content = draft.content;
|
||||
// Single-tweet → use content; thread → use threadPosts (keep content in sync w/ tweet 1)
|
||||
post.content = tweets[0];
|
||||
post.threadPosts = isThread ? tweets : undefined;
|
||||
post.status = draft.status;
|
||||
post.scheduledAt = scheduledAtIso;
|
||||
post.hashtags = hashtags;
|
||||
|
|
|
|||
|
|
@ -3026,7 +3026,7 @@ routes.get("/threads", (c) => {
|
|||
theme: "dark",
|
||||
body: `<folk-thread-gallery space="${escapeHtml(space)}"></folk-thread-gallery>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=4"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=5"></script>`,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
@ -3181,7 +3181,7 @@ routes.get("/", (c) => {
|
|||
theme: "dark",
|
||||
styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
|
||||
body: `<folk-thread-gallery space="${escapeHtml(space)}"></folk-thread-gallery>`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=4"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=5"></script>`,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue