From 708bddf4dba6793fd24a7c5722a45277581ae6c1 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 12 May 2023 14:05:22 +0530 Subject: [PATCH] feat: Refetch the latest messages on action cable reconnect in widget (#6996) --- .../api/v1/widget/messages_controller.rb | 5 +- app/javascript/widget/api/conversation.js | 4 +- app/javascript/widget/api/endPoints.js | 4 +- .../widget/api/specs/endPoints.spec.js | 23 ++ app/javascript/widget/helpers/actionCable.js | 16 ++ .../store/modules/conversation/actions.js | 34 +++ .../store/modules/conversation/index.js | 1 + .../store/modules/conversation/mutations.js | 12 + .../specs/conversation/actions.spec.js | 205 ++++++++++++++++++ .../specs/conversation/mutations.spec.js | 68 ++++++ 10 files changed, 366 insertions(+), 6 deletions(-) diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 6287c94ee..76efb2c42 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -48,7 +48,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController def message_finder_params { filter_internal_messages: true, - before: permitted_params[:before] + before: permitted_params[:before], + after: permitted_params[:after] } end @@ -62,7 +63,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController def permitted_params # timestamp parameter is used in create conversation method - params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) + params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) end def set_message diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 78e4fe382..ded771316 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -16,8 +16,8 @@ const sendAttachmentAPI = async attachment => { return API.post(urlData.url, urlData.params); }; -const getMessagesAPI = async ({ before }) => { - const urlData = endPoints.getConversation({ before }); +const getMessagesAPI = async ({ before, after }) => { + const urlData = endPoints.getConversation({ before, after }); return API.get(urlData.url, { params: urlData.params }); }; diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js index 298196a43..3839a8e82 100755 --- a/app/javascript/widget/api/endPoints.js +++ b/app/javascript/widget/api/endPoints.js @@ -57,9 +57,9 @@ const sendAttachment = ({ attachment }) => { }; }; -const getConversation = ({ before }) => ({ +const getConversation = ({ before, after }) => ({ url: `/api/v1/widget/messages${window.location.search}`, - params: { before }, + params: { before, after }, }); const updateMessage = id => ({ diff --git a/app/javascript/widget/api/specs/endPoints.spec.js b/app/javascript/widget/api/specs/endPoints.spec.js index 21ed98a5a..13e57a034 100644 --- a/app/javascript/widget/api/specs/endPoints.spec.js +++ b/app/javascript/widget/api/specs/endPoints.spec.js @@ -79,3 +79,26 @@ describe('#triggerCampaign', () => { spy.mockRestore(); }); }); + +describe('#getConversation', () => { + it('should returns correct payload', () => { + const spy = jest.spyOn(global, 'Date').mockImplementation(() => ({ + toString: () => 'mock date', + })); + const windowSpy = jest.spyOn(window, 'window', 'get'); + expect( + endPoints.getConversation({ + after: 123, + }) + ).toEqual({ + url: `/api/v1/widget/messages`, + params: { + after: 123, + before: undefined, + }, + }); + windowSpy.mockRestore(); + + spy.mockRestore(); + }); +}); diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js index 40f95c342..6d7af376c 100644 --- a/app/javascript/widget/helpers/actionCable.js +++ b/app/javascript/widget/helpers/actionCable.js @@ -25,6 +25,22 @@ class ActionCableConnector extends BaseActionCableConnector { }; } + onDisconnected = () => { + this.setLastMessageId(); + }; + + onReconnect = () => { + this.syncLatestMessages(); + }; + + setLastMessageId = () => { + this.app.$store.dispatch('conversation/setLastMessageId'); + }; + + syncLatestMessages = () => { + this.app.$store.dispatch('conversation/syncLatestMessages'); + }; + onStatusChange = data => { if (data.status === 'resolved') { this.app.$store.dispatch('campaign/resetCampaign'); diff --git a/app/javascript/widget/store/modules/conversation/actions.js b/app/javascript/widget/store/modules/conversation/actions.js index ec29ea13c..526e1d5aa 100644 --- a/app/javascript/widget/store/modules/conversation/actions.js +++ b/app/javascript/widget/store/modules/conversation/actions.js @@ -52,6 +52,10 @@ export const actions = { } }, + setLastMessageId: async ({ commit }) => { + commit('setLastMessageId'); + }, + sendAttachment: async ({ commit }, params) => { const { attachment: { thumbUrl, fileType }, @@ -99,6 +103,36 @@ export const actions = { } }, + syncLatestMessages: async ({ state, commit }) => { + try { + const { lastMessageId, conversations } = state; + + const { + data: { payload, meta }, + } = await getMessagesAPI({ after: lastMessageId }); + + const { contact_last_seen_at: lastSeen } = meta; + const formattedMessages = getNonDeletedMessages({ messages: payload }); + const missingMessages = formattedMessages.filter( + message => conversations?.[message.id] === undefined + ); + if (!missingMessages.length) return; + missingMessages.forEach(message => { + conversations[message.id] = message; + }); + // Sort conversation messages by created_at + const updatedConversation = Object.fromEntries( + Object.entries(conversations).sort( + (a, b) => a[1].created_at - b[1].created_at + ) + ); + commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true }); + commit('setMissingMessagesInConversation', updatedConversation); + } catch (error) { + // IgnoreError + } + }, + clearConversations: ({ commit }) => { commit('clearConversations'); }, diff --git a/app/javascript/widget/store/modules/conversation/index.js b/app/javascript/widget/store/modules/conversation/index.js index d8bf30d04..9869b6a87 100755 --- a/app/javascript/widget/store/modules/conversation/index.js +++ b/app/javascript/widget/store/modules/conversation/index.js @@ -13,6 +13,7 @@ const state = { isAgentTyping: false, isCreating: false, }, + lastMessageId: null, }; export default { diff --git a/app/javascript/widget/store/modules/conversation/mutations.js b/app/javascript/widget/store/modules/conversation/mutations.js index ca6dafada..04b4987ff 100644 --- a/app/javascript/widget/store/modules/conversation/mutations.js +++ b/app/javascript/widget/store/modules/conversation/mutations.js @@ -62,6 +62,10 @@ export const mutations = { payload.map(message => Vue.set($state.conversations, message.id, message)); }, + setMissingMessagesInConversation($state, payload) { + Vue.set($state, 'conversation', payload); + }, + updateMessage($state, { id, content_attributes }) { $state.conversations[id] = { ...$state.conversations[id], @@ -94,4 +98,12 @@ export const mutations = { setMetaUserLastSeenAt($state, lastSeen) { $state.meta.userLastSeenAt = lastSeen; }, + + setLastMessageId($state) { + const { conversations } = $state; + const lastMessage = Object.values(conversations).pop(); + if (!lastMessage) return; + const { id } = lastMessage; + $state.lastMessageId = id; + }, }; diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js index 82e03d38a..81d0d7694 100644 --- a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js @@ -217,4 +217,209 @@ describe('#actions', () => { ]); }); }); + + describe('#syncLatestMessages', () => { + it('latest message should append to end of list', async () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, // Sunday, 23 April 2023 10:05:55 + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09 + conversation_id: 20, + }, + }, + lastMessageId: 463, + }; + API.get.mockResolvedValue({ + data: { + payload: [ + { + id: 465, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682504326, // Wednesday, 26 April 2023 10:18:46 + conversation_id: 20, + }, + ], + meta: { + contact_last_seen_at: 1466424490, + }, + }, + }); + await actions.syncLatestMessages({ state, commit }, {}); + expect(commit.mock.calls).toEqual([ + ['conversation/setMetaUserLastSeenAt', 1466424490, { root: true }], + [ + 'setMissingMessagesInConversation', + + { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + '465': { + id: 465, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682504326, + conversation_id: 20, + }, + }, + ], + ]); + }); + + it('old message should insert to exact position', async () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, // Sunday, 23 April 2023 10:05:55 + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09 + conversation_id: 20, + }, + }, + lastMessageId: 463, + }; + API.get.mockResolvedValue({ + data: { + payload: [ + { + id: 460, + content: 'Hi how are you', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682417926, // Tuesday, 25 April 2023 10:18:46 + conversation_id: 20, + }, + ], + meta: { + contact_last_seen_at: 14664223490, + }, + }, + }); + await actions.syncLatestMessages({ state, commit }, {}); + + expect(commit.mock.calls).toEqual([ + ['conversation/setMetaUserLastSeenAt', 14664223490, { root: true }], + [ + 'setMissingMessagesInConversation', + + { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, + conversation_id: 20, + }, + '460': { + id: 460, + content: 'Hi how are you', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682417926, + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + }, + ], + ]); + }); + + it('abort syncing if there is no missing messages ', async () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversation: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682244355, // Sunday, 23 April 2023 10:05:55 + conversation_id: 20, + }, + '463': { + id: 463, + content: 'ss', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, // Wednesday, 26 April 2023 06:32:09 + conversation_id: 20, + }, + }, + lastMessageId: 463, + }; + API.get.mockResolvedValue({ + data: { + payload: [], + meta: { + contact_last_seen_at: 14664223490, + }, + }, + }); + await actions.syncLatestMessages({ state, commit }, {}); + + expect(commit.mock.calls).toEqual([]); + }); + }); }); diff --git a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js index 96d2e8da8..0b96c6e20 100644 --- a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js @@ -183,4 +183,72 @@ describe('#mutations', () => { expect(state.conversations).toEqual({}); }); }); + + describe('#setMissingMessages', () => { + it('sets messages if payload is not empty', () => { + const state = { + uiFlags: { allMessagesLoaded: false }, + conversations: { + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + '464': { + id: 464, + content: 'hey will be back soon', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + }, + }; + mutations.setMessagesInConversation(state, [ + { + id: 455, + content: 'Hey billowing-grass-423 how are you?', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + ]); + expect(state.conversations).toEqual({ + '454': { + id: 454, + content: 'hi', + message_type: 0, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + '455': { + id: 455, + content: 'Hey billowing-grass-423 how are you?', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682432667, + conversation_id: 20, + }, + '464': { + id: 464, + content: 'hey will be back soon', + message_type: 3, + content_type: 'text', + content_attributes: {}, + created_at: 1682490729, + conversation_id: 20, + }, + }); + }); + }); });