diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 3552765c7..a72c180c1 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -29,6 +29,10 @@ const getConversationAPI = async () => { return API.get(`/api/v1/widget/conversations${window.location.search}`); }; +const getConversationsAPI = async () => { + return API.get(`/api/v1/widget/conversations`); +}; + const toggleTyping = async ({ typingStatus }) => { return API.post( `/api/v1/widget/conversations/toggle_typing${window.location.search}`, @@ -58,4 +62,5 @@ export { toggleTyping, setUserLastSeenAt, sendEmailTranscript, + getConversationsAPI, }; diff --git a/app/javascript/widget/store/modules/conversation_new/actions.js b/app/javascript/widget/store/modules/conversation_new/actions.js new file mode 100644 index 000000000..9b2914736 --- /dev/null +++ b/app/javascript/widget/store/modules/conversation_new/actions.js @@ -0,0 +1,141 @@ +import { + createConversationAPI, + sendMessageAPI, + sendAttachmentAPI, + // getMessagesAPI, + // toggleTyping, + // setUserLastSeenAt, + getConversationAPI, + getConversationsAPI, +} from 'widget/api/conversation'; +import { refreshActionCableConnector } from '../../../helpers/actionCable'; + +import { + createTemporaryMessage, + createTemporaryAttachmentMessage, +} from './helpers'; + +// Get activeConversation and pass it down to each action call, to +// target the right converdation +export const actions = { + fetchAllConversations: async ({ commit }) => { + try { + commit('setUIFlag', { isFetching: true }); + const { data } = await getConversationsAPI(); + data.forEach(conversation => { + const { id: conversationId, messages } = conversation; + commit('addConversationEntry', conversation); + commit('addConversationId', conversation.id); + commit('addMessagesEntry', { conversationId, messages }); + commit('addMessageIds', { conversationId, messages }); + }); + } catch (error) { + throw new Error(error); + } finally { + commit('setUIFlag', { isFetching: false }); + } + }, + fetchConversationById: async ({ commit }, params) => { + const { conversationId } = params; + try { + commit('setConversationUIFlag', { isFetching: true }); + const { data } = await getConversationAPI(conversationId); + + const { messages } = data; + commit('updateConversationEntry', data); + commit('addMessagesEntry', { conversationId, messages }); + commit('addMessageIds', { conversationId, messages }); + } catch (error) { + throw new Error(error); + } finally { + commit('setConversationUIFlag', { isFetching: false }); + } + }, + createConversation: async ({ commit }, params) => { + commit('setConversationUIFlag', { isCreating: true }); + try { + const { data } = await createConversationAPI(params); + const { id: conversationId, messages } = data; + + commit('addConversationEntry', data); + commit('addConversationId', conversationId); + commit('addMessagesEntry', { conversationId, messages }); + commit('addMessageIds', { conversationId, messages }); + } catch (error) { + throw new Error(error); + } finally { + commit('setConversationUIFlag', { isCreating: false }); + } + }, + sendMessage: async ({ commit }, params) => { + const { content, conversationId } = params; + const message = createTemporaryMessage({ content }); + const messages = [message]; + commit('addMessagesEntry', { conversationId, messages }); + commit('addMessageIds', { conversationId, messages }); + await sendMessageAPI(content, conversationId); + }, + sendAttachment: async ({ commit }, params) => { + const { + attachment: { thumbUrl, fileType }, + conversationId, + } = params; + const message = createTemporaryAttachmentMessage({ thumbUrl, fileType }); + const messages = [message]; + commit('addMessagesEntry', { conversationId, messages }); + commit('addMessageIds', { conversationId, messages }); + try { + const { data } = await sendAttachmentAPI(params); + commit('updateAttachmentMessageStatus', { + message: data, + tempId: tempMessage.id, + }); + } catch (error) { + // Show error + } + }, + // fetchOldConversations: async ({ commit }, { before } = {}) => { + // try { + // commit('setConversationListLoading', true); + // const { data } = await getMessagesAPI({ before }); + // const formattedMessages = getNonDeletedMessages({ messages: data }); + // commit('setMessagesInConversation', formattedMessages); + // commit('setConversationListLoading', false); + // } catch (error) { + // commit('setConversationListLoading', false); + // } + // }, + // clearConversations: ({ commit }) => { + // commit('clearConversations'); + // }, + // addOrUpdateMessage: async ({ commit }, data) => { + // const { id, content_attributes } = data; + // if (content_attributes && content_attributes.deleted) { + // commit('deleteMessage', id); + // return; + // } + // commit('pushMessageToConversation', data); + // }, + // toggleAgentTyping({ commit }, data) { + // commit('toggleAgentTypingStatus', data); + // }, + // toggleUserTyping: async (_, data) => { + // try { + // await toggleTyping(data); + // } catch (error) { + // // IgnoreError + // } + // }, + // setUserLastSeen: async ({ commit, getters: appGetters }) => { + // if (!appGetters.getConversationSize) { + // return; + // } + // const lastSeen = Date.now() / 1000; + // try { + // commit('setMetaUserLastSeenAt', lastSeen); + // await setUserLastSeenAt({ lastSeen }); + // } catch (error) { + // // IgnoreError + // } + // }, +}; diff --git a/app/javascript/widget/store/modules/conversation_new/getters.js b/app/javascript/widget/store/modules/conversation_new/getters.js new file mode 100644 index 000000000..165584c1c --- /dev/null +++ b/app/javascript/widget/store/modules/conversation_new/getters.js @@ -0,0 +1,56 @@ +import { MESSAGE_TYPE } from 'widget/helpers/constants'; +import groupBy from 'lodash.groupby'; +import { groupConversationBySender } from './helpers'; +import { formatUnixDate } from 'shared/helpers/DateHelper'; + +export const getters = { + getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded, + getIsCreating: _state => _state.uiFlags.isCreating, + getIsAgentTyping: _state => _state.uiFlags.isAgentTyping, + getConversation: _state => _state.conversations, + getConversationSize: _state => Object.keys(_state.conversations).length, + getEarliestMessage: _state => { + const conversation = Object.values(_state.conversations); + if (conversation.length) { + return conversation[0]; + } + return {}; + }, + getGroupedConversation: _state => { + const conversationGroupedByDate = groupBy( + Object.values(_state.conversations), + message => formatUnixDate(message.created_at) + ); + return Object.keys(conversationGroupedByDate).map(date => ({ + date, + messages: groupConversationBySender(conversationGroupedByDate[date]), + })); + }, + getIsFetchingList: _state => _state.uiFlags.isFetchingList, + getMessageCount: _state => { + return Object.values(_state.conversations).length; + }, + getUnreadMessageCount: _state => { + const { userLastSeenAt } = _state.meta; + const count = Object.values(_state.conversations).filter(chat => { + const { created_at: createdAt, message_type: messageType } = chat; + const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING; + const hasNotSeen = userLastSeenAt + ? createdAt * 1000 > userLastSeenAt * 1000 + : true; + return hasNotSeen && isOutGoing; + }).length; + return count; + }, + getUnreadTextMessages: (_state, _getters) => { + const unreadCount = _getters.getUnreadMessageCount; + const allMessages = [...Object.values(_state.conversations)]; + const unreadAgentMessages = allMessages.filter(message => { + const { message_type: messageType } = message; + return messageType === MESSAGE_TYPE.OUTGOING; + }); + const maxUnreadCount = Math.min(unreadCount, 3); + const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount); + return allUnreadMessages; + }, +}; diff --git a/app/javascript/widget/store/modules/conversation_new/helpers.js b/app/javascript/widget/store/modules/conversation_new/helpers.js new file mode 100644 index 000000000..65f2cfa1d --- /dev/null +++ b/app/javascript/widget/store/modules/conversation_new/helpers.js @@ -0,0 +1,71 @@ +import { MESSAGE_TYPE } from 'widget/helpers/constants'; +import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper'; + +import getUuid from '../../../helpers/uuid'; +export const createTemporaryMessage = ({ attachments, content }) => { + const timestamp = new Date().getTime() / 1000; + return { + id: getUuid(), + content, + attachments, + status: 'in_progress', + created_at: timestamp, + message_type: MESSAGE_TYPE.INCOMING, + }; +}; +export const createTemporaryAttachmentMessage = ({ + thumbUrl, + fileType, + content, +}) => { + const attachment = { + thumb_url: thumbUrl, + data_url: thumbUrl, + file_type: fileType, + status: 'in_progress', + }; + const message = createTemporaryMessage({ + attachments: [attachment], + content, + }); + return message; +}; + +const getSenderName = message => (message.sender ? message.sender.name : ''); + +const shouldShowAvatar = (message, nextMessage) => { + const currentSender = getSenderName(message); + const nextSender = getSenderName(nextMessage); + + return ( + currentSender !== nextSender || + message.message_type !== nextMessage.message_type || + isASubmittedFormMessage(nextMessage) + ); +}; + +export const groupConversationBySender = conversationsForADate => + conversationsForADate.map((message, index) => { + let showAvatar = false; + const isLastMessage = index === conversationsForADate.length - 1; + if (isASubmittedFormMessage(message)) { + showAvatar = false; + } else if (isLastMessage) { + showAvatar = true; + } else { + const nextMessage = conversationsForADate[index + 1]; + showAvatar = shouldShowAvatar(message, nextMessage); + } + return { showAvatar, ...message }; + }); + +export const findUndeliveredMessage = (messageInbox, { content }) => + Object.values(messageInbox).filter( + message => message.content === content && message.status === 'in_progress' + ); + +export const getNonDeletedMessages = ({ messages }) => { + return messages.filter( + item => !(item.content_attributes && item.content_attributes.deleted) + ); +}; diff --git a/app/javascript/widget/store/modules/conversation_new/index.js b/app/javascript/widget/store/modules/conversation_new/index.js index 49ca61bd4..1883e1482 100755 --- a/app/javascript/widget/store/modules/conversation_new/index.js +++ b/app/javascript/widget/store/modules/conversation_new/index.js @@ -24,6 +24,7 @@ const state = { uiFlags: { allConversationsLoaded: false, isFetching: false, + isCreating: true, }, }; diff --git a/app/javascript/widget/store/modules/conversation_new/mutations.js b/app/javascript/widget/store/modules/conversation_new/mutations.js index 047805e23..a0074260f 100644 --- a/app/javascript/widget/store/modules/conversation_new/mutations.js +++ b/app/javascript/widget/store/modules/conversation_new/mutations.js @@ -48,6 +48,21 @@ export const mutations = { }; }, + addMessagesEntry($state, { conversationId, messages = [] }) { + if (!conversationId) return; + + const allMessages = $state.messages; + const newMessages = messages.reduce( + (obj, message) => ({ + ...obj, + [message.id]: message, + }), + {} + ); + const updatedMessages = { ...allMessages, ...newMessages }; + Vue.set($state.messages, 'byId', updatedMessages); + }, + addMessageIds($state, { conversationId, messages }) { const conversationById = $state.conversations.byId[conversationId]; if (!conversationById) return;