Files
amethyst/quartz
Claude b9d6cefbeb feat(call): per-peer 30s invite timeout and per-peer status in video grid
Two changes that tighten how the caller-side UX handles slow or
unresponsive callees in a group call:

1. Per-peer 30-second invite timeout

   Previously the caller used a single 60-second call-wide timeout: if
   *any* peer answered within the window the timer was cancelled and
   slow peers could remain "ringing" indefinitely. For a mid-call
   invite there was no timeout at all — the invitee stayed in
   pendingPeerPubKeys forever, burning a PeerConnection on the caller
   side and keeping the invitee's device ringing.

   CallManager now schedules an independent 30-second timer for every
   peer in pendingPeerPubKeys (or Offering.peerPubKeys in the initial
   offer phase). The timer is started when the peer is added to
   pending — in beginOffering, initiateCall and invitePeer — and is
   cancelled when the peer answers (onCallAnswered), rejects
   (onCallRejected) or hangs up (onPeerHangup). On expiry the peer is
   dropped from the group, a CallHangup is published to them so their
   device stops ringing, and onPeerLeft fires so CallController can
   dispose the per-peer PeerConnection. If the drop leaves the caller
   with zero connected and zero pending peers, the call ends with
   EndReason.TIMEOUT.

   Callee ringing still uses the 60-second CALL_TIMEOUT_MS in
   onIncomingCallEvent; the two timers now serve distinct roles.

   New constant CallManager.PEER_INVITE_TIMEOUT_MS = 30_000L.

   NIP-AC.md "Event Lifecycle" documents both timers.

2. Per-peer "Calling..." status in the video grid

   The shared "Waiting for others to join…" banner across the top of
   ConnectedCallUI is removed. PeerVideoGrid now takes a
   pendingPeerPubKeys set and, for each peer still pending, routes
   rendering through PeerAvatarCell with a "Calling…" status line
   under the username — so it's obvious *which* participants the call
   is waiting on, not just *that* it's waiting. A peer in pending
   never shows as a video cell even if a stale track is still in the
   map, because they haven't actually answered yet.

   The voice-only path in ConnectedCallUI now also uses PeerVideoGrid
   (with empty tracks/active-video) instead of the single
   GroupCallPictures + GroupCallNames stack, so the per-peer status
   behavior is consistent for audio calls.

Tests added in CallManagerTest:
- perPeerTimeoutEndsP2PCallWhenBobNeverAnswers
- perPeerTimeoutDropsSlowCalleeFromGroupCall
- perPeerTimeoutIsCancelledOnAnswer
- perPeerTimeoutDropsMidCallInviteeWhenNoAnswer
- perPeerTimeoutIsCancelledOnReject
- perPeerTimeoutEndsCallWhenAllCalleesIgnore

All existing CallManagerTest cases still pass unchanged.

https://claude.ai/code/session_01XSPDbahLwHs9sdF5XfRvHB
2026-04-14 23:23:42 +00:00
..