feat(voice): DM voice/video calls — call button, ring/accept/decline UI (#127)#134
Conversation
Adds 1:1 and group-DM calling on top of the existing channel-agnostic voice stack, wiring the new server-side call signaling (accordserver #32). accordkit: - AccordCallSignal model + call.ring/decline/cancel/end gateway streams - VoiceApi.ring / declineCall / cancelCall REST endpoints - forwarded streams on AccordClient client: - CallController: outgoing call (join voice + ring), incoming ring state, accept (implicit join) / decline / cancel, and gateway-driven transitions - call.* events wired through the per-connection event handler; a peer joining the ringing channel clears the "Calling…" state - VoiceController.join accepts a null spaceId for DM calls - call/video buttons in the DM conversation header; outgoing calls open the full-screen voice view with a "Calling…" banner that pops when the call ends - IncomingCallOverlay banner (accept/decline) mounted app-wide, with a looping ringtone and a "Call declined" snackbar - ringtone assets (incoming + outgoing ringback) Tests: accordkit endpoint + gateway dispatch tests; CallState.copyWith unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The outgoing-call ringback was dead code: `startRingtone(outgoing: true)` and the bundled `ring_outgoing.wav` existed but nothing called them, so the caller saw a "Calling…" spinner in silence while the callee heard a ringtone. Wire the ringback in `startCall` and stop it the moment the call is answered, declined, or cancelled. Also fix a state leak: hitting the in-call "Disconnect" button while still ringing routes through `VoiceController.leave()`, which never cleared `CallController.outgoingChannelId` — so the "Calling…" state (and now the ringback) leaked after a manual hang-up. A `build()` listener on the voice channel clears the outgoing state and silences the ringback whenever the ringing session ends by any path the explicit transitions don't cover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- acceptIncoming: return null when voice join fails, preventing the full-screen view from opening for a channel we aren't actually connected to - startCall: set outgoingChannelId synchronously before the first await so concurrent taps (before the join completes) are blocked by hasOutgoing; also clear it on join failure or missing client - cancelOutgoing: guard at top — no-ops when there is no outgoing call, so it cannot disconnect an unrelated voice session - startCall / markAnswered / handleDecline / cancelOutgoing: wire the outgoing ringback tone (ring_outgoing.wav was added but never played) - handleRing / handleDecline / handleCancelOrEnd / accept/declineIncoming: wrap fire-and-forget sound futures in unawaited() to make intent explicit - test: add hasOutgoing group documenting the re-entrancy guard semantics
Code Review — DM voice/video callsOverall the PR is well-structured and follows existing conventions. Five issues were found and fixed in the follow-up commit ( 🔴 HIGH —
|
Closes #127. Implements DM voice/video calling on top of the existing channel-agnostic voice stack, consuming the new server-side call signaling from accordserver #32 (DM
voice/join+call.ring/call.decline/call.cancel/call.end).accordkit (SDK)
AccordCallSignalmodel for thecall.*events.onCallRing/onCallDecline/onCallCancel/onCallEnd(dispatch + getters), forwarded onAccordClient.VoiceApi.ring/declineCall/cancelCall(POST /channels/{id}/call/{ring,decline,cancel}).Client
CallController(Riverpod, keepAlive) owns app-wide call state:spaceId: null) +call/ring; optional video;call.ring;call/decline, cancel → leave +call/cancel;call.declineends our outgoing call,call.cancel/call.enddismiss an incoming ring.call.*events wired into the per-connection event handler (they're targeted at DM participants, so they're honored on whichever connection owns the DM).VoiceController.joinnow accepts a nullablespaceIdfor DM calls.IncomingCallOverlay: app-wide ring banner (caller avatar/name, accept/decline), a looping ringtone, and a "Call declined" snackbar. In-call UI reusesVoiceChannelViewwith no space chrome.Tests
dart testgreen.CallState.copyWithunit tests (clear-flag semantics) —flutter testgreen.flutter analyzeclean across all changed files.Notes / follow-ups
voice.state_updateon join/leave.lib/features/spaces/views/accord_home_channels.dartexist on the base branch and are untouched here.🤖 Generated with Claude Code