diff options
| author | Adam Soltys <[email protected]> | 2026-10-18 09:01:40 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-18 09:01:41 -0700 |
| commit | bfa52c7a2cf1cc06455536d9a6676b0003a30452 (patch) | |
| tree | 436e2411ea1238a124543dadeb13c77b85d1f270 /src/sw-nwc.js | |
| parent | 39083f3ca2f80c7c51fefffb1e473f3d0a7d4ed6 (diff) | |
| download | coinosv3-bfa52c7a2cf1cc06455536d9a6676b0003a30452.tar.gz coinosv3-bfa52c7a2cf1cc06455536d9a6676b0003a30452.zip | |
Calls ring a closed app: the notifier wakes the phone for a call offer, the service worker fetches the offer's wrap, opens it with your key and rings — "Alice is calling", until answered or missed — for someone you follow or talk to; the tap opens coinos ringing with the offer it kept. A call answered from a push still connects: the caller sends its candidates again once answered. Settings → Notifications → Calls. Also: the worker's DM key mirror retries when the wallet's keys aren't there yet (it waited five minutes).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'src/sw-nwc.js')
| -rw-r--r-- | src/sw-nwc.js | 35 |
1 files changed, 34 insertions, 1 deletions
diff --git a/src/sw-nwc.js b/src/sw-nwc.js index 3dac246..b6af6b8 100644 --- a/src/sw-nwc.js +++ b/src/sw-nwc.js @@ -7,15 +7,48 @@ import { respondFromBg, tendRenewals } from './nwc-respond.js'; import { withRequestLock } from './nwc-lock.js'; import { bgAutoWithdraw } from './bg-autowithdraw.js'; -import { allInboxes, classifyDm, shouldNotifyDm } from './dm-inbox.js'; +import { allInboxes, classifyDm, shouldNotifyDm, classifyCall } from './dm-inbox.js'; const NOTIFIER = 'https://nwcpush.coinos.io'; +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 +const WRAPS = /^(localhost|127\.0\.0\.1)$/.test(self.location.hostname) ? self.location.origin + '/nwcpush' : NOTIFIER; self.addEventListener('push', (e) => { let data = {}; try { data = e.data ? e.data.json() : {}; } catch (_) {} // User-facing notifications (payments, DMs, community chat). An open // window already shows everything live — stay quiet then. + // A call ringing in: the push names the offer's wrap, kept at the notifier + // (too big to ride in a push). Opened here with our key; a fresh offer + // from someone we follow or talk to rings, as a notification that stays + // until it's answered or missed. The offer is kept for the app to pick up + // when the tap opens it (CALL_CACHE). An open window rings by itself. + if (data.type === 'notify' && data.reason === 'call') { + e.waitUntil((async () => { + const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); + if (clients.some((c) => c.visibilityState === 'visible')) return; + let ring = null; + try { + const r = await fetch(WRAPS + '/wrap/' + encodeURIComponent(data.wrapId || '')); + if (r.ok) ring = await classifyCall(await r.json(), await allInboxes()); + } catch (err) { console.warn('[sw-call] could not open:', err && err.message); } + if (!ring) return; + try { + const cache = await caches.open(CALL_CACHE); + 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 self.registration.showNotification((ring.name || 'Someone') + ' is calling', { + body: ring.type === 'video' ? 'Video call. Tap to answer.' : 'Voice call. Tap to answer.', + 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' }, + }); + })()); + return; + } if (data.type === 'notify') { e.waitUntil((async () => { const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); |
