diff --git a/app/javascript/widget/components/ChatMessage.vue b/app/javascript/widget/components/ChatMessage.vue index f80933b83..9a23f1458 100755 --- a/app/javascript/widget/components/ChatMessage.vue +++ b/app/javascript/widget/components/ChatMessage.vue @@ -1,5 +1,9 @@ diff --git a/app/javascript/widget/components/UserMessage.vue b/app/javascript/widget/components/UserMessage.vue index 81ae2edad..e6cebc8e2 100755 --- a/app/javascript/widget/components/UserMessage.vue +++ b/app/javascript/widget/components/UserMessage.vue @@ -1,7 +1,7 @@ @@ -15,8 +15,12 @@ export default { UserMessageBubble, }, props: { - message: String, avatarUrl: String, + message: String, + status: { + type: String, + default: '', + }, }, }; diff --git a/app/javascript/widget/components/UserMessageBubble.vue b/app/javascript/widget/components/UserMessageBubble.vue index 977d78eff..1517f2995 100755 --- a/app/javascript/widget/components/UserMessageBubble.vue +++ b/app/javascript/widget/components/UserMessageBubble.vue @@ -1,7 +1,7 @@ @@ -16,10 +16,17 @@ export default { ...mapGetters({ widgetColor: 'appConfig/getWidgetColor', }), + backgroundColor() { + return this.status !== 'in_progress' ? this.widgetColor : '#c0ccda'; + }, }, mixins: [messageFormatterMixin], props: { message: String, + status: { + type: String, + default: '', + }, }, }; diff --git a/app/javascript/widget/helpers/uuid.js b/app/javascript/widget/helpers/uuid.js new file mode 100644 index 000000000..740d1bc3a --- /dev/null +++ b/app/javascript/widget/helpers/uuid.js @@ -0,0 +1,10 @@ +const getUuid = () => + 'xxxxxxxx4xxx'.replace(/[xy]/g, c => { + // eslint-disable-next-line + const r = (Math.random() * 16) | 0; + // eslint-disable-next-line + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + +export default getUuid; diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js index bff606001..6f1f25953 100755 --- a/app/javascript/widget/store/modules/conversation.js +++ b/app/javascript/widget/store/modules/conversation.js @@ -1,6 +1,24 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; import { sendMessageAPI, getConversationAPI } from 'widget/api/conversation'; +import { MESSAGE_TYPE } from 'widget/helpers/constants'; +import getUuid from '../../helpers/uuid'; + +export const createTemporaryMessage = content => { + const timestamp = new Date().getTime(); + return { + id: getUuid(), + content, + status: 'in_progress', + created_at: timestamp, + message_type: MESSAGE_TYPE.INCOMING, + }; +}; + +export const findUndeliveredMessage = (messageInbox, { content }) => + Object.values(messageInbox).filter( + message => message.content === content && message.status === 'in_progress' + ); export const DEFAULT_CONVERSATION = 'default'; const state = { @@ -13,8 +31,9 @@ const getters = { }; const actions = { - sendMessage: async (_, params) => { + sendMessage: async ({ commit }, params) => { const { content } = params; + commit('pushMessageToConversations', createTemporaryMessage(content)); await sendMessageAPI(content); }, @@ -38,9 +57,27 @@ const mutations = { }, pushMessageToConversations($state, message) { - const { id } = message; + const { id, status, message_type: type } = message; const messagesInbox = $state.conversations; - Vue.set(messagesInbox, id, message); + const isMessageIncoming = type === MESSAGE_TYPE.INCOMING; + const isTemporaryMessage = status === 'in_progress'; + + if (!isMessageIncoming || isTemporaryMessage) { + Vue.set(messagesInbox, id, message); + return; + } + + const [messageInConversation] = findUndeliveredMessage( + messagesInbox, + message + ); + + if (!messageInConversation) { + Vue.set(messagesInbox, id, message); + } else { + Vue.delete(messagesInbox, messageInConversation.id); + Vue.set(messagesInbox, id, message); + } }, initMessagesInConversation(_state, payload) { diff --git a/app/javascript/widget/store/modules/specs/conversation.spec.js b/app/javascript/widget/store/modules/specs/conversation.spec.js new file mode 100644 index 000000000..03a97618b --- /dev/null +++ b/app/javascript/widget/store/modules/specs/conversation.spec.js @@ -0,0 +1,37 @@ +import { + findUndeliveredMessage, + createTemporaryMessage, +} from '../conversation'; + +describe('#findUndeliveredMessage', () => { + it('returns message objects if exist', () => { + const conversation = { + 1: { + id: 1, + content: 'Hello', + status: 'in_progress', + }, + 2: { + id: 2, + content: 'Hello', + status: 'sent', + }, + 3: { + id: 3, + content: 'How may I help you', + status: 'sent', + }, + }; + expect( + findUndeliveredMessage(conversation, { content: 'Hello' }) + ).toStrictEqual([{ id: 1, content: 'Hello', status: 'in_progress' }]); + }); +}); + +describe('#createTemporaryMessage', () => { + it('returns message object', () => { + const message = createTemporaryMessage('hello'); + expect(message.content).toBe('hello'); + expect(message.status).toBe('in_progress'); + }); +}); diff --git a/jest.config.js b/jest.config.js index 45f9a258e..1986065f4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,15 +2,15 @@ process.env.VUE_CLI_BABEL_TARGET_NODE = true; process.env.VUE_CLI_BABEL_TRANSPILE_MODULES = true; module.exports = { - moduleDirectories: ['node_modules', 'app/javascript/app'], - moduleFileExtensions: ['js', 'jsx', 'json', 'vue', 'ts', 'tsx', 'vue'], + moduleDirectories: ['node_modules', 'app/javascript'], + moduleFileExtensions: ['js', 'jsx', 'json', 'vue', 'ts', 'tsx'], automock: false, resetMocks: true, transform: { '^.+\\.vue$': 'vue-jest', - '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$': + '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.(js|jsx)?$': 'babel-jest', }, cacheDirectory: '/.jest-cache', collectCoverage: false,