From aaebbd5d51e0e149c36975122dd0ae59e6c011bb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 18 Oct 2026 09:27:09 -0700 Subject: Call history: each call is a row in its conversation โ€” Outgoing / Incoming video call ยท 4:12, Missed, Declined, No answer, Cancelled, Busy โ€” a tap calls back. Kept like a message and sent to our own inbox (kind 1405, gift-wrapped to us alone) so our other devices list it; a call logged by two of our devices shows once, by its call-id. A call that rang while the app was shut and went unanswered is a missed call when it next opens. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg --- src/calls.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src/calls.js') diff --git a/src/calls.js b/src/calls.js index 98d0a46..827d605 100644 --- a/src/calls.js +++ b/src/calls.js @@ -66,6 +66,7 @@ export const candidateJson = (c) => JSON.stringify({ candidate: c.candidate, sdp // media(type, facing) โ†’ MediaStream: microphone, and camera for 'video' // mayRing(pk) whether this person may ring us (followed, or a conversation) // changed() the call's state moved: draw it +// ended(record) a call is over: { id, peer, type, dir, reason, connected, seconds, at } for its log // trickle() false for a signer that's slow or asks each time (a // bunker, Amber): the candidates go inside the offer or // answer instead of one signed message each @@ -93,6 +94,10 @@ export function createCalls(env) { stopMedia(c); c.state = 'ended'; c.reason = reason; c.endedAt = Date.now(); changed(); + try { + if (env.ended) env.ended({ id: c.id, peer: c.peer, type: c.type, dir: c.dir, reason, at: c.offerAt || c.at, connected: !!c.connectedAt, + seconds: c.connectedAt ? Math.round((c.endedAt - c.connectedAt) / 1000) : 0 }); + } catch {} clearTimeout(endTimer); endTimer = setTimeout(() => { if (call === c) { call = null; changed(); } }, ENDED_MS); } @@ -245,7 +250,7 @@ export function createCalls(env) { if (!(await env.mayRing(from))) return; 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' }; + const n = call = { id, peer: from, type, dir: 'in', state: 'incoming', offer: ev.content, ice: [], at: Date.now(), offerAt: ev.created_at * 1000, muted: false, camOff: false, facing: 'user' }; ringTimer = setTimeout(() => { if (call === n && n.state === 'incoming') end('missed', n); }, Math.max(5000, RING_MS - (Date.now() - ev.created_at * 1000))); changed(); return; -- cgit v1.2.3