aboutsummaryrefslogtreecommitdiffstats
path: root/src/calls.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-18 09:01:40 -0700
committerAdam Soltys <[email protected]>2026-10-18 09:01:41 -0700
commitbfa52c7a2cf1cc06455536d9a6676b0003a30452 (patch)
tree436e2411ea1238a124543dadeb13c77b85d1f270 /src/calls.js
parent39083f3ca2f80c7c51fefffb1e473f3d0a7d4ed6 (diff)
downloadcoinosv3-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/calls.js')
-rw-r--r--src/calls.js25
1 files changed, 19 insertions, 6 deletions
diff --git a/src/calls.js b/src/calls.js
index 2447a2f..98d0a46 100644
--- a/src/calls.js
+++ b/src/calls.js
@@ -23,7 +23,8 @@ const ALT = {
[CALL.HANGUP]: 'WebRTC call hangup', [CALL.REJECT]: 'WebRTC call rejection', [CALL.RENEGOTIATE]: 'WebRTC call renegotiation',
};
export const STALE_S = 20; // older setup messages are a relay replaying the past
-const RING_MS = 60_000;
+export const RING_S = 60;
+const RING_MS = RING_S * 1000;
const ENDED_MS = 2500; // how long "Call ended" stays up
const now = () => Math.floor(Date.now() / 1000);
@@ -102,7 +103,12 @@ export function createCalls(env) {
c.remote = new MediaStream();
pc.ontrack = (e) => { for (const t of (e.streams[0] || { getTracks: () => [e.track] }).getTracks()) if (!c.remote.getTracks().includes(t)) c.remote.addTrack(t); changed(); };
c.trickle = env.trickle ? env.trickle() !== false : true;
- pc.onicecandidate = (e) => { if (c.trickle && e.candidate && call === c && c.state !== 'ended') sendFor(c, CALL.ICE, c.peer, candidateJson(e.candidate)).catch(() => {}); };
+ c.sent = [];
+ pc.onicecandidate = (e) => {
+ if (!c.trickle || !e.candidate || call !== c || c.state === 'ended') return;
+ c.sent.push(e.candidate);
+ sendFor(c, CALL.ICE, c.peer, candidateJson(e.candidate)).catch(() => {});
+ };
pc.onconnectionstatechange = () => {
if (call !== c) return;
const s = pc.connectionState;
@@ -218,11 +224,14 @@ export function createCalls(env) {
}
// ---- what arrives
- async function handle(ev) {
+ // `maxAge`: an offer the service worker kept from a push may be older than
+ // a relay's live one (the person had to tap the notification): it still
+ // rings, for what's left of its minute
+ async function handle(ev, { maxAge = STALE_S } = {}) {
if (!ev || seen.has(ev.id)) return;
+ if (now() - ev.created_at > maxAge) return;
seen.add(ev.id);
if (seen.size > 5000) seen.clear();
- if (now() - ev.created_at > STALE_S) return;
const me = env.me();
const id = callIdOf(ev), from = ev.pubkey, self = from === me;
const c = call;
@@ -237,7 +246,7 @@ export function createCalls(env) {
clearTimeout(endTimer);
const type = tagOf(ev, 'call-type') === 'video' ? 'video' : 'voice';
const n = call = { id, peer: from, type, dir: 'in', state: 'incoming', offer: ev.content, ice: [], at: Date.now(), muted: false, camOff: false, facing: 'user' };
- ringTimer = setTimeout(() => { if (call === n && n.state === 'incoming') end('missed', n); }, RING_MS);
+ ringTimer = setTimeout(() => { if (call === n && n.state === 'incoming') end('missed', n); }, Math.max(5000, RING_MS - (Date.now() - ev.created_at * 1000)));
changed();
return;
}
@@ -248,7 +257,11 @@ export function createCalls(env) {
if (c.state === 'offering') {
c.state = 'connecting'; changed();
try { await c.pc.setRemoteDescription({ type: 'answer', sdp: ev.content }); c.remoteSet = true; await addCandidates(c); }
- catch { end('error', c); }
+ catch { end('error', c); return; }
+ // Our candidates went out while they rang — to an app that may have
+ // been closed then (woken by a push, it heard none of them). Once
+ // more, now that they're listening; a repeat is harmless.
+ if (c.trickle) for (const cand of c.sent || []) sendFor(c, CALL.ICE, c.peer, candidateJson(cand)).catch(() => {});
} else if (c.renegotiating) { // their answer to our renegotiation
c.renegotiating = false;
try { await c.pc.setRemoteDescription({ type: 'answer', sdp: ev.content }); } catch {}