chore: floating button for incoming call

This commit is contained in:
Sojan
2025-04-29 03:45:08 -07:00
parent 4c579bc71e
commit 3f0c01e166
11 changed files with 896 additions and 314 deletions
@@ -61,6 +61,16 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
}
).perform
# Broadcast incoming call notification on account-level channel
broadcast_call_status('incoming_call', {
call_sid: call_sid,
conversation_id: conversation.id,
inbox_id: inbox.id,
inbox_name: inbox.name,
contact_name: contact.name || from_number,
contact_id: contact.id
}, account_id: inbox.account_id)
# Generate minimal TwiML response
response = Twilio::TwiML::VoiceResponse.new
response.say(message: 'Thank you for calling. An agent will be with you shortly.')
@@ -144,18 +154,12 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
}
).perform
# Broadcast minimal update to frontend
ActionCable.server.broadcast(
"#{conversation.account_id}_#{conversation.inbox_id}",
{
event_name: 'call_status_changed',
data: {
call_sid: call_sid,
status: conversation.additional_attributes['call_status'] || 'in-progress',
conversation_id: conversation.id
}
}
)
# Broadcast call status updates on account-level channel
broadcast_call_status('call_status_changed', {
call_sid: call_sid,
status: conversation.additional_attributes['call_status'] || 'in-progress',
conversation_id: conversation.id
}, account_id: conversation.account_id)
# Return minimal response
head :ok
@@ -210,4 +214,17 @@ class Api::V1::Accounts::Channels::Voice::WebhooksController < Api::V1::Accounts
def base_url
ENV.fetch('FRONTEND_URL', "https://#{request.host_with_port}")
end
def broadcast_call_status(event_name, data, account_id:)
# Include account_id in the data to help with validation
data_with_account = data.merge(account_id: account_id)
ActionCable.server.broadcast(
"account_#{account_id}",
{
event: event_name,
data: data_with_account
}
)
end
end
@@ -1,5 +1,5 @@
class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:end_call, :call_status]
before_action :fetch_conversation, only: [:end_call, :join_call, :reject_call, :call_status]
def end_call
call_sid = params[:call_sid] || @conversation.additional_attributes&.dig('call_sid')
@@ -53,6 +53,121 @@ class Api::V1::Accounts::VoiceController < Api::V1::Accounts::BaseController
end
end
def join_call
call_sid = params[:call_sid] || @conversation.additional_attributes&.dig('call_sid')
return render json: { error: 'No active call found' }, status: :not_found unless call_sid
# Get the inbox and channel information
inbox = @conversation.inbox
channel = inbox&.channel
if channel.is_a?(Channel::Voice) && channel.provider == 'twilio'
config = channel.provider_config_hash
begin
# Create a Twilio client
client = Twilio::REST::Client.new(config['account_sid'], config['auth_token'])
# Get the conference SID from the conversation attributes
# For incoming calls, Twilio typically places the caller in a conference that agents can join
conference_sid = @conversation.additional_attributes&.dig('conference_sid')
if conference_sid
# Create a call that connects the agent to the conference
client.calls.create(
to: current_user.phone_number || config['agent_phone_number'],
from: channel.phone_number,
status_callback: "#{ENV.fetch('FRONTEND_URL', nil)}/twilio/voice/status_callback",
status_callback_event: ['initiated', 'ringing', 'answered', 'completed'],
status_callback_method: 'POST',
url: "#{ENV.fetch('FRONTEND_URL', nil)}/twilio/voice/twiml?conference_sid=#{conference_sid}&agent_id=#{current_user.id}"
)
# Update conversation to show agent joined
@conversation.additional_attributes ||= {}
@conversation.additional_attributes['agent_joined'] = true
@conversation.additional_attributes['joined_at'] = Time.now.to_i
@conversation.additional_attributes['joined_by'] = {
id: current_user.id,
name: current_user.name
}
@conversation.save!
# Create an activity message noting the agent joined
Messages::MessageBuilder.new(
nil,
@conversation,
{
content: "#{current_user.name} joined the call",
message_type: :activity,
additional_attributes: {
call_sid: call_sid,
conference_sid: conference_sid,
joined_by: current_user.name,
joined_at: Time.now.to_i
}
}
).perform
render json: {
status: 'success',
message: 'Agent joining call',
conference_sid: conference_sid
}
else
render json: { error: 'Conference not found for this call' }, status: :unprocessable_entity
end
rescue Twilio::REST::RestError => e
render json: { error: "Failed to join call: #{e.message}" }, status: :internal_server_error
end
else
render json: { error: 'Unsupported channel provider for call control' }, status: :unprocessable_entity
end
end
def reject_call
call_sid = params[:call_sid] || @conversation.additional_attributes&.dig('call_sid')
return render json: { error: 'No active call found' }, status: :not_found unless call_sid
# Get the inbox and channel information
inbox = @conversation.inbox
channel = inbox&.channel
if channel.is_a?(Channel::Voice) && channel.provider == 'twilio'
# Update conversation to show agent rejected call
@conversation.additional_attributes ||= {}
@conversation.additional_attributes['agent_rejected'] = true
@conversation.additional_attributes['rejected_at'] = Time.now.to_i
@conversation.additional_attributes['rejected_by'] = {
id: current_user.id,
name: current_user.name
}
@conversation.save!
# Create an activity message noting the agent rejected the call
Messages::MessageBuilder.new(
nil,
@conversation,
{
content: "#{current_user.name} declined to answer",
message_type: :activity,
additional_attributes: {
call_sid: call_sid,
rejected_by: current_user.name,
rejected_at: Time.now.to_i
}
}
).perform
render json: {
status: 'success',
message: 'Call rejected by agent'
}
else
render json: { error: 'Unsupported channel provider for call control' }, status: :unprocessable_entity
end
end
def call_status
call_sid = @conversation.additional_attributes&.dig('call_sid')
return render json: { error: 'No call found' }, status: :not_found unless call_sid