Agents who refresh or dismiss the floating widget can now click a Join / Rejoin action on the voice_call bubble to (re)connect to a ringing or in-progress call. The CTA respects the same visibility rules as the floating widget — outbound calls only surface for the initiator, and inbound assigned calls only surface for the assignee. useCallSession gains a manageSessionState option so the bubble can reuse joinCall/activeCall without registering duplicate Twilio listeners or running its own duration timer. Closes PLA-117
155 lines
3.8 KiB
JavaScript
155 lines
3.8 KiB
JavaScript
import { CONTENT_TYPES } from 'dashboard/components-next/message/constants';
|
|
import { useCallsStore } from 'dashboard/stores/calls';
|
|
import types from 'dashboard/store/mutation-types';
|
|
|
|
export const TERMINAL_STATUSES = [
|
|
'completed',
|
|
'busy',
|
|
'failed',
|
|
'no-answer',
|
|
'canceled',
|
|
'missed',
|
|
'ended',
|
|
];
|
|
|
|
export const isInbound = direction => direction === 'inbound';
|
|
|
|
const isVoiceCallMessage = message => {
|
|
return CONTENT_TYPES.VOICE_CALL === message?.content_type;
|
|
};
|
|
|
|
const shouldSkipCall = (callDirection, senderId, currentUserId) => {
|
|
return callDirection === 'outbound' && senderId !== currentUserId;
|
|
};
|
|
|
|
const extractAssigneeId = conversation => {
|
|
return conversation?.assignee_id || conversation?.meta?.assignee?.id || null;
|
|
};
|
|
|
|
const isAssignedToAnotherAgent = (assigneeId, currentUserId) => {
|
|
if (currentUserId == null) return false;
|
|
return !!assigneeId && assigneeId !== currentUserId;
|
|
};
|
|
|
|
const shouldShowCall = ({
|
|
callDirection,
|
|
senderId,
|
|
assigneeId,
|
|
currentUserId,
|
|
}) => {
|
|
if (shouldSkipCall(callDirection, senderId, currentUserId)) return false;
|
|
// Outbound calls are scoped to the initiator via shouldSkipCall; the
|
|
// conversation may be auto-assigned to a different agent on creation, so
|
|
// skip the assignee filter for outbound to avoid hiding the caller's own widget.
|
|
if (callDirection === 'outbound') return true;
|
|
return !isAssignedToAnotherAgent(assigneeId, currentUserId);
|
|
};
|
|
|
|
// Whether the current user is allowed to take over / join an existing call.
|
|
// Mirrors the floating-widget visibility rules so the bubble's join action
|
|
// stays in sync.
|
|
export const canCurrentUserJoinCall = ({
|
|
call,
|
|
conversation,
|
|
senderId,
|
|
currentUserId,
|
|
}) => {
|
|
if (!call) return false;
|
|
return shouldShowCall({
|
|
callDirection: call.direction === 'outgoing' ? 'outbound' : 'inbound',
|
|
senderId,
|
|
assigneeId: extractAssigneeId(conversation),
|
|
currentUserId,
|
|
});
|
|
};
|
|
|
|
function extractCallData(message) {
|
|
const call = message?.call || {};
|
|
return {
|
|
callSid: call.provider_call_id,
|
|
status: call.status,
|
|
callDirection: call.direction === 'outgoing' ? 'outbound' : 'inbound',
|
|
conversationId: message?.conversation_id,
|
|
assigneeId: extractAssigneeId(message?.conversation),
|
|
senderId: message?.sender?.id,
|
|
};
|
|
}
|
|
|
|
export function handleVoiceCallCreated(message, currentUserId) {
|
|
if (!isVoiceCallMessage(message)) return;
|
|
|
|
const { callSid, callDirection, conversationId, assigneeId, senderId } =
|
|
extractCallData(message);
|
|
|
|
if (
|
|
!shouldShowCall({
|
|
callDirection,
|
|
senderId,
|
|
assigneeId,
|
|
currentUserId,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const callsStore = useCallsStore();
|
|
callsStore.addCall({
|
|
callSid,
|
|
conversationId,
|
|
callDirection,
|
|
senderId,
|
|
});
|
|
}
|
|
|
|
export function handleVoiceCallUpdated(commit, message, currentUserId) {
|
|
if (!isVoiceCallMessage(message)) return;
|
|
|
|
const {
|
|
callSid,
|
|
status,
|
|
callDirection,
|
|
conversationId,
|
|
assigneeId,
|
|
senderId,
|
|
} = extractCallData(message);
|
|
|
|
const callsStore = useCallsStore();
|
|
|
|
callsStore.handleCallStatusChanged({ callSid, status, conversationId });
|
|
|
|
commit(types.UPDATE_MESSAGE_CALL_STATUS, {
|
|
conversationId,
|
|
callStatus: status,
|
|
callSid,
|
|
});
|
|
|
|
if (
|
|
!shouldShowCall({
|
|
callDirection,
|
|
senderId,
|
|
assigneeId,
|
|
currentUserId,
|
|
})
|
|
) {
|
|
callsStore.removeCall(callSid);
|
|
return;
|
|
}
|
|
|
|
if (status === 'ringing') {
|
|
callsStore.addCall({
|
|
callSid,
|
|
conversationId,
|
|
callDirection,
|
|
senderId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function syncConversationCallVisibility(conversation, currentUserId) {
|
|
const assigneeId = extractAssigneeId(conversation);
|
|
if (!isAssignedToAnotherAgent(assigneeId, currentUserId)) return;
|
|
|
|
const callsStore = useCallsStore();
|
|
callsStore.removeCallsForConversation(conversation.id);
|
|
}
|