From bec795f764d805d2a49d4a27553ad819aabc84f3 Mon Sep 17 00:00:00 2001 From: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:43:22 +0530 Subject: [PATCH 1/5] Bump version to 4.14.2 --- VERSION_CW | 2 +- config/app.yml | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION_CW b/VERSION_CW index d2b9909a9..0fb7a35b6 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -4.14.1 +4.14.2 diff --git a/config/app.yml b/config/app.yml index fec34cd07..c2494befa 100644 --- a/config/app.yml +++ b/config/app.yml @@ -1,5 +1,5 @@ shared: &shared - version: '4.14.1' + version: '4.14.2' development: <<: *shared diff --git a/package.json b/package.json index 41313c8b3..bc51bbd85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/chatwoot", - "version": "4.14.1", + "version": "4.14.2", "license": "MIT", "scripts": { "eslint": "eslint app/**/*.{js,vue}", From c6e5657ee52192112c9913d1de797bcd86b5be8a Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:16:41 +0530 Subject: [PATCH 2/5] fix(call): emit assignee activity message when agent answers a call (#14700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Linear Ticket - https://linear.app/chatwoot/issue/CW-7249/debug-assignment-log ## Description When an agent answers an inbound WhatsApp call on an unassigned conversation, the conversation is claimed for that agent — but no "assigned" activity message or assignee-changed event was emitted, so the assignment was invisible in the timeline (and notifications/live assignee panel didn't update). The claim ran before `update_conversation_call_status`, so that later `update!` on the same conversation clobbered `saved_change_to_assignee_id?` before `after_commit` fired. Moving the claim to be the conversation's final write in the transaction restores the activity message, the `ASSIGNEE_CHANGED` event, and the live assignee update. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? 1. Open an unassigned WhatsApp-call conversation. 2. As an agent, answer an inbound call. 3. Before: the conversation is assigned to you, but no "self-assigned" activity message appears. After: the activity message is created and the assignee updates live. ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- enterprise/app/services/whatsapp/call_service.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/enterprise/app/services/whatsapp/call_service.rb b/enterprise/app/services/whatsapp/call_service.rb index 93eba957c..743409839 100644 --- a/enterprise/app/services/whatsapp/call_service.rb +++ b/enterprise/app/services/whatsapp/call_service.rb @@ -9,7 +9,7 @@ class Whatsapp::CallService call.with_lock do transition_to_in_progress! update_message_status('in_progress') - update_conversation_call_status(call.display_status) + claim_conversation_and_set_call_status broadcast(:accepted, accepted_by_agent_id: agent.id) end call @@ -56,7 +56,6 @@ class Whatsapp::CallService forward_answer_to_meta! call.update!(status: 'in_progress', accepted_by_agent_id: agent.id, started_at: Time.current, meta: (call.meta || {}).merge('sdp_answer' => sdp_answer)) - claim_conversation_for_agent end def forward_answer_to_meta! @@ -64,9 +63,13 @@ class Whatsapp::CallService invoke_provider!(:accept_call, sdp_answer) end - # Take ownership of the conversation if no one holds it; leave assignee alone otherwise (transfer via UI). - def claim_conversation_for_agent - call.conversation.update!(assignee: agent) if call.conversation.assignee_id.blank? + # Claim an unheld conversation and set call_status in one save so previous_changes carries both the + # assignee change (activity message + ASSIGNEE_CHANGED) and the call_status change (conversation.updated webhook). + def claim_conversation_and_set_call_status + conversation = call.conversation + attrs = { additional_attributes: (conversation.additional_attributes || {}).merge('call_status' => call.display_status) } + attrs[:assignee] = agent if conversation.assignee_id.blank? + conversation.update!(attrs) end # Raise on Meta failure (bool false or transport error) so callers bail before From 4d3196b02fd7b1046ff22a71ea1d1059f69e3193 Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Thu, 11 Jun 2026 13:05:52 +0530 Subject: [PATCH 3/5] feat: Show unread count for all conversations (#14627) ## Description Adds the unread count for All Conversations to the left sidebar. The unread counts API now returns an `all_count` aggregate based on the same permission-scoped inbox counts already used for sidebar badges, and the sidebar renders that backend-provided value on the All Conversations item. Fixes [CW-7240](https://linear.app/chatwoot/issue/CW-7240/unread-count-for-all-conversations) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? - `bundle exec rspec spec/services/conversations/unread_counts/counter_spec.rb spec/enterprise/services/conversations/unread_counts/counter_spec.rb spec/controllers/api/v1/accounts/conversations_controller_spec.rb` - `pnpm test app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js` - `pnpm exec eslint app/javascript/dashboard/components-next/sidebar/Sidebar.vue app/javascript/dashboard/store/modules/conversationUnreadCounts.js app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js` - `bundle exec rubocop app/services/conversations/unread_counts/counter.rb spec/services/conversations/unread_counts/counter_spec.rb spec/enterprise/services/conversations/unread_counts/counter_spec.rb spec/controllers/api/v1/accounts/conversations_controller_spec.rb` ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Co-authored-by: Muhsin Keloth --- .../components-next/sidebar/Sidebar.vue | 4 ++++ .../store/modules/conversationUnreadCounts.js | 14 ++++++++++++-- .../conversationUnreadCounts/actions.spec.js | 1 + .../conversationUnreadCounts/getters.spec.js | 15 +++++++++++++++ .../conversationUnreadCounts/mutations.spec.js | 16 +++++++++++++++- .../conversations/unread_counts/counter.rb | 7 +++++-- .../v1/accounts/conversations_controller_spec.rb | 1 + .../conversations/unread_counts/counter_spec.rb | 5 ++++- .../conversations/unread_counts/counter_spec.rb | 3 +++ 9 files changed, 60 insertions(+), 6 deletions(-) diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index d9f523dfc..f71652ca0 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -175,6 +175,9 @@ useEventListener(document, 'touchend', onResizeEnd); const inboxes = useMapGetter('inboxes/getInboxes'); const labels = useMapGetter('labels/getLabelsOnSidebar'); +const allUnreadCount = useMapGetter( + 'conversationUnreadCounts/getAllUnreadCount' +); const getInboxUnreadCount = useMapGetter( 'conversationUnreadCounts/getInboxUnreadCount' ); @@ -297,6 +300,7 @@ const menuItems = computed(() => { { name: 'All', label: t('SIDEBAR.ALL_CONVERSATIONS'), + badgeCount: allUnreadCount.value, activeOn: ['inbox_conversation'], to: accountScopedRoute('home'), }, diff --git a/app/javascript/dashboard/store/modules/conversationUnreadCounts.js b/app/javascript/dashboard/store/modules/conversationUnreadCounts.js index 0503c0806..c03249311 100644 --- a/app/javascript/dashboard/store/modules/conversationUnreadCounts.js +++ b/app/javascript/dashboard/store/modules/conversationUnreadCounts.js @@ -2,15 +2,21 @@ import ConversationAPI from '../../api/conversations'; import types from '../mutation-types'; export const state = { + allCount: 0, inboxes: {}, labels: {}, teams: {}, }; +const normalizeCount = count => { + const parsedCount = Number(count); + return Number.isFinite(parsedCount) && parsedCount > 0 ? parsedCount : 0; +}; + const normalizeCounts = counts => { return Object.entries(counts || {}).reduce((result, [id, count]) => { - const parsedCount = Number(count); - if (Number.isFinite(parsedCount) && parsedCount > 0) { + const parsedCount = normalizeCount(count); + if (parsedCount > 0) { result[String(id)] = parsedCount; } @@ -19,6 +25,9 @@ const normalizeCounts = counts => { }; export const getters = { + getAllUnreadCount($state) { + return $state.allCount; + }, getInboxUnreadCount: $state => inboxId => { return $state.inboxes[String(inboxId)] || 0; }, @@ -55,6 +64,7 @@ export const actions = { export const mutations = { [types.SET_CONVERSATION_UNREAD_COUNTS]($state, payload = {}) { + $state.allCount = normalizeCount(payload.all_count); $state.inboxes = normalizeCounts(payload.inboxes); $state.labels = normalizeCounts(payload.labels); $state.teams = normalizeCounts(payload.teams); diff --git a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js index 3100cdd10..29fadc897 100644 --- a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js @@ -15,6 +15,7 @@ describe('#actions', () => { describe('#get', () => { it('commits unread counts when API is successful', async () => { const payload = { + all_count: 2, inboxes: { 1: '2' }, labels: { 3: 4 }, teams: { 5: 6 }, diff --git a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js index a3e74fc37..9fe19e22d 100644 --- a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js @@ -3,6 +3,7 @@ import { getters } from '../../conversationUnreadCounts'; describe('#getters', () => { it('returns inbox unread count by id', () => { const state = { + allCount: 0, inboxes: { 1: 2 }, labels: {}, teams: {}, @@ -15,6 +16,7 @@ describe('#getters', () => { it('returns label unread count by id', () => { const state = { + allCount: 0, inboxes: {}, labels: { 3: 4 }, teams: {}, @@ -27,6 +29,7 @@ describe('#getters', () => { it('returns team unread count by id', () => { const state = { + allCount: 0, inboxes: {}, labels: {}, teams: { 5: 6 }, @@ -37,8 +40,20 @@ describe('#getters', () => { expect(getters.getTeamUnreadCount(state)(6)).toBe(0); }); + it('returns all unread count', () => { + const state = { + allCount: 7, + inboxes: {}, + labels: {}, + teams: {}, + }; + + expect(getters.getAllUnreadCount(state)).toBe(7); + }); + it('returns unread count maps', () => { const state = { + allCount: 0, inboxes: { 1: 2 }, labels: { 3: 4 }, teams: { 5: 6 }, diff --git a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js index 3f7e2b1ec..8941d7430 100644 --- a/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js @@ -4,9 +4,10 @@ import { mutations } from '../../conversationUnreadCounts'; describe('#mutations', () => { describe('#SET_CONVERSATION_UNREAD_COUNTS', () => { it('normalizes unread count payload', () => { - const state = { inboxes: {}, labels: {}, teams: {} }; + const state = { allCount: 0, inboxes: {}, labels: {}, teams: {} }; mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, { + all_count: '3', inboxes: { 1: '2', 2: 0, @@ -23,6 +24,7 @@ describe('#mutations', () => { }); expect(state).toEqual({ + allCount: 3, inboxes: { 1: 2 }, labels: { 4: 5 }, teams: { 6: 7 }, @@ -31,6 +33,7 @@ describe('#mutations', () => { it('clears counts when payload is empty', () => { const state = { + allCount: 2, inboxes: { 1: 2 }, labels: { 4: 5 }, teams: { 6: 7 }, @@ -39,10 +42,21 @@ describe('#mutations', () => { mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, {}); expect(state).toEqual({ + allCount: 0, inboxes: {}, labels: {}, teams: {}, }); }); + + it('normalizes invalid aggregate counts to zero', () => { + const state = { allCount: 2, inboxes: {}, labels: {}, teams: {} }; + + mutations[types.SET_CONVERSATION_UNREAD_COUNTS](state, { + all_count: 'invalid', + }); + + expect(state.allCount).toBe(0); + }); }); }); diff --git a/app/services/conversations/unread_counts/counter.rb b/app/services/conversations/unread_counts/counter.rb index f4126b61f..b1ba5ddb0 100644 --- a/app/services/conversations/unread_counts/counter.rb +++ b/app/services/conversations/unread_counts/counter.rb @@ -19,8 +19,11 @@ class Conversations::UnreadCounts::Counter ensure_base_cache! ensure_assignment_cache! if assignment_mode? + inbox_counts = unread_inbox_counts + { - inboxes: unread_inbox_counts, + all_count: inbox_counts.values.sum, + inboxes: inbox_counts, labels: unread_label_counts, teams: unread_team_counts } @@ -191,7 +194,7 @@ class Conversations::UnreadCounts::Counter end def empty_counts - { inboxes: {}, labels: {}, teams: {} } + { all_count: 0, inboxes: {}, labels: {}, teams: {} } end def store diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index f8fd446d2..bdb8117ac 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -141,6 +141,7 @@ RSpec.describe 'Conversations API', type: :request do expect(response).to have_http_status(:success) expect(response.parsed_body['payload']).to eq( + 'all_count' => 1, 'inboxes' => { visible_inbox.id.to_s => 1 }, 'labels' => { label.id.to_s => 1 }, 'teams' => {} diff --git a/spec/enterprise/services/conversations/unread_counts/counter_spec.rb b/spec/enterprise/services/conversations/unread_counts/counter_spec.rb index cbb2d6c98..e8f850e78 100644 --- a/spec/enterprise/services/conversations/unread_counts/counter_spec.rb +++ b/spec/enterprise/services/conversations/unread_counts/counter_spec.rb @@ -26,6 +26,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: agent).perform + expect(result[:all_count]).to eq(2) expect(result[:inboxes]).to eq(inbox.id.to_s => 2) expect(result[:labels]).to eq(label.id.to_s => 2) expect(result[:teams]).to eq(team.id.to_s => 2) @@ -40,6 +41,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: agent).perform + expect(result[:all_count]).to eq(2) expect(result[:inboxes]).to eq(inbox.id.to_s => 2) expect(result[:labels]).to eq(label.id.to_s => 2) expect(result[:teams]).to eq(team.id.to_s => 2) @@ -53,6 +55,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: agent).perform + expect(result[:all_count]).to eq(1) expect(result[:inboxes]).to eq(inbox.id.to_s => 1) expect(result[:labels]).to eq(label.id.to_s => 1) expect(result[:teams]).to eq(team.id.to_s => 1) @@ -65,7 +68,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: agent).perform - expect(result).to eq(inboxes: {}, labels: {}, teams: {}) + expect(result).to eq(all_count: 0, inboxes: {}, labels: {}, teams: {}) expect(store.base_ready?(account.id)).to be(false) expect(store.assignment_ready?(account.id)).to be(false) end diff --git a/spec/services/conversations/unread_counts/counter_spec.rb b/spec/services/conversations/unread_counts/counter_spec.rb index bfd10436e..723f2d437 100644 --- a/spec/services/conversations/unread_counts/counter_spec.rb +++ b/spec/services/conversations/unread_counts/counter_spec.rb @@ -62,6 +62,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: agent).perform expect(result).to eq( + all_count: 1, inboxes: { visible_inbox.id.to_s => 1 }, labels: { label.id.to_s => 1 }, teams: { visible_team.id.to_s => 1 } @@ -75,6 +76,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: admin).perform expect(result).to eq( + all_count: 2, inboxes: { visible_inbox.id.to_s => 1, hidden_inbox.id.to_s => 1 }, labels: { label.id.to_s => 2 }, teams: { visible_team.id.to_s => 2 } @@ -87,6 +89,7 @@ RSpec.describe Conversations::UnreadCounts::Counter do result = described_class.new(account: account, user: agent).perform expect(result).to eq( + all_count: 1, inboxes: { visible_inbox.id.to_s => 1 }, labels: {}, teams: { visible_team.id.to_s => 1 } From 940428c10e5dd23e7dfb195038dd540231845bce Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:47:08 +0530 Subject: [PATCH 4/5] fix: preserve markdown links with unsafe href characters in help center content (#14686) --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bc51bbd85..09eb084f5 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@amplitude/analytics-browser": "^2.11.10", "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", - "@chatwoot/prosemirror-schema": "1.3.19", + "@chatwoot/prosemirror-schema": "1.3.21", "@chatwoot/utils": "^0.0.55", "@formkit/core": "^1.7.2", "@formkit/vue": "^1.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68e667953..e843f8b09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: 1.2.3 version: 1.2.3 '@chatwoot/prosemirror-schema': - specifier: 1.3.19 - version: 1.3.19 + specifier: 1.3.21 + version: 1.3.21 '@chatwoot/utils': specifier: ^0.0.55 version: 0.0.55 @@ -458,8 +458,8 @@ packages: '@chatwoot/ninja-keys@1.2.3': resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==} - '@chatwoot/prosemirror-schema@1.3.19': - resolution: {integrity: sha512-LbATIAeTzclvbIK6WjtrGUO37AtMWkzCJi+s/KpUIp81TORhc0fHnclGT3353AbxeH6dF51/4hFTTsmF+ziqGA==} + '@chatwoot/prosemirror-schema@1.3.21': + resolution: {integrity: sha512-Y/OfXH1orK14foRcMrUees8lDjnDS9qZylVMyrstbfNyP5hbkBofsGAi6SP+5oIbPZ8iSG0sJyXtscoXpu/OFw==} '@chatwoot/utils@0.0.55': resolution: {integrity: sha512-8G6HYQe1ZEYfJEsSYfDVvE+uhf98JDRjtGlpB+bzMko+yltbrk4yACSo/ImC3jSaJ6K8yPTSjJToSRmsQbL2iQ==} @@ -5128,7 +5128,7 @@ snapshots: hotkeys-js: 3.8.7 lit: 2.2.6 - '@chatwoot/prosemirror-schema@1.3.19': + '@chatwoot/prosemirror-schema@1.3.21': dependencies: markdown-it-sup: 2.0.0 prosemirror-commands: 1.7.1 From ce93ddec7876b1d291b8b2185e63ab0083376793 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 11 Jun 2026 14:09:13 +0530 Subject: [PATCH 5/5] fix: re-fetch widget conversation status on reconnect (#14683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an inbox has **"Allow messages after conversation is resolved"** disabled, the live-chat widget should hide the reply box once a conversation is resolved — but users could still send a reply, silently reopening it. This syncs the widget's conversation status on reconnect so the reply box hides correctly. ## Closes - CW-7272 ## How to reproduce 1. Disable **Allow messages after conversation is resolved** on an inbox. 2. Open the widget, then let its websocket drop (background tab / lose network). 3. While disconnected, get the conversation resolved (e.g. auto-resolve on inactivity). 4. Reconnect → reply box is still visible and a sent message reopens the resolved conversation. ## Why Reply-box hiding is gated on the widget's local status, updated via the `conversation.status_changed` socket event. If the resolve happens while disconnected, the event is missed — and on reconnect the widget only re-synced messages, never the status, so it stayed stale at `open`. ## What changed - `onReconnect` now also dispatches `conversationAttributes/getAttributes` to refresh status after a missed event. - Added a spec covering the reconnect behavior. Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- app/javascript/widget/helpers/actionCable.js | 3 ++ .../widget/helpers/specs/actionCable.spec.js | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 app/javascript/widget/helpers/specs/actionCable.spec.js diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js index 60c379ed8..546317018 100644 --- a/app/javascript/widget/helpers/actionCable.js +++ b/app/javascript/widget/helpers/actionCable.js @@ -36,6 +36,9 @@ class ActionCableConnector extends BaseActionCableConnector { onReconnect = () => { this.syncLatestMessages(); + // Re-fetch conversation attributes so a status change (e.g. auto-resolve) + // that happened while disconnected is reflected, keeping the reply box state correct. + this.app.$store.dispatch('conversationAttributes/getAttributes'); }; setLastMessageId = () => { diff --git a/app/javascript/widget/helpers/specs/actionCable.spec.js b/app/javascript/widget/helpers/specs/actionCable.spec.js new file mode 100644 index 000000000..6aa3872bf --- /dev/null +++ b/app/javascript/widget/helpers/specs/actionCable.spec.js @@ -0,0 +1,53 @@ +import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; +import ActionCableConnector from '../actionCable'; + +vi.mock('@rails/actioncable', () => ({ + createConsumer: () => ({ + subscriptions: { create: () => ({}) }, + disconnect: vi.fn(), + }), +})); + +describe('Widget ActionCableConnector', () => { + let app; + let mockDispatch; + let connector; + + beforeEach(() => { + vi.useFakeTimers(); + mockDispatch = vi.fn(); + app = { + $store: { + dispatch: mockDispatch, + getters: { + getCurrentAccountId: 1, + getCurrentUserID: 1, + }, + }, + }; + connector = new ActionCableConnector(app, 'test-token'); + mockDispatch.mockClear(); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.useRealTimers(); + }); + + it('registers the conversation.status_changed event handler', () => { + expect(connector.events['conversation.status_changed']).toBe( + connector.onStatusChange + ); + }); + + it('re-fetches conversation attributes on reconnect so a status change missed while disconnected is reflected', () => { + connector.onReconnect(); + + expect(mockDispatch).toHaveBeenCalledWith( + 'conversation/syncLatestMessages' + ); + expect(mockDispatch).toHaveBeenCalledWith( + 'conversationAttributes/getAttributes' + ); + }); +});