From ce93ddec7876b1d291b8b2185e63ab0083376793 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 11 Jun 2026 14:09:13 +0530 Subject: [PATCH] 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' + ); + }); +});