Keep the notifications worth interrupting for
Splitting system messages out of the modal was right for payout receipts,
but it would have silenced two that a member genuinely loses money by
ignoring:
- "@someone is trying to buy. Link your wallet so it pays you" — a sale
is blocked right now, and the referral is lost permanently once it
routes to someone else.
- "You missed 43 POL on InstantAdPay" — a payout passed them by, and the
message explains exactly how to stop the next one doing the same.
So the dividing line is not system-versus-human, it is "does this need you
to do something". Those two become kind 'alert' and still interrupt; the
receipts stay kind 'notice' and stay in the inbox.
The modal no longer credits an alert to a person either. It was saying "A
message from @martbost" over machine-generated text, because system mail
is sent by member 1 at ADMIN_EMAIL. Alerts now read "Action needed on your
account".
Also reclassified "X is now in your line for good" as a notice — it is
good news about a referral the member gained, with nothing at stake.
qa/messages-notice.mjs now covers both lanes: 14 checks, including that an
alert interrupts and a flood of 25 receipts does not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-12
@@ -30,12 +30,18 @@ async function lastBroadcastAt(fromEmail) {
|
||||
//
|
||||
// `kind` separates the two things that used to share this channel:
|
||||
// 'broadcast' — a person wrote it to their team. Rare, and worth interrupting for.
|
||||
// 'notice' — the system generated it (payout landed, purchase confirmed, and so on).
|
||||
// These arrive constantly, so they belong in the inbox and must never pop
|
||||
// a modal. Before this split they did, which meant a member with a backlog
|
||||
// of payout notices got a fresh popup after every single ad view.
|
||||
// 'alert' — the system generated it AND the member loses money by not acting: a sale
|
||||
// blocked because payouts are off, a payout that passed them by. Rare, and
|
||||
// the whole reason this channel interrupts at all.
|
||||
// 'notice' — the system generated it and nothing is required: a payout landed, a
|
||||
// purchase confirmed. These arrive constantly, so they belong in the inbox
|
||||
// and must never pop a modal. Before this split they did, which meant a
|
||||
// member with a backlog of payout notices got a fresh popup after every
|
||||
// single ad view.
|
||||
//
|
||||
// The line is not system-versus-human, it is "does this need you to do something".
|
||||
async function deliver(fromMember, fromEmail, recipients, subject, body, kind) {
|
||||
const k = kind === 'notice' ? 'notice' : 'broadcast';
|
||||
const k = ['notice', 'alert'].includes(kind) ? kind : 'broadcast';
|
||||
const now = Date.now();
|
||||
let n = 0;
|
||||
if (db.enabled()) {
|
||||
@@ -56,8 +62,8 @@ async function deliver(fromMember, fromEmail, recipients, subject, body, kind) {
|
||||
}
|
||||
// Both kinds are inbox mail; only 'chat' is the separate two-way thread.
|
||||
const isBroadcast = i => (i.kind || 'broadcast') !== 'chat';
|
||||
// ...but only a message a PERSON wrote may interrupt with the modal.
|
||||
const isHuman = i => (i.kind || 'broadcast') === 'broadcast';
|
||||
// ...but only a human broadcast or a money-at-stake alert may interrupt with the modal.
|
||||
const popsModal = i => ['broadcast', 'alert'].includes(i.kind || 'broadcast');
|
||||
const isChat = i => i.kind === 'chat';
|
||||
async function inbox(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
@@ -166,15 +172,15 @@ async function unreadCount(email) {
|
||||
async function newestUnread(email) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
const rows = await db.q(`SELECT id, from_member, subject, body, sent FROM sponsor_messages
|
||||
WHERE to_email=? AND kind='broadcast' AND read_ts IS NULL ORDER BY sent DESC LIMIT 1`, [e]);
|
||||
const rows = await db.q(`SELECT id, from_member, subject, body, sent, kind FROM sponsor_messages
|
||||
WHERE to_email=? AND kind IN ('broadcast','alert') AND read_ts IS NULL ORDER BY sent DESC LIMIT 1`, [e]);
|
||||
if (!rows.length) return null;
|
||||
const r = rows[0];
|
||||
return { id: r.id, fromMember: r.from_member, subject: r.subject, body: r.body, sent: Number(r.sent) };
|
||||
return { id: r.id, fromMember: r.from_member, subject: r.subject, body: r.body, sent: Number(r.sent), kind: r.kind };
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
const u = J.db.items.filter(i => i.toEmail === e && isHuman(i) && !i.readTs).sort((a, b) => b.sent - a.sent)[0];
|
||||
return u ? { id: u.id, fromMember: u.fromMember, subject: u.subject, body: u.body, sent: u.sent } : null;
|
||||
const u = J.db.items.filter(i => i.toEmail === e && popsModal(i) && !i.readTs).sort((a, b) => b.sent - a.sent)[0];
|
||||
return u ? { id: u.id, fromMember: u.fromMember, subject: u.subject, body: u.body, sent: u.sent, kind: u.kind || 'broadcast' } : null;
|
||||
}
|
||||
async function markRead(email, id) {
|
||||
const e = String(email || '').toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user