diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index adb33eb6d..2371bd272 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -9,6 +9,7 @@ class ActionCableConnector extends BaseActionCableConnector { const { websocketURL = '' } = window.chatwootConfig || {}; super(app, pubsubToken, websocketURL); this.CancelTyping = []; + this.conversationEventTimestamps = new Map(); this.events = { 'message.created': this.onMessageCreated, 'message.updated': this.onMessageUpdated, @@ -33,6 +34,23 @@ class ActionCableConnector extends BaseActionCableConnector { }; } + /** + * Checks if a conversation event is out of sequence by comparing timestamps + * @param {number} conversationId - The ID of the conversation to check + * @param {number} eventTimestamp - The timestamp of the current event + * @returns {boolean} Returns true if event is out of sequence, false otherwise + */ + eventOutOfSequence(conversationId, eventTimestamp) { + if (!eventTimestamp) return false; + const lastTimestamp = this.conversationEventTimestamps.get(conversationId); + if (!lastTimestamp || eventTimestamp > lastTimestamp) { + this.conversationEventTimestamps.set(conversationId, eventTimestamp); + return false; + } + + return true; + } + // eslint-disable-next-line class-methods-use-this onReconnect = () => { emitter.emit(BUS_EVENTS.WEBSOCKET_RECONNECT); @@ -68,8 +86,10 @@ class ActionCableConnector extends BaseActionCableConnector { } }; - onAssigneeChanged = payload => { + onAssigneeChanged = (payload, eventTimestamp) => { const { id } = payload; + if (this.eventOutOfSequence(id, eventTimestamp)) return; + if (id) { this.app.$store.dispatch('updateConversation', payload); } @@ -81,7 +101,8 @@ class ActionCableConnector extends BaseActionCableConnector { this.fetchConversationStats(); }; - onConversationRead = data => { + onConversationRead = (data, eventTimestamp) => { + if (this.eventOutOfSequence(data.id, eventTimestamp)) return; this.app.$store.dispatch('updateConversation', data); }; @@ -104,12 +125,14 @@ class ActionCableConnector extends BaseActionCableConnector { // eslint-disable-next-line class-methods-use-this onReload = () => window.location.reload(); - onStatusChange = data => { + onStatusChange = (data, eventTimestamp) => { + if (this.eventOutOfSequence(data.id, eventTimestamp)) return; this.app.$store.dispatch('updateConversation', data); this.fetchConversationStats(); }; - onConversationUpdated = data => { + onConversationUpdated = (data, eventTimestamp) => { + if (this.eventOutOfSequence(data.id, eventTimestamp)) return; this.app.$store.dispatch('updateConversation', data); this.fetchConversationStats(); }; diff --git a/app/javascript/shared/helpers/BaseActionCableConnector.js b/app/javascript/shared/helpers/BaseActionCableConnector.js index 3eb61a80a..e7b135786 100644 --- a/app/javascript/shared/helpers/BaseActionCableConnector.js +++ b/app/javascript/shared/helpers/BaseActionCableConnector.js @@ -79,10 +79,10 @@ class BaseActionCableConnector { this.consumer.disconnect(); } - onReceived = ({ event, data } = {}) => { + onReceived = ({ event, data, event_timestamp } = {}) => { if (this.isAValidEvent(data)) { if (this.events[event] && typeof this.events[event] === 'function') { - this.events[event](data); + this.events[event](data, event_timestamp); } } };