aboutsummaryrefslogtreecommitdiffstats
path: root/nwcpush
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-01 12:35:15 -0700
committerAdam Soltys <[email protected]>2026-10-01 12:35:15 -0700
commit1da40523fc00b9963012abe067a6c4910c7578b6 (patch)
tree396b8fc8a66989fc56e61ad1370011b40cf93027 /nwcpush
parent509d632595399bc355fcf3826875214dc9d3b68c (diff)
downloadcoinosv3-1da40523fc00b9963012abe067a6c4910c7578b6.tar.gz
coinosv3-1da40523fc00b9963012abe067a6c4910c7578b6.zip
Zaps and reactions get their own notification switches.
The notifier now watches kind-7 reactions p-tagged at the user (opt-in by an explicit flag, so older apps that can't word them stay quiet) and sends zap receipts as 'zap' to apps that report the category, 'payment' to older ones. Coinos zap receipts carry their amount into the push. The service worker names the reactor when it knows them and stays quiet for a muted one. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VP5Ljqk7EYcm5QQAbzDwog
Diffstat (limited to 'nwcpush')
-rw-r--r--nwcpush/server.js30
1 files changed, 24 insertions, 6 deletions
diff --git a/nwcpush/server.js b/nwcpush/server.js
index d4eb6a5..4110f91 100644
--- a/nwcpush/server.js
+++ b/nwcpush/server.js
@@ -138,8 +138,8 @@ function resubscribeNotify() {
if (!ptags.length && !authors.length) return;
const filters = [];
// DMs / direct invites / zap receipts land p-tagged at the user
- // ...as do replies and mentions (kind 1 naming the user)
- if (ptags.length) filters.push({ kinds: [1059, 9735, 9737, 1], '#p': ptags, since: Math.floor(Date.now() / 1000) });
+ // ...as do replies and mentions (kind 1 naming the user) and reactions (7)
+ if (ptags.length) filters.push({ kinds: [1059, 9735, 9737, 1, 7], '#p': ptags, since: Math.floor(Date.now() / 1000) });
// community chat: wraps authored by a channel's derived stream key
if (authors.length) filters.push({ kinds: [1059], authors, since: Math.floor(Date.now() / 1000) });
// one sub per filter — the pool API takes a single filter object
@@ -152,7 +152,7 @@ function resubscribeNotify() {
// DMs are throttled barely at all now: the device drops the ones it decides
// are noise (a stranger, or our own sent-copy), and a server-side cooldown
// would let a discarded push swallow the friend's message that followed it.
-const COOLDOWN = { payment: 10_000, dm: 1_000, chat: 90_000, mention: 5_000 };
+const COOLDOWN = { payment: 10_000, zap: 10_000, dm: 1_000, chat: 90_000, mention: 5_000, reaction: 30_000 };
const lastPush = new Map(); // endpointId:reason -> ts
const seenNotifyEvents = new Map(); // event id -> ts
function onNotifyEvent(ev) {
@@ -177,7 +177,20 @@ function onNotifyEvent(ev) {
}
return;
}
- const reason = ev.kind === 9735 || ev.kind === 9737 ? 'payment'
+ // A like or emoji on the user's post. Opt-in by an explicit flag: an app
+ // too old to send one has no words for it and would call it chat.
+ if (ev.kind === 7) {
+ const ps = (ev.tags || []).filter((t) => t[0] === 'p' && t[1]).map((t) => t[1]);
+ const extra = { from: ev.pubkey, emoji: String(ev.content || '+').slice(0, 16) };
+ for (const [id, r] of Object.entries(notifyRegs)) {
+ const mine = r.ptags || [];
+ if (r.reasons?.reaction !== true || mine.includes(ev.pubkey)) continue;
+ if (ps.some((x) => mine.includes(x))) pushNotify(id, r, 'reaction', extra).catch(() => {});
+ }
+ return;
+ }
+ const zap = ev.kind === 9735 || ev.kind === 9737;
+ const reason = zap ? 'payment'
: p && notifyPtags().includes(p) ? 'dm' : 'chat';
// A wrapped DM hides its sender from us by design, so we can't decide here
// whether it's from a friend or a stranger — we hand the whole wrap to the
@@ -185,11 +198,16 @@ function onNotifyEvent(ev) {
// around 4KB after encryption; anything larger goes as a bare nudge and the
// device shows its generic notification.
const extra = reason === 'dm' && JSON.stringify(ev).length < 3500 ? { wrap: ev } : {};
+ // a coinos zap receipt says how much in the clear
+ const amt = ev.kind === 9737 ? parseInt(ev.tags?.find((t) => t[0] === 'amount')?.[1], 10) : 0;
+ if (amt > 0) extra.amountSat = amt;
for (const [id, r] of Object.entries(notifyRegs)) {
const hit = reason === 'chat'
? (r.authors || []).includes(ev.pubkey)
: p && (r.ptags || []).includes(p);
- if (hit) pushNotify(id, r, reason, extra).catch(() => {});
+ // a zap is its own category for apps that know one; older apps keep
+ // getting it as a payment
+ if (hit) pushNotify(id, r, zap && typeof r.reasons?.zap === 'boolean' ? 'zap' : reason, extra).catch(() => {});
}
}
@@ -438,7 +456,7 @@ const server = Bun.serve({
const id = Bun.hash(sub_.endpoint).toString(36);
// per-category opt-outs; absent means everything on (older clients)
const reasons = {};
- for (const k of ['payment', 'dm', 'chat', 'mention']) {
+ for (const k of ['payment', 'zap', 'dm', 'chat', 'mention', 'reaction']) {
if (typeof body.notify.reasons?.[k] === 'boolean') reasons[k] = body.notify.reasons[k];
}
notifyRegs[id] = {