Daily digest (opt-in) + finite "you're caught up" ending
Reader-retention as ritual, not capture (Codex's framing). Opt-in calm morning email of today's brief; the on-site twin is the finite end-of-feed nudge. * Schema: users.digest_enabled + digest_unsub_token; digest_sends (dedupe + visibility). auth.get_user now returns the digest fields. * goodnews/digest.py: build (dated calm subject, items w/ summary + "why it's here" + UB/source links + one-click unsubscribe, "you're caught up" sign-off) and send_due_digests (morning-window gated, >=4-item floor or skip quietly, deduped, reuses SMTP). No streaks/urgency/"you missed". * API: /auth/me exposes digest_enabled; POST /api/account/digest toggle; GET /api/digest/unsubscribe (token, no login, calm confirmation page). * CLI: cycle gains a morning-gated digest step (--no-digest) + a send-digests command (--force). * Frontend: digest toggle on the Account profile; the Highlights end-cap now says "you're caught up — see you tomorrow" with a one-tap "Get tomorrow's brief by email" (signed-in → enable; anon → sign in). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -303,6 +303,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
// The finite-ending's gentle nudge: one tap to get tomorrow's brief by email.
|
||||
let digestBusy = $state(false);
|
||||
async function subscribeDigest() {
|
||||
if (!auth.user) { showSignIn = true; return; } // sign in, then enable below
|
||||
if (auth.user.digest_enabled || digestBusy) return;
|
||||
digestBusy = true;
|
||||
try { await postJSON('/api/account/digest', { enabled: true }); await refreshAuth(); }
|
||||
catch { /* leave as-is */ }
|
||||
finally { digestBusy = false; }
|
||||
}
|
||||
|
||||
const MIX_EVENT = { notToday: 'not_today', lessLikeThis: 'less_like_this', alwaysHide: 'hide_topic' };
|
||||
function applyAction(kind, value) {
|
||||
applyPrefAction(kind, value); // updates + persists + syncs to account
|
||||
@@ -430,7 +441,17 @@
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
<p class="endcap rise">✦ that's the good news for today ✦</p>
|
||||
<div class="endcap rise">
|
||||
<p class="endmark">✦ that's the good news for today ✦</p>
|
||||
<p class="endsub">You're caught up — see you tomorrow.</p>
|
||||
{#if auth.user?.digest_enabled}
|
||||
<p class="digestnote">Tomorrow's brief is headed to your inbox ☕</p>
|
||||
{:else}
|
||||
<button class="digestcta" onclick={subscribeDigest} disabled={digestBusy}>
|
||||
{digestBusy ? '…' : 'Get tomorrow’s brief by email'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="muted center pad">No highlights yet today — try a calmer filter, or check back soon.</p>
|
||||
{/if}
|
||||
@@ -532,6 +553,15 @@
|
||||
text-align: center; color: var(--muted); font-family: var(--serif);
|
||||
font-style: italic; margin: 40px 0 10px; letter-spacing: 0.02em;
|
||||
}
|
||||
.endcap .endmark { margin: 0; }
|
||||
.endcap .endsub { margin: 4px 0 0; font-size: 0.92rem; }
|
||||
.endcap .digestnote { margin: 14px 0 0; font-style: normal; font-family: var(--label); font-size: 0.86rem; color: var(--accent-deep); }
|
||||
.endcap .digestcta {
|
||||
margin-top: 16px; font-family: var(--label); font-style: normal; font-size: 0.9rem; cursor: pointer;
|
||||
background: var(--accent); color: #fff; border: none; border-radius: 999px; padding: 10px 22px;
|
||||
}
|
||||
.endcap .digestcta:hover { background: var(--accent-deep); }
|
||||
.endcap .digestcta:disabled { opacity: 0.6; cursor: default; }
|
||||
.loadmore { display: flex; justify-content: center; margin: 30px 0 6px; }
|
||||
.loadmore button {
|
||||
background: var(--surface); border: 1px solid var(--line); color: var(--accent-deep);
|
||||
|
||||
Reference in New Issue
Block a user