aboutsummaryrefslogtreecommitdiffstats
path: root/src/sw-nwc.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/sw-nwc.js')
-rw-r--r--src/sw-nwc.js51
1 files changed, 26 insertions, 25 deletions
diff --git a/src/sw-nwc.js b/src/sw-nwc.js
index 78bb2bd..a4e4e56 100644
--- a/src/sw-nwc.js
+++ b/src/sw-nwc.js
@@ -6,16 +6,17 @@
import { respondFromBg, tendRenewals, tendLnPays } from './nwc-respond.js';
import { withRequestLock, withWorkLock } from './nwc-lock.js';
-import { appendBgLog } from './nwc-bg.js';
+import { appendBgLog, takeLease } from './nwc-bg.js';
import { bgAutoWithdraw } from './bg-autowithdraw.js';
import { allInboxes, classifyDm, shouldNotifyDm, classifyCall } from './dm-inbox.js';
+import { notifyText } from './notify-text.js';
const NOTIFIER = 'https://nwcpush.coinos.io';
// The worker's diary (nwc-bg.js): every line also lands in IndexedDB as it is
// written, so a worker the phone stops mid-push leaves its last step behind.
const stamp = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
const diary = (m) => { console.log('[sw-nwc]', m); appendBgLog(`${stamp()} ${m}`); };
-diary('worker started');
+diary(`worker started${typeof navigator !== 'undefined' && navigator.locks ? '' : ' (no Web Locks)'}`);
const CALL_CACHE = 'coinos-call'; // a call offer a push brought, for the app the tap opens
// where a call's offer wrap is fetched from: the notifier, or (the app served
// on this machine, as the tests do) this origin
@@ -108,8 +109,11 @@ self.addEventListener('push', (e) => {
await cache.put('/__pending-call', new Response(JSON.stringify({ offer: ring.offer, at: Date.now() }), { headers: { 'content-type': 'application/json' } }));
} catch (_) {}
for (const c of clients) { try { c.postMessage({ type: 'call-offer', offer: ring.offer }); } catch (_) {} }
- await show((ring.name || 'Someone') + ' is calling', {
- body: ring.type === 'video' ? 'Video call. Tap to answer.' : 'Voice call. Tap to answer.',
+ // the words are measured against Chrome's spam model (notify-text.js):
+ // "<name> is calling" is hidden behind a warning, "Calling" / "<name>" shows
+ const [callTitle, callBody] = notifyText('call', { name: ring.name });
+ await show(callTitle, {
+ body: callBody,
icon: 'icon-192.png', badge: 'badge-96.png',
tag: 'call', renotify: true, requireInteraction: true, vibrate: [400, 200, 400, 200, 400],
data: { url: './?open=call', view: 'call' },
@@ -141,24 +145,15 @@ self.addEventListener('push', (e) => {
if (verdict && verdict.name) dmName = verdict.name;
} catch (err) { console.warn('[sw-dm] could not classify:', err && err.message); }
}
- // A reaction names who, when this device knows them, and is dropped
- // when they are muted here — the notifier can't know either.
- let who = null;
+ // A reaction from someone muted on this device is dropped — the
+ // notifier can't know. (It no longer names who: a name in the line
+ // is what tips Chrome's spam model over, see notify-text.js.)
if (data.reason === 'reaction' && data.from) {
const boxes = await allInboxes().catch(() => []);
if (boxes.some((b) => (b.muted || []).includes(data.from))) return;
- who = boxes.map((b) => b.names && b.names[data.from]).find(Boolean) || null;
}
const emoji = !data.emoji || data.emoji === '+' ? '\u2764\ufe0f' : /^:[\w-]+:$/.test(data.emoji) ? data.emoji : data.emoji.slice(0, 8);
- const T = {
- payment: ['Payment received', data.amountSat ? `+${Number(data.amountSat).toLocaleString()} sats. Open coinos to see it.` : 'Open coinos to see it.'],
- dm: dmName ? [dmName, 'sent you a message.'] : ['New message', 'You have a new private message.'],
- chat: ['New chat activity', 'There are new messages in your communities.'],
- mention: [data.reply ? 'New reply to your post' : 'You were mentioned', data.text || 'Open coinos to see it.'],
- zap: ['Zap received', data.amountSat ? `+${Number(data.amountSat).toLocaleString()} sats. Open coinos to see it.` : 'Someone zapped you. Open coinos to see it.'],
- reaction: [who ? `${who} reacted ${emoji}` : `New reaction ${emoji}`, 'to your post. Open coinos to see it.'],
- };
- const [title, body] = T[data.reason] || T.chat;
+ const [title, body] = notifyText(data.reason, { amountSat: data.amountSat, name: dmName, reply: data.reply, emoji });
// a reply lands you in the Notifications list, where it is
const target = data.reason === 'mention' || data.reason === 'reaction' ? { url: './?open=notifs', view: 'notifs' } : { url: './' };
await show(title, {
@@ -179,6 +174,11 @@ self.addEventListener('push', (e) => {
// four payments, no line for the fourth, then no more wakes). On an
// Apple endpoint the stand-in goes up FIRST; the outcome replaces it.
const apple = await applePush();
+ // What a push may take. iOS stops the worker about ten seconds in (PullCap's
+ // diary of 2026-10-24: every wake's last line 8–12 s after its push, in the
+ // middle of a payment), whatever the waitUntil — so there everything is
+ // planned inside nine; elsewhere the usual thirty.
+ const budgetEnd = startedAt + (apple ? 9_000 : 27_000);
const dropStandIn = async () => {
const open = await self.registration.getNotifications({ tag: 'coinos-bg' }).catch(() => []);
for (const n of open) n.close();
@@ -204,10 +204,10 @@ self.addEventListener('push', (e) => {
// has run, cover coins without one. After the answer, inside what is
// left of the push budget.
const tend = async () => {
- const late = await tendLnPays({ deadline: startedAt + 27_000, log: diary }).catch((err) => { diary('earlier sends failed: ' + (err && err.message)); return []; });
+ const late = await tendLnPays({ deadline: budgetEnd, log: diary }).catch((err) => { diary('earlier sends failed: ' + (err && err.message)); return []; });
for (const n of late) await notice(n);
if (late.length && apple) await dropStandIn();
- await tendRenewals({ deadline: startedAt + 27_000, log: (m) => diary('renew: ' + m) })
+ await tendRenewals({ deadline: budgetEnd, log: (m) => diary('renew: ' + m) })
.catch((err) => diary('renewals failed: ' + (err && err.message)));
};
// an NWC wake-up may also have moved money in; check the rule after
@@ -217,10 +217,11 @@ self.addEventListener('push', (e) => {
await withWorkLock(async () => {
const waited = Date.now() - startedAt;
if (waited > 500) diary(`waited ${waited} ms for an earlier push to finish`);
+ if (Date.now() > budgetEnd - 2_000) { diary('no time left after waiting — leaving it to the next try'); return; }
let handled = false;
try {
handled = await withRequestLock(data.event && data.event.id,
- () => respondFromBg(data, { notifier: NOTIFIER, log: diary }),
+ () => respondFromBg(data, { notifier: NOTIFIER, log: diary, deadline: budgetEnd }),
() => { diary('another context holds this request'); return true; });
} catch (err) {
diary('auto-answer failed: ' + (err && err.message));
@@ -243,21 +244,21 @@ self.addEventListener('push', (e) => {
await tend();
return;
}
+ if (Date.now() > budgetEnd) { diary('out of time — leaving it to the next try'); return; }
// A kind-21001 offer request is money coming IN (someone asking us to
// mint an invoice) — "an app wants to pay" would read as the opposite.
const incoming = data.event && data.event.kind === 21001;
diary(incoming ? 'waking the user: incoming payment' : 'waking the user: payment request');
- await show(incoming ? 'Incoming payment' : 'Payment request', {
- body: incoming
- ? 'Someone is trying to send you money. Open coinos so your wallet can receive it.'
- : 'An app is asking your wallet to pay. Open Coinos to approve.',
+ const [reqTitle, reqBody] = notifyText(incoming ? 'incoming' : 'spend');
+ await show(reqTitle, {
+ body: reqBody,
icon: 'icon-192.png', badge: 'badge-96.png',
tag: 'nwc-' + (data.servicePubkey || 'req'), renotify: false,
data: { url: './' },
});
if (apple) await dropStandIn();
await tend();
- });
+ }, { lease: () => takeLease('work', { wait: Math.max(0, budgetEnd - Date.now() - 3_000) }) });
diary(`push done in ${Date.now() - startedAt} ms`);
});
});