Files
c4a6a19e9b feat(voice): WhatsApp Cloud Calling — UI [6] (#14346)
## Summary

Frontend for WhatsApp Cloud Calling: header / contact-panel call
buttons, ringing widget, accept/reject/hangup, mute, in-bubble audio
player + transcript, recording-on-hangup upload, mid-call reload
warning. WebRTC is browser-direct to Meta — no media server bridge.

## Closes
- https://linear.app/chatwoot/issue/PLA-150

## How to test

Requires backend support — the controller, services, model changes, and
routes ship in **#14334** (`feature/pla-150`). Merge / deploy that first
(or simultaneously); the FE alone won't function without those
endpoints.

Then on staging, for a WhatsApp Cloud + embedded-signup inbox with the
new \`Configuration → Enable voice calling\` toggle ON and webhook
registered:

1. **Outbound** — open a conversation, click the phone icon in the
conversation header (or contact panel), grant mic, your phone rings,
answer, audio both ways, hang up. Recording + transcript land in the
bubble within ~10s.
2. **Inbound** — call the business number from your phone. The
FloatingCallWidget appears bottom-right with caller name. Click accept,
audio both ways, hang up. Recording + transcript appear.
3. **Mute** — during an active WhatsApp call, click the mic icon next to
hangup. Speech stops reaching Meta until you click again.
4. **Mid-call reload guard** — try `Cmd-R` during an active call;
browser shows a confirm prompt.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-22 18:42:39 +05:30

48 lines
1.6 KiB
Ruby

class Voice::CallStatus::Manager
pattr_initialize [:call!]
def process_status_update(status, duration: nil, timestamp: nil)
return unless Call::STATUSES.include?(status)
return if call.status == status
# Don't overwrite a terminal status — Twilio's late `completed` events would
# otherwise clobber an agent-rejection reason.
return if Call::TERMINAL_STATUSES.include?(call.status)
apply_call_updates!(status, duration: duration, timestamp: timestamp)
call.conversation.update!(last_activity_at: Time.zone.now)
# Bump updated_at so the message.updated dispatcher rebroadcasts with the fresh Call embedded.
call.message&.touch # rubocop:disable Rails/SkipsModelValidations
end
private
def apply_call_updates!(status, duration:, timestamp:)
attrs = { status: status }
ts = timestamp || now_seconds
if status == 'in_progress'
# Twilio can emit multiple in-progress updates (answered + in-progress, retries).
# Keep the earliest timestamp so duration_seconds doesn't shift forward.
started_at = Time.zone.at(ts)
attrs[:started_at] = started_at if call.started_at.nil? || started_at < call.started_at
elsif Call::TERMINAL_STATUSES.include?(status)
call.ended_at = ts
attrs[:meta] = call.meta
attrs[:duration_seconds] = resolved_duration(duration, ts)
end
call.update!(attrs)
end
def resolved_duration(provided_duration, timestamp)
return provided_duration if provided_duration
return unless call.started_at
[timestamp - call.started_at.to_i, 0].max
end
def now_seconds
Time.zone.now.to_i
end
end