diff --git a/db.js b/db.js
index ae95395..c440a24 100644
--- a/db.js
+++ b/db.js
@@ -240,10 +240,22 @@ async function bootstrap() {
// Idempotent — after the first pass nothing matches.
for (const like of ['You just got paid %POL on InstantAdPay',
'You missed %POL on InstantAdPay',
- '%is trying to buy. Link your wallet so it pays you']) {
+ '%is trying to buy. %',
+ 'You lost %payouts were not switched on%',
+ '% is now in your line for good%']) {
try { await q("UPDATE sponsor_messages SET kind='notice' WHERE kind='broadcast' AND subject LIKE ?", [like]); }
catch (e) {}
}
+ // ...but two of those DO need the member to act, and they lose money by not acting: a sale
+ // blocked or already lost because payouts are off, and a payout that passed them by. Those
+ // keep interrupting. The dividing line is "does this need you to do something", not
+ // "who sent it".
+ for (const like of ['You missed %POL on InstantAdPay',
+ '%is trying to buy. %',
+ 'You lost %payouts were not switched on%']) {
+ try { await q("UPDATE sponsor_messages SET kind='alert' WHERE kind='notice' AND subject LIKE ?", [like]); }
+ catch (e) {}
+ }
// per-account chat settings: availability toggle (default on) + muted-member list (JSON emails)
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_available TINYINT NOT NULL DEFAULT 1');
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL');
diff --git a/messages.js b/messages.js
index d01298e..9d88c31 100644
--- a/messages.js
+++ b/messages.js
@@ -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();
diff --git a/public/assets/my.js b/public/assets/my.js
index baf7b2f..7bd0373 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -1302,7 +1302,11 @@
if (!$('msgModal') || !msg || msgModalShown.has(msg.id)) return;
if (!$('msgModal').hidden) return; // one already open
msgModalShown.add(msg.id);
- $('mmFrom').textContent = 'A message from ' + (msg.fromName || 'your sponsor');
+ // An alert is generated by the system, so crediting it to a person ("A message from
+ // @martbost") is both wrong and misleading about what the member is looking at.
+ $('mmFrom').textContent = msg.kind === 'alert'
+ ? 'Action needed on your account'
+ : 'A message from ' + (msg.fromName || 'your sponsor');
$('mmSubject').textContent = msg.subject || '';
$('mmBody').innerHTML = msg.body || ''; // server-sanitized
$('msgModal').hidden = false;
diff --git a/public/my.html b/public/my.html
index b8437ed..20e9554 100644
--- a/public/my.html
+++ b/public/my.html
@@ -1033,7 +1033,7 @@
-
+