feat: wire WebRTC call signaling end-to-end

Connects all call infrastructure so calls flow through the system:

- EventProcessor routes unwrapped call events (offer/answer/ICE/
  hangup/reject) from gift wraps to CallManager
- CallController orchestrates WebRTC session lifecycle: creates
  PeerConnection, generates SDP offers/answers, exchanges ICE
  candidates via gift-wrapped events, and manages foreground service
- AccountViewModel initializes CallManager + CallController and
  wires answer/ICE callbacks between them
- Account.publishCallSignaling() publishes gift-wrapped events
- DM chat top bar gets a call button (1-on-1 rooms only) that
  initiates a voice call and navigates to ActiveCall screen
- ActiveCall route registered in AppNavigation with CallScreen

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
Claude
2026-04-01 22:40:13 +00:00
parent 3c486a383b
commit d39c8801c9
8 changed files with 312 additions and 3 deletions
@@ -51,6 +51,9 @@ class CallManager(
private val _state = MutableStateFlow<CallState>(CallState.Idle)
val state: StateFlow<CallState> = _state.asStateFlow()
var onAnswerReceived: ((CallAnswerEvent) -> Unit)? = null
var onIceCandidateReceived: ((CallIceCandidateEvent) -> Unit)? = null
private var timeoutJob: Job? = null
companion object {
@@ -115,6 +118,7 @@ class CallManager(
_state.value = CallState.Connecting(current.callId, current.peerPubKey, current.callType)
cancelTimeout()
onAnswerReceived?.invoke(event)
}
fun onCallRejected(event: CallRejectEvent) {
@@ -127,8 +131,7 @@ class CallManager(
}
fun onIceCandidate(event: CallIceCandidateEvent) {
// ICE candidates are handled by the WebRTC session directly.
// This method exists for the call manager to validate the call-id.
onIceCandidateReceived?.invoke(event)
}
fun onPeerConnected() {