chore: new floating widget design
This commit is contained in:
@@ -146,6 +146,27 @@ export default {
|
||||
this.showCallWidget = false;
|
||||
this.$store.dispatch('calls/clearActiveCall');
|
||||
this.$store.dispatch('calls/clearIncomingCall');
|
||||
|
||||
// Clear the activeCallConversation state in all ContactInfo components
|
||||
this.$nextTick(() => {
|
||||
const clearContactInfoCallState = (components) => {
|
||||
if (!components) return;
|
||||
|
||||
components.forEach(component => {
|
||||
if (component.$options && component.$options.name === 'ContactInfo') {
|
||||
if (component.activeCallConversation) {
|
||||
component.activeCallConversation = null;
|
||||
component.$forceUpdate();
|
||||
}
|
||||
}
|
||||
if (component.$children && component.$children.length) {
|
||||
clearContactInfoCallState(component.$children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
clearContactInfoCallState(this.$children);
|
||||
});
|
||||
},
|
||||
handleCallJoined() {
|
||||
this.showCallWidget = true;
|
||||
@@ -249,6 +270,10 @@ export default {
|
||||
:contact-name="activeCall ? activeCall.contactName : (incomingCall ? incomingCall.contactName : '')"
|
||||
:contact-id="activeCall ? activeCall.contactId : (incomingCall ? incomingCall.contactId : null)"
|
||||
:inbox-id="activeCall ? activeCall.inboxId : (incomingCall ? incomingCall.inboxId : null)"
|
||||
:inbox-avatar-url="activeCall ? activeCall.inboxAvatarUrl : (incomingCall ? incomingCall.inboxAvatarUrl : '')"
|
||||
:inbox-phone-number="activeCall ? activeCall.inboxPhoneNumber : (incomingCall ? incomingCall.inboxPhoneNumber : '')"
|
||||
:avatar-url="activeCall ? activeCall.avatarUrl : (incomingCall ? incomingCall.avatarUrl : '')"
|
||||
:phone-number="activeCall ? activeCall.phoneNumber : (incomingCall ? incomingCall.phoneNumber : '')"
|
||||
:use-web-rtc="true"
|
||||
@callEnded="handleCallEnded"
|
||||
@callJoined="handleCallJoined"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -203,17 +203,19 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
conversationId: data.conversation_id,
|
||||
inboxId: data.inbox_id,
|
||||
inboxName: data.inbox_name,
|
||||
contactName: data.contact_name,
|
||||
inboxAvatarUrl: data.inbox_avatar_url, // Inbox avatar URL
|
||||
inboxPhoneNumber: data.inbox_phone_number, // Inbox phone number
|
||||
contactName: data.contact_name || 'Unknown Caller', // Add fallback name
|
||||
contactId: data.contact_id,
|
||||
accountId: data.account_id,
|
||||
isOutbound: data.is_outbound || false, // Check if this is an outbound call requiring agent join
|
||||
conference_sid: data.conference_sid, // Pass the conference_sid directly to the floating widget
|
||||
requiresAgentJoin: data.requires_agent_join || false, // Flag for calls needing immediate agent join
|
||||
callDirection: data.call_direction // Add call direction for additional context
|
||||
callDirection: data.call_direction, // Add call direction for additional context
|
||||
phoneNumber: data.phone_number, // Include phone number for display in the UI
|
||||
avatarUrl: data.avatar_url // Include avatar URL for display in the UI
|
||||
};
|
||||
|
||||
// Process outbound calls
|
||||
|
||||
// Update store
|
||||
this.app.$store.dispatch('calls/setIncomingCall', normalizedPayload);
|
||||
|
||||
@@ -221,8 +223,6 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
if (window.app && window.app.$data) {
|
||||
window.app.$data.showCallWidget = true;
|
||||
}
|
||||
|
||||
// For outbound calls, we don't need to play a ringtone as we're initiating the call
|
||||
};
|
||||
|
||||
onCallStatusChanged = data => {
|
||||
@@ -240,20 +240,16 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
|
||||
// For terminal statuses, clear the active call to close the widget
|
||||
if (['ended', 'missed', 'completed', 'failed', 'busy', 'no_answer'].includes(data.status)) {
|
||||
console.log(`ActionCable: Call status changed to terminal status: ${data.status}`);
|
||||
|
||||
// Clear active call for terminal statuses
|
||||
this.app.$store.dispatch('calls/clearActiveCall');
|
||||
|
||||
// Ensure window.app.$data exists before modifying it
|
||||
if (window.app && window.app.$data) {
|
||||
console.log('ActionCable: Hiding call widget');
|
||||
window.app.$data.showCallWidget = false;
|
||||
}
|
||||
|
||||
// Update conversation list to show current status
|
||||
if (data.conversation_id) {
|
||||
console.log(`ActionCable: Updating conversation last activity for conversation ${data.conversation_id}`);
|
||||
this.app.$store.dispatch('updateConversationLastActivity', {
|
||||
conversationId: data.conversation_id,
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
|
||||
@@ -63,7 +63,15 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
storeActiveCall: 'calls/getActiveCall',
|
||||
storeHasActiveCall: 'calls/hasActiveCall',
|
||||
}),
|
||||
// Check if there's an active call either in store or local component
|
||||
hasActiveCall() {
|
||||
return this.storeHasActiveCall || !!this.activeCallConversation;
|
||||
},
|
||||
contactProfileLink() {
|
||||
return `/app/accounts/${this.$route.params.accountId}/contacts/${this.contact.id}`;
|
||||
},
|
||||
@@ -242,9 +250,15 @@ export default {
|
||||
},
|
||||
|
||||
handleCallEnded() {
|
||||
// Immediately reset local state
|
||||
this.activeCallConversation = null;
|
||||
this.isCallLoading = false;
|
||||
|
||||
// Clear global call state
|
||||
this.$store.dispatch('calls/clearActiveCall');
|
||||
|
||||
// Force re-render the component to ensure button state updates
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
// Simplified emergency end call function
|
||||
@@ -446,12 +460,29 @@ export default {
|
||||
this.showMergeModal = true;
|
||||
},
|
||||
onCallButtonClick() {
|
||||
if (this.activeCallConversation) {
|
||||
useAlert('Call already ongoing', 'warning');
|
||||
// If there's an active call in the store, just show widget
|
||||
if (this.storeHasActiveCall) {
|
||||
useAlert('Call already active, showing call widget', 'info');
|
||||
if (window.app && window.app.$data) {
|
||||
window.app.$data.showCallWidget = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have a stale local state
|
||||
if (this.activeCallConversation) {
|
||||
// Reset our local state
|
||||
this.activeCallConversation = null;
|
||||
this.$forceUpdate();
|
||||
|
||||
// If store is clear, initiate a new call after state reset
|
||||
if (!this.storeHasActiveCall) {
|
||||
this.$nextTick(() => {
|
||||
this.initiateVoiceCall();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No active call, proceed with initiating a new one
|
||||
this.initiateVoiceCall();
|
||||
}
|
||||
},
|
||||
@@ -570,15 +601,13 @@ export default {
|
||||
</ComposeConversation>
|
||||
<NextButton
|
||||
v-if="contact.phone_number"
|
||||
v-tooltip.top-end="
|
||||
activeCallConversation ? 'Call already ongoing' : 'Call'
|
||||
"
|
||||
v-tooltip.top-end="hasActiveCall ? 'Call already ongoing' : 'Call'"
|
||||
icon="i-ph-phone"
|
||||
slate
|
||||
faded
|
||||
sm
|
||||
:is-loading="!activeCallConversation && isCallLoading"
|
||||
:color="activeCallConversation ? 'teal' : undefined"
|
||||
:is-loading="!hasActiveCall && isCallLoading"
|
||||
:color="hasActiveCall ? 'teal' : undefined"
|
||||
@click.stop.prevent="onCallButtonClick"
|
||||
/>
|
||||
<NextButton
|
||||
|
||||
@@ -33,8 +33,6 @@ module Voice
|
||||
}.freeze
|
||||
|
||||
def process
|
||||
# Log incoming parameters
|
||||
Rails.logger.info("🎤 CONFERENCE STATUS: #{params['StatusCallbackEvent']}")
|
||||
|
||||
# Extract status info and find conversation
|
||||
info = status_info
|
||||
@@ -46,7 +44,6 @@ module Voice
|
||||
update_participant_info(conversation, info)
|
||||
|
||||
# Process the event with the conference manager
|
||||
Rails.logger.info("📊 PROCESSING EVENT: #{info[:event]}")
|
||||
|
||||
Voice::ConferenceManagerService.new(
|
||||
conversation: conversation,
|
||||
@@ -96,7 +93,6 @@ module Voice
|
||||
return conversation if conversation
|
||||
end
|
||||
|
||||
Rails.logger.error("❌ CONVERSATION NOT FOUND for event: #{info[:event]}")
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -164,22 +160,35 @@ module Voice
|
||||
|
||||
def broadcast_agent_notification(conversation, info)
|
||||
contact = conversation.contact
|
||||
inbox = conversation.inbox
|
||||
|
||||
# Get contact name, ensuring we have a valid value
|
||||
contact_name_value = contact&.name.presence || contact&.phone_number || 'Outbound Call'
|
||||
|
||||
# Create the data payload
|
||||
broadcast_data = {
|
||||
call_sid: info[:call_sid],
|
||||
conversation_id: conversation.id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
inbox_name: conversation.inbox.name,
|
||||
inbox_avatar_url: inbox.avatar_url, # Include inbox avatar
|
||||
inbox_phone_number: inbox.channel.phone_number, # Include inbox phone number
|
||||
contact_name: contact_name_value,
|
||||
contact_id: contact&.id,
|
||||
is_outbound: true,
|
||||
account_id: account.id,
|
||||
conference_sid: info[:conference_sid],
|
||||
phone_number: contact&.phone_number, # Include phone number for display in UI
|
||||
avatar_url: contact&.avatar_url, # Include avatar URL for display in UI
|
||||
call_direction: 'outbound' # Add call direction for context
|
||||
}
|
||||
|
||||
|
||||
ActionCable.server.broadcast(
|
||||
"account_#{account.id}",
|
||||
{
|
||||
event: 'incoming_call',
|
||||
data: {
|
||||
call_sid: info[:call_sid],
|
||||
conversation_id: conversation.id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
inbox_name: conversation.inbox.name,
|
||||
contact_name: contact&.name || 'Outbound Call',
|
||||
contact_id: contact&.id,
|
||||
is_outbound: true,
|
||||
account_id: account.id,
|
||||
conference_sid: info[:conference_sid]
|
||||
}
|
||||
data: broadcast_data
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -3,8 +3,6 @@ module Voice
|
||||
pattr_initialize [:account!, :params!]
|
||||
|
||||
def process
|
||||
Rails.logger.info("🔍 INCOMING CALL: Starting processing for call_sid=#{caller_info[:call_sid]}")
|
||||
Rails.logger.info("📞 CALL DETAILS: From=#{caller_info[:from_number]} To=#{caller_info[:to_number]}")
|
||||
|
||||
begin
|
||||
find_inbox
|
||||
@@ -22,11 +20,8 @@ module Voice
|
||||
|
||||
twiml = generate_twiml_response
|
||||
|
||||
Rails.logger.info("✅ INCOMING CALL: Successfully processed for call_sid=#{caller_info[:call_sid]}")
|
||||
twiml
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("❌ INCOMING CALL ERROR: #{e.message}")
|
||||
Rails.logger.error("❌ INCOMING CALL BACKTRACE: #{e.backtrace[0..5].join("\n")}")
|
||||
|
||||
# Return a simple error TwiML
|
||||
error_twiml(e.message)
|
||||
@@ -53,7 +48,6 @@ module Voice
|
||||
|
||||
raise "Inbox not found for phone number #{caller_info[:to_number]}" unless @inbox.present?
|
||||
|
||||
Rails.logger.info("📥 FOUND INBOX: inbox_id=#{@inbox.id} for phone=#{caller_info[:to_number]}")
|
||||
end
|
||||
|
||||
def create_contact
|
||||
@@ -65,7 +59,6 @@ module Voice
|
||||
c.name = "Contact from #{phone_number}"
|
||||
end
|
||||
|
||||
Rails.logger.info("👤 CONTACT: contact_id=#{@contact.id} name=#{@contact.name} phone=#{@contact.phone_number}")
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
@@ -79,7 +72,6 @@ module Voice
|
||||
@contact_inbox.source_id ||= caller_info[:from_number]
|
||||
@contact_inbox.save!
|
||||
|
||||
Rails.logger.info("📬 CONTACT INBOX: id=#{@contact_inbox.id} source_id=#{@contact_inbox.source_id}")
|
||||
|
||||
# Create a new conversation with basic call details
|
||||
# Status will be properly set by CallStatusManager later
|
||||
@@ -101,7 +93,6 @@ module Voice
|
||||
@conversation.additional_attributes['conference_sid'] = conference_name
|
||||
@conversation.save!
|
||||
|
||||
Rails.logger.info("💬 CONVERSATION: id=#{@conversation.id} display_id=#{@conversation.display_id} conference=#{conference_name}")
|
||||
end
|
||||
|
||||
def create_voice_call_message
|
||||
@@ -144,7 +135,6 @@ module Voice
|
||||
message_params
|
||||
).perform
|
||||
|
||||
Rails.logger.info("✉️ VOICE CALL MESSAGE: id=#{@voice_call_message.id} content_type=#{@voice_call_message.content_type}")
|
||||
|
||||
# Broadcast call notification
|
||||
broadcast_call_status
|
||||
@@ -167,27 +157,37 @@ module Voice
|
||||
"Incoming call from #{@contact.name.presence || caller_info[:from_number]}"
|
||||
)
|
||||
|
||||
Rails.logger.info("📝 ACTIVITY MESSAGE: id=#{activity_message.id}")
|
||||
end
|
||||
|
||||
def broadcast_call_status
|
||||
# Get contact name, ensuring we have a valid value
|
||||
contact_name_value = @contact.name.presence || caller_info[:from_number]
|
||||
|
||||
# Create the data payload
|
||||
broadcast_data = {
|
||||
call_sid: caller_info[:call_sid],
|
||||
conversation_id: @conversation.id,
|
||||
inbox_id: @inbox.id,
|
||||
inbox_name: @inbox.name,
|
||||
inbox_avatar_url: @inbox.avatar_url, # Include inbox avatar
|
||||
inbox_phone_number: @inbox.channel.phone_number, # Include inbox phone number
|
||||
contact_name: contact_name_value,
|
||||
contact_id: @contact.id,
|
||||
account_id: account.id,
|
||||
phone_number: @contact.phone_number, # Include phone number for display in UI
|
||||
avatar_url: @contact.avatar_url, # Include avatar URL for display in UI
|
||||
call_direction: 'inbound' # Add call direction for context
|
||||
}
|
||||
|
||||
|
||||
ActionCable.server.broadcast(
|
||||
"account_#{account.id}",
|
||||
{
|
||||
event: 'incoming_call',
|
||||
data: {
|
||||
call_sid: caller_info[:call_sid],
|
||||
conversation_id: @conversation.id,
|
||||
inbox_id: @inbox.id,
|
||||
inbox_name: @inbox.name,
|
||||
contact_name: @contact.name || caller_info[:from_number],
|
||||
contact_id: @contact.id,
|
||||
account_id: account.id
|
||||
}
|
||||
data: broadcast_data
|
||||
}
|
||||
)
|
||||
|
||||
Rails.logger.info('📢 BROADCAST: Sent incoming_call notification')
|
||||
end
|
||||
|
||||
def generate_twiml_response
|
||||
@@ -197,7 +197,6 @@ module Voice
|
||||
response.say(message: 'Thank you for calling. Please wait while we connect you with an agent.')
|
||||
|
||||
callback_url = "#{base_url}/api/v1/accounts/#{account.id}/channels/voice/webhooks/conference_status"
|
||||
Rails.logger.info("🔗 CONFERENCE CALLBACK URL: #{callback_url}")
|
||||
|
||||
response.dial do |dial|
|
||||
dial.conference(
|
||||
@@ -214,7 +213,6 @@ module Voice
|
||||
)
|
||||
end
|
||||
|
||||
Rails.logger.info("📞 TWIML: Generated conference TwiML for #{conference_name}")
|
||||
response.to_s
|
||||
end
|
||||
|
||||
@@ -223,13 +221,11 @@ module Voice
|
||||
response.say(message: 'We are experiencing technical difficulties with our phone system. Please try again later.')
|
||||
response.hangup
|
||||
|
||||
Rails.logger.info("❌ ERROR TWIML: Generated error TwiML due to: #{message}")
|
||||
response.to_s
|
||||
end
|
||||
|
||||
def base_url
|
||||
url = ENV.fetch('FRONTEND_URL', "https://#{params['host_with_port']}")
|
||||
Rails.logger.info("🌐 BASE URL: Using #{url}")
|
||||
url.gsub(%r{/$}, '') # Remove trailing slash if present
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,6 +26,7 @@ module Voice
|
||||
@voice_inbox = account.inboxes.find_by(channel_type: 'Channel::Voice')
|
||||
raise 'No Voice channel found' if @voice_inbox.blank?
|
||||
raise 'Contact has no phone number' if contact.phone_number.blank?
|
||||
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
@@ -122,24 +123,34 @@ module Voice
|
||||
end
|
||||
|
||||
def broadcast_to_agent
|
||||
# Get contact name, ensuring we have a valid value
|
||||
contact_name_value = contact.name.presence || contact.phone_number
|
||||
|
||||
# Create the data payload
|
||||
broadcast_data = {
|
||||
call_sid: @call_details[:call_sid],
|
||||
conversation_id: @conversation.id,
|
||||
inbox_id: @voice_inbox.id,
|
||||
inbox_name: @voice_inbox.name,
|
||||
inbox_avatar_url: @voice_inbox.avatar_url, # Include inbox avatar
|
||||
inbox_phone_number: @voice_inbox.channel.phone_number, # Include inbox phone number
|
||||
contact_name: contact_name_value,
|
||||
contact_id: contact.id,
|
||||
account_id: account.id,
|
||||
is_outbound: true,
|
||||
conference_sid: @conference_name,
|
||||
requires_agent_join: true,
|
||||
call_direction: 'outbound',
|
||||
phone_number: contact.phone_number, # Include phone number for display in the UI
|
||||
avatar_url: contact.avatar_url # Include avatar URL for display in the UI
|
||||
}
|
||||
|
||||
# Direct notification that agent needs to join
|
||||
ActionCable.server.broadcast(
|
||||
"account_#{account.id}",
|
||||
{
|
||||
event: 'incoming_call',
|
||||
data: {
|
||||
call_sid: @call_details[:call_sid],
|
||||
conversation_id: @conversation.id,
|
||||
inbox_id: @voice_inbox.id,
|
||||
inbox_name: @voice_inbox.name,
|
||||
contact_name: contact.name || contact.phone_number,
|
||||
contact_id: contact.id,
|
||||
account_id: account.id,
|
||||
is_outbound: true,
|
||||
conference_sid: @conference_name,
|
||||
requires_agent_join: true,
|
||||
call_direction: 'outbound'
|
||||
}
|
||||
data: broadcast_data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user