When an inbound voice call ends, the conversation bubble now (1) renders an inline audio player as soon as Twilio finishes the recording and (2) shows the call duration alongside "Call ended" so the agent gets the at-a-glance summary without opening the recording. Fixes https://linear.app/chatwoot/issue/PLA-118/feat-recordings-on-calls-should-be-attached-on-the-conversation and https://linear.app/chatwoot/issue/PLA-119/duration-of-the-call-is-not-visible-on-the-chat-bubble ## How to test 1. Set up a Twilio voice inbox and trigger an inbound call. 2. Answer the call from an agent, talk for a few seconds, then hang up. 3. As soon as the call ends, the bubble should read **"Call ended — 0:NN"** (where NN is the call duration in seconds). 4. Wait a few seconds for Twilio to finish processing the recording (usually <30s after hangup). 5. The same bubble should now show an inline audio player below the duration. Press play; the recording should be audible. 6. Refresh the page — both the duration and the player should still be there. 7. End a second call on the same conversation — its bubble should get its own duration + player, independent of the first. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
106 lines
3.4 KiB
Ruby
106 lines
3.4 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: calls
|
|
#
|
|
# id :bigint not null, primary key
|
|
# direction :integer not null
|
|
# duration_seconds :integer
|
|
# end_reason :string
|
|
# meta :jsonb
|
|
# provider :integer default("twilio"), not null
|
|
# started_at :datetime
|
|
# status :string default("ringing"), not null
|
|
# transcript :text
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# accepted_by_agent_id :bigint
|
|
# account_id :bigint not null
|
|
# contact_id :bigint not null
|
|
# conversation_id :bigint not null
|
|
# inbox_id :bigint not null
|
|
# message_id :bigint
|
|
# provider_call_id :string not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_calls_on_account_id_and_contact_id (account_id,contact_id)
|
|
# index_calls_on_account_id_and_conversation_id (account_id,conversation_id)
|
|
# index_calls_on_message_id (message_id)
|
|
# index_calls_on_provider_and_provider_call_id (provider,provider_call_id) UNIQUE
|
|
#
|
|
class Call < ApplicationRecord
|
|
# All valid call statuses
|
|
STATUSES = %w[ringing in_progress completed no_answer failed].freeze
|
|
# Statuses where the call is finished and won't change again
|
|
TERMINAL_STATUSES = %w[completed no_answer failed].freeze
|
|
|
|
store_accessor :meta, :conference_sid, :twilio_conference_sid, :recording_sid, :parent_call_sid, :initiated_at, :ended_at
|
|
|
|
enum :provider, { twilio: 0, whatsapp: 1 }
|
|
enum :direction, { incoming: 0, outgoing: 1 }
|
|
|
|
belongs_to :account
|
|
belongs_to :inbox
|
|
belongs_to :conversation
|
|
belongs_to :contact
|
|
belongs_to :message, optional: true, inverse_of: :call
|
|
belongs_to :accepted_by_agent, class_name: 'User', optional: true
|
|
|
|
has_one_attached :recording
|
|
|
|
validates :provider_call_id, presence: true
|
|
validates :provider, presence: true
|
|
validates :direction, presence: true
|
|
validates :status, presence: true, inclusion: { in: STATUSES }
|
|
|
|
scope :active, -> { where.not(status: TERMINAL_STATUSES) }
|
|
scope :by_conference_sid, ->(sid) { where("meta->>'conference_sid' = ?", sid) }
|
|
scope :by_twilio_conference_sid, ->(sid) { where("meta->>'twilio_conference_sid' = ?", sid) }
|
|
|
|
def self.find_by_provider_call_id(provider, sid)
|
|
find_by(provider: provider, provider_call_id: sid)
|
|
end
|
|
|
|
def default_conference_sid
|
|
"conf_account_#{account_id}_call_#{id}"
|
|
end
|
|
|
|
def display_status
|
|
status.to_s.tr('_', '-')
|
|
end
|
|
|
|
def from_number
|
|
incoming? ? contact.phone_number : inbox.channel&.phone_number
|
|
end
|
|
|
|
def to_number
|
|
incoming? ? inbox.channel&.phone_number : contact.phone_number
|
|
end
|
|
|
|
def recording_url
|
|
return nil unless recording.attached?
|
|
|
|
Rails.application.routes.url_helpers.rails_blob_url(recording)
|
|
end
|
|
|
|
def push_event_data
|
|
{
|
|
id: id,
|
|
provider_call_id: provider_call_id,
|
|
provider: provider,
|
|
direction: direction,
|
|
status: display_status,
|
|
duration_seconds: duration_seconds,
|
|
conference_sid: conference_sid,
|
|
accepted_by_agent_id: accepted_by_agent_id,
|
|
accepted_by_agent_name: accepted_by_agent&.available_name,
|
|
started_at: started_at&.to_i,
|
|
ended_at: ended_at,
|
|
from_number: from_number,
|
|
to_number: to_number,
|
|
recording_url: recording_url,
|
|
transcript: transcript
|
|
}
|
|
end
|
|
end
|