diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/calls.js | 12 | ||||
| -rw-r--r-- | src/dm-inbox.js | 9 | ||||
| -rw-r--r-- | src/features/messages.js | 40 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | src/style.css | 3 | ||||
| -rw-r--r-- | src/sw-nwc.js | 11 |
6 files changed, 65 insertions, 12 deletions
diff --git a/src/calls.js b/src/calls.js index 827d605..66d6f16 100644 --- a/src/calls.js +++ b/src/calls.js @@ -64,7 +64,8 @@ export const candidateJson = (c) => JSON.stringify({ candidate: c.candidate, sdp // send(peerPk, signed) wrap and publish to them (the app picks the relays) // iceServers() → RTCIceServer[] (TURN from coinos, STUN at least) // media(type, facing) → MediaStream: microphone, and camera for 'video' -// mayRing(pk) whether this person may ring us (followed, or a conversation) +// mayRing(pk) 'ring' (someone followed, or written to), 'quiet' (anyone else: +// no ring, but the call is told of, as missed), or null (muted: nothing) // 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 @@ -247,7 +248,14 @@ export function createCalls(env) { if (c.id !== id) { const s = await env.sign(callEvent(CALL.REJECT, from, id, 'busy')); env.send(from, s).catch(() => {}); } return; } - if (!(await env.mayRing(from))) return; + const may = await env.mayRing(from); + if (!may) return; + if (may === 'quiet') { + // not someone who rings us: no ring, but not nothing either — it's + // logged as a call that didn't ring + try { if (env.ended) env.ended({ id, peer: from, type: tagOf(ev, 'call-type') === 'video' ? 'video' : 'voice', dir: 'in', reason: 'unrung', connected: false, seconds: 0, at: ev.created_at * 1000 }); } catch {} + 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(), offerAt: ev.created_at * 1000, muted: false, camOff: false, facing: 'user' }; diff --git a/src/dm-inbox.js b/src/dm-inbox.js index 1a026c1..58fc72d 100644 --- a/src/dm-inbox.js +++ b/src/dm-inbox.js @@ -122,7 +122,8 @@ export function shouldNotifyDm(verdict) { // it's for, and worth ringing only as a fresh offer from someone this // account follows or talks to, and hasn't muted. Returns { offer, name, // type } to ring with, or null for anything else (a candidate, a stranger, -// a stale one, a wrap we hold no key for). +// a stale one, a wrap we hold no key for); `quiet` for a caller who +// may not ring us (not followed, not written to). export async function classifyCall(wrap, inboxes, nowS = Math.floor(Date.now() / 1000)) { for (const rec of inboxes) { if (!rec || !rec.sk) continue; @@ -133,9 +134,11 @@ export async function classifyCall(wrap, inboxes, nowS = Math.floor(Date.now() / const from = ev.pubkey; if (ev.kind !== CALL.OFFER || from === rec.pubkey || nowS - ev.created_at > RING_S) return null; if ((rec.muted || []).includes(from)) return null; - if (!(rec.follows || []).includes(from) && !(rec.known || []).includes(from)) return null; const type = (ev.tags.find((x) => x[0] === 'call-type') || [])[1] === 'video' ? 'video' : 'voice'; - return { offer: ev, name: (rec.names || {})[from] || null, type }; + // someone not followed nor written to doesn't ring — `quiet`: the app + // lists it as a call that didn't ring, next time it opens + const quiet = !(rec.follows || []).includes(from) && !(rec.rings || rec.known || []).includes(from); + return { offer: ev, name: (rec.names || {})[from] || null, type, quiet }; } return null; } diff --git a/src/features/messages.js b/src/features/messages.js index cde11dc..54c9cfe 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -2507,6 +2507,9 @@ export function messagesFeature(ctx) { ? [...new Set(newest.tags.filter((x) => x[0] === 'p' && /^[0-9a-f]{64}$/.test(x[1] || '')).map((x) => x[1]))] : []; const known = [...threads.keys()]; + // who may ring us when the app is shut: those we've written to (the + // follows ride separately); `known` stays the DM filter's wider list + const rings = known.filter((pk) => wroteTo(pk)); const names = {}; for (const pk of [...follows, ...known]) { const p = profiles.get(pk); @@ -2515,7 +2518,7 @@ export function messagesFeature(ctx) { await saveInbox(wallet._cacheKey(), { pubkey: id.pubkey, sk: id.signer instanceof Uint8Array ? bytesToHex(id.signer) : null, - follows, known, names, hasList: !!newest, updated: Date.now(), + follows, known, rings, names, hasList: !!newest, updated: Date.now(), muted: [...mutesNow().set], // no buzz from a muted sender either }); } catch { inboxAt = 0; } @@ -9091,8 +9094,18 @@ export function messagesFeature(ctx) { return navigator.mediaDevices.getUserMedia(type === 'video' ? { audio, video } : { audio }); } // who may ring: someone you follow, or someone you've written to - const mayRing = (pk) => !hiddenPk(pk) && (followsNow().set.has(pk) - || [...(threads.get(pk) || new Map()).values()].some((m) => m.mine)); + // who may ring: someone you follow, or someone you've written to. Anyone + // else's call doesn't ring but is told of, as a missed call that didn't + // ring (a wife's call must not vanish because you never followed her); + // a muted person's leaves nothing. + const mayRing = (pk) => hiddenPk(pk) ? null + : followsNow().set.has(pk) || wroteTo(pk) ? 'ring' : 'quiet'; + // a message of ours in the conversation — not a call logged there (a + // stranger's unrung call would otherwise let their next one ring) + function wroteTo(pk) { + for (const m of (threads.get(pk) || new Map()).values()) if (m.mine && m.rumor.kind !== CALL_LOG) return true; + return false; + } function callsNow() { if (calls) return calls; calls = createCalls({ @@ -9128,7 +9141,7 @@ export function messagesFeature(ctx) { // devices list it too. A call rung on two of our devices is logged by // both; the conversation shows it once, by its call-id. const callStatus = (rec) => rec.connected ? 'answered' - : rec.reason === 'missed' ? 'missed' + : rec.reason === 'missed' || rec.reason === 'unrung' ? 'missed' : rec.reason === 'declined' || rec.reason === 'peer-declined' ? 'declined' : rec.reason === 'no-answer' ? 'no-answer' : rec.reason === 'busy' ? 'busy' @@ -9157,7 +9170,7 @@ export function messagesFeature(ctx) { kind: CALL_LOG, pubkey: id.pubkey, created_at: Math.floor((rec.at || Date.now()) / 1000), content: callLogText(rec.dir, rec.type, status, rec.seconds), tags: [['p', rec.peer], ['call-id', rec.id], ['call-type', rec.type], ['direction', rec.dir], ['status', status], - ['duration', String(rec.seconds || 0)], ['alt', 'Call log']], + ['duration', String(rec.seconds || 0)], ...(rec.reason === 'unrung' ? [['rang', 'no']] : []), ['alt', 'Call log']], }); noteDM(rec.peer, rumor, true); persistDms(); @@ -9184,11 +9197,15 @@ export function messagesFeature(ctx) { const tag = (k) => (m.rumor.tags.find((x) => x[0] === k) || [])[1]; const type = tag('call-type') === 'video' ? 'video' : 'voice', status = tag('status') || 'failed', dir = tag('direction') || 'out'; const missed = status === 'missed' || (dir === 'in' && status === 'declined'); + // a call that didn't ring (someone you don't follow), saying so — and, + // while that's still so, how to have them ring next time + const unrung = tag('rang') === 'no'; return h('div', { class: 'chat-row call-log' + (missed ? ' missed' : ''), 'data-call-log': tag('call-id') || '' }, h('button', { type: 'button', class: 'call-log-pill', title: t(type === 'video' ? 'callVideo' : 'callVoice'), onClick: () => startCall(peer, type) }, callIcon(type === 'video' ? 'video' : 'phone'), h('span', {}, callLogText(dir, type, status, +tag('duration') || 0)), - h('span', { class: 'chat-time' }, timeLabel(m.rumor.created_at * 1000)))); + h('span', { class: 'chat-time' }, timeLabel(m.rumor.created_at * 1000))), + unrung ? h('div', { class: 'small faint call-log-why' }, t(mayRing(peer) === 'ring' ? 'callLogUnrungWas' : 'callLogUnrung')) : null); } // the call offer a push brought (sw-nwc.js keeps it): rung now, once @@ -9196,6 +9213,17 @@ export function messagesFeature(ctx) { try { if (typeof caches === 'undefined') return; const cache = await caches.open('coinos-call'); + // calls the worker saw from people who don't ring us: logged, unrung + const quiet = await cache.match('/__quiet-calls'); + if (quiet) { + await cache.delete('/__quiet-calls'); + for (const offer of (await quiet.json()) || []) { + if (!offer || !offer.pubkey || !mayRing(offer.pubkey)) continue; + const id = (offer.tags.find((x) => x[0] === 'call-id') || [])[1]; + const type = (offer.tags.find((x) => x[0] === 'call-type') || [])[1] === 'video' ? 'video' : 'voice'; + if (id) logCall({ id, peer: offer.pubkey, type, dir: 'in', reason: 'unrung', seconds: 0, at: offer.created_at * 1000 }).catch(() => {}); + } + } const r = await cache.match('/__pending-call'); if (!r) return; await cache.delete('/__pending-call'); diff --git a/src/i18n.js b/src/i18n.js index 0189616..b7432b0 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -530,6 +530,8 @@ const STR = { callLogCancelled: 'Cancelled {what}', callLogBusy: 'Outgoing {what} · Busy', callLogFailed: '{what} · Couldn’t connect', + callLogUnrung: 'This didn’t ring: calls ring from people you follow or have written to.', + callLogUnrungWas: 'This didn’t ring: you didn’t follow them then.', feedAttach: 'Attach a picture or video', postLike: 'Like', postBoost: 'Boost', diff --git a/src/style.css b/src/style.css index dff1c50..235cb4b 100644 --- a/src/style.css +++ b/src/style.css @@ -1942,7 +1942,8 @@ body::after { .call-min-status { font-variant-numeric: tabular-nums; opacity: .9; } .call-min .call-audio { display: none; } /* a call in its conversation: a row of its own, centred, a tap calls back */ -.chat-row.call-log { justify-content: center; margin: 6px 0; } +.chat-row.call-log { justify-content: center; margin: 6px 0; flex-direction: column; align-items: center; gap: 3px; } +.call-log-why { text-align: center; max-width: 320px; } .call-log-pill { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface2); color: inherit; font-size: 13px; cursor: pointer; } .call-log-pill .chat-time { margin: 0; font-size: 11px; opacity: .7; } diff --git a/src/sw-nwc.js b/src/sw-nwc.js index b6af6b8..f9d383e 100644 --- a/src/sw-nwc.js +++ b/src/sw-nwc.js @@ -35,6 +35,17 @@ self.addEventListener('push', (e) => { 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; + if (ring.quiet) { + // no ring for them; a note for the app, which lists it as a missed call + try { + const cache = await caches.open(CALL_CACHE); + const had = await cache.match('/__quiet-calls'); + const list = had ? await had.json() : []; + list.push(ring.offer); + await cache.put('/__quiet-calls', new Response(JSON.stringify(list.slice(-20)), { headers: { 'content-type': 'application/json' } })); + } catch (_) {} + 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' } })); |
