From 353089473e06f9103003719278548210bb5fd440 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 30 Apr 2026 18:38:10 +0400 Subject: [PATCH 1/2] feat(voice): Assignment aware visibility and join conflict for inbound calls (#14333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Inbound voice calls now route ownership cleanly: the call widget is hidden from agents who aren't the conversation assignee, the first agent to pick up becomes the assignee, and any later join attempt by another agent is rejected with a clear " is already handling the call." alert. Closes https://linear.app/chatwoot/issue/PLA-98/inbound-voice-calls-assignment-aware-visibility-auto-assignment-on ### How to test 1. As Agent A and Agent B, open the dashboard for the same voice inbox in two browsers. 2. Place an inbound call to the inbox with the conversation **unassigned** — both agents should see the call widget. 3. Have Agent A click **Join**. Agent A's widget transitions to the active call; Agent B's widget disappears (conversation is now assigned to Agent A). 4. While the call is in progress, attempt to join from a third agent (e.g., via the bubble in the conversation timeline) — the join is rejected with the toast `Agent A is already handling the call.` 5. Resolve the conversation, then place a second call to a conversation that is already manually assigned to Agent A — only Agent A sees the widget; nobody else does. 6. Race test: trigger two near-simultaneous join attempts (two agents click Join within a few hundred ms of each other) — exactly one wins; the other gets the conflict alert. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> --- .../message/bubbles/VoiceCall.vue | 64 +++++++++++++---- .../dashboard/composables/useCallSession.js | 8 +++ app/javascript/dashboard/helper/voice.js | 71 ++++++++++++++++--- .../i18n/locale/en/conversation.json | 3 +- .../store/modules/conversations/actions.js | 10 +-- app/javascript/dashboard/stores/calls.js | 14 ++++ config/locales/en.yml | 2 + .../api/v1/accounts/conference_controller.rb | 5 ++ enterprise/app/models/call.rb | 1 + .../app/services/voice/conference/manager.rb | 24 ++++++- .../provider/twilio/conference_service.rb | 29 +++++++- .../call_already_accepted.rb | 11 +++ 12 files changed, 212 insertions(+), 30 deletions(-) create mode 100644 lib/custom_exceptions/call_already_accepted.rb diff --git a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue index a270882ac..f2383551c 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue @@ -1,5 +1,7 @@ - - diff --git a/app/javascript/dashboard/components-next/message/bubbles/Template/ListPicker.vue b/app/javascript/dashboard/components-next/message/bubbles/Template/ListPicker.vue deleted file mode 100644 index 7d9401b80..000000000 --- a/app/javascript/dashboard/components-next/message/bubbles/Template/ListPicker.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/bubbles/Template/Media.vue b/app/javascript/dashboard/components-next/message/bubbles/Template/Media.vue deleted file mode 100644 index 24340ab90..000000000 --- a/app/javascript/dashboard/components-next/message/bubbles/Template/Media.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/bubbles/Template/QuickReply.vue b/app/javascript/dashboard/components-next/message/bubbles/Template/QuickReply.vue deleted file mode 100644 index 19727578f..000000000 --- a/app/javascript/dashboard/components-next/message/bubbles/Template/QuickReply.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/stories/Template/CallToAction.story.vue b/app/javascript/dashboard/components-next/message/stories/Template/CallToAction.story.vue deleted file mode 100644 index 1c2b956a3..000000000 --- a/app/javascript/dashboard/components-next/message/stories/Template/CallToAction.story.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/stories/Template/Card.story.vue b/app/javascript/dashboard/components-next/message/stories/Template/Card.story.vue deleted file mode 100644 index caa594bab..000000000 --- a/app/javascript/dashboard/components-next/message/stories/Template/Card.story.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/stories/Template/ListPicker.story.vue b/app/javascript/dashboard/components-next/message/stories/Template/ListPicker.story.vue deleted file mode 100644 index f1e9dd8b8..000000000 --- a/app/javascript/dashboard/components-next/message/stories/Template/ListPicker.story.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/stories/Template/Media.story.vue b/app/javascript/dashboard/components-next/message/stories/Template/Media.story.vue deleted file mode 100644 index 67d7fc6b8..000000000 --- a/app/javascript/dashboard/components-next/message/stories/Template/Media.story.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/stories/Template/QuickReply.story.vue b/app/javascript/dashboard/components-next/message/stories/Template/QuickReply.story.vue deleted file mode 100644 index 1d670e07c..000000000 --- a/app/javascript/dashboard/components-next/message/stories/Template/QuickReply.story.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/stories/Template/Text.story.vue b/app/javascript/dashboard/components-next/message/stories/Template/Text.story.vue deleted file mode 100644 index cbb6df569..000000000 --- a/app/javascript/dashboard/components-next/message/stories/Template/Text.story.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/app/javascript/dashboard/components-next/message/bubbles/Template/CSAT.vue b/app/javascript/dashboard/components-next/template-preview/CSATTemplate.vue similarity index 100% rename from app/javascript/dashboard/components-next/message/bubbles/Template/CSAT.vue rename to app/javascript/dashboard/components-next/template-preview/CSATTemplate.vue diff --git a/app/javascript/dashboard/components-next/template-preview/CallToActionTemplate.vue b/app/javascript/dashboard/components-next/template-preview/CallToActionTemplate.vue new file mode 100644 index 000000000..b4d61d252 --- /dev/null +++ b/app/javascript/dashboard/components-next/template-preview/CallToActionTemplate.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/javascript/dashboard/components-next/message/bubbles/Template/Card.vue b/app/javascript/dashboard/components-next/template-preview/CardTemplate.vue similarity index 60% rename from app/javascript/dashboard/components-next/message/bubbles/Template/Card.vue rename to app/javascript/dashboard/components-next/template-preview/CardTemplate.vue index 9a4f9f19a..e862bc008 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/Template/Card.vue +++ b/app/javascript/dashboard/components-next/template-preview/CardTemplate.vue @@ -10,22 +10,22 @@ defineProps({