diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index d8ad826ca..a2c6d122d 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -12,6 +12,62 @@ import { import messageReadActions from './actions/messageReadActions'; import messageTranslateActions from './actions/messageTranslateActions'; import * as Sentry from '@sentry/vue'; +import { applyPageFilters } from './helpers'; +import { matchesFilters } from './helpers/filterHelpers'; + +const DEFAULT_TAB = 'all'; + +const deriveTabKeys = (conversation, state, rootGetters) => { + const tabKeys = new Set([DEFAULT_TAB]); + const filtersByTab = state.filtersByTab || {}; + const currentUserId = rootGetters.getCurrentUser?.id; + + Object.entries(filtersByTab).forEach(([rawTabKey, filters = {}]) => { + const tabKey = rawTabKey || DEFAULT_TAB; + + if (tabKey === 'appliedFilters') { + if ( + state.appliedFilters?.length && + matchesFilters(conversation, state.appliedFilters) + ) { + tabKeys.add(tabKey); + } + return; + } + + const { inboxId, status, labels = [], teamId, conversationType } = filters; + + const matchesFilter = applyPageFilters(conversation, { + inboxId, + status, + labels, + teamId, + conversationType, + }); + + if (!matchesFilter) { + return; + } + + if (tabKey === 'me') { + if (conversation.meta?.assignee?.id === currentUserId) { + tabKeys.add(tabKey); + } + return; + } + + if (tabKey === 'unassigned') { + if (!conversation.meta?.assignee) { + tabKeys.add(tabKey); + } + return; + } + + tabKeys.add(tabKey); + }); + + return Array.from(tabKeys); +}; export const hasMessageFailedWithExternalError = pendingMessage => { // This helper is used to check if the message has failed with an external error. @@ -27,10 +83,14 @@ export const hasMessageFailedWithExternalError = pendingMessage => { // actions const actions = { - getConversation: async ({ commit }, conversationId) => { + getConversation: async ({ commit, state, rootGetters }, conversationId) => { try { const response = await ConversationApi.show(conversationId); - commit(types.UPDATE_CONVERSATION, response.data); + const tabKeys = deriveTabKeys(response.data, state, rootGetters); + commit(types.UPDATE_CONVERSATION, { + conversation: response.data, + tabKeys, + }); commit(`contacts/${types.SET_CONTACT_ITEM}`, response.data.meta.sender); } catch (error) { // Ignore error @@ -125,11 +185,9 @@ const actions = { { commit, state, dispatch }, { conversationId } ) => { - const { allConversations, syncConversationsMessages } = state; + const { conversationsById, syncConversationsMessages } = state; const lastMessageId = syncConversationsMessages[conversationId]; - const selectedChat = allConversations.find( - conversation => conversation.id === conversationId - ); + const selectedChat = conversationsById[conversationId]; if (!selectedChat) return; try { const { messages } = selectedChat; @@ -171,10 +229,8 @@ const actions = { { commit, state }, { conversationId } ) => { - const { allConversations } = state; - const selectedChat = allConversations.find( - conversation => conversation.id === conversationId - ); + const { conversationsById } = state; + const selectedChat = conversationsById[conversationId]; if (!selectedChat) return; const { messages } = selectedChat; const lastMessage = messages.last(); @@ -337,7 +393,10 @@ const actions = { } }, - addConversation({ commit, state, dispatch, rootState }, conversation) { + addConversation( + { commit, state, dispatch, rootState, rootGetters }, + conversation + ) { const { currentInbox, appliedFilters } = state; const { inbox_id: inboxId, @@ -353,7 +412,8 @@ const actions = { !isOnUnattendedView(rootState) && isMatchingInboxFilter ) { - commit(types.ADD_CONVERSATION, conversation); + const tabKeys = deriveTabKeys(conversation, state, rootGetters); + commit(types.ADD_CONVERSATION, { conversation, tabKeys }); dispatch('contacts/setContact', sender); } }, @@ -370,11 +430,12 @@ const actions = { } }, - updateConversation({ commit, dispatch }, conversation) { + updateConversation({ commit, dispatch, state, rootGetters }, conversation) { const { meta: { sender }, } = conversation; - commit(types.UPDATE_CONVERSATION, conversation); + const tabKeys = deriveTabKeys(conversation, state, rootGetters); + commit(types.UPDATE_CONVERSATION, { conversation, tabKeys }); dispatch('conversationLabels/setConversationLabel', { id: conversation.id, diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index 9f5744fbb..2d13ca9ef 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -8,22 +8,80 @@ import { } from '../../../helper/permissionsHelper'; import camelcaseKeys from 'camelcase-keys'; +const DEFAULT_TAB = 'all'; + +const normalizeTabKey = tabKey => tabKey || DEFAULT_TAB; + +const buildOrderedIdListForTab = (state, tabKey) => { + const tabPages = state.conversationPagesByTab[normalizeTabKey(tabKey)]; + if (!tabPages) { + return []; + } + const seen = new Set(); + const orderedIds = []; + Object.keys(tabPages) + .map(page => Number(page)) + .sort((a, b) => a - b) + .forEach(pageNumber => { + const ids = tabPages[String(pageNumber)] || []; + ids.forEach(id => { + if (!seen.has(id)) { + seen.add(id); + orderedIds.push(id); + } + }); + }); + return orderedIds; +}; + +const conversationsFromIds = (state, ids) => + ids.map(id => state.conversationsById[id]).filter(Boolean); + +const buildTabList = (state, tabKey) => { + const orderedIds = buildOrderedIdListForTab(state, tabKey); + return conversationsFromIds(state, orderedIds); +}; + +const buildAllConversationList = state => { + const flattenIds = Object.values(state.conversationPagesByTab).reduce( + (acc, tabPages) => { + Object.values(tabPages).forEach(pageIds => { + pageIds.forEach(id => { + if (!acc.set.has(id)) { + acc.set.add(id); + acc.ids.push(id); + } + }); + }); + return acc; + }, + { set: new Set(), ids: [] } + ); + + if (!flattenIds.ids.length) { + return Object.values(state.conversationsById); + } + + return conversationsFromIds(state, flattenIds.ids); +}; + export const getSelectedChatConversation = ({ - allConversations, + conversationsById, selectedChatId, -}) => - allConversations.filter(conversation => conversation.id === selectedChatId); +}) => { + const chat = conversationsById[selectedChatId]; + return chat ? [chat] : []; +}; const getters = { - getAllConversations: ({ allConversations, chatSortFilter: sortKey }) => { - return allConversations.sort((a, b) => sortComparator(a, b, sortKey)); + getAllConversations: state => { + const list = buildTabList(state, DEFAULT_TAB); + if (!list.length) { + return buildAllConversationList(state); + } + return [...list].sort((a, b) => sortComparator(a, b, state.chatSortFilter)); }, - getFilteredConversations: ( - { allConversations, chatSortFilter, appliedFilters }, - _, - __, - rootGetters - ) => { + getFilteredConversations: (state, _, __, rootGetters) => { const currentUser = rootGetters.getCurrentUser; const currentUserId = rootGetters.getCurrentUser.id; const currentAccountId = rootGetters.getCurrentAccountId; @@ -31,11 +89,15 @@ const getters = { const permissions = getUserPermissions(currentUser, currentAccountId); const userRole = getUserRole(currentUser, currentAccountId); - return allConversations + const baseList = buildTabList(state, 'appliedFilters').length + ? buildTabList(state, 'appliedFilters') + : buildAllConversationList(state); + + return baseList .filter(conversation => { const matchesFilterResult = matchesFilters( conversation, - appliedFilters + state.appliedFilters ); const allowedForRole = applyRoleFilter( conversation, @@ -46,13 +108,10 @@ const getters = { return matchesFilterResult && allowedForRole; }) - .sort((a, b) => sortComparator(a, b, chatSortFilter)); + .sort((a, b) => sortComparator(a, b, state.chatSortFilter)); }, - getSelectedChat: ({ selectedChatId, allConversations }) => { - const selectedChat = allConversations.find( - conversation => conversation.id === selectedChatId - ); - return selectedChat || {}; + getSelectedChat: ({ selectedChatId, conversationsById }) => { + return conversationsById[selectedChatId] || {}; }, getSelectedChatAttachments: ({ selectedChatId, attachments }) => { return attachments[selectedChatId] || []; @@ -72,10 +131,13 @@ const getters = { return lastEmail; }, - getMineChats: (_state, _, __, rootGetters) => activeFilters => { + getMineChats: (state, _, __, rootGetters) => activeFilters => { const currentUserID = rootGetters.getCurrentUser?.id; + const sourceList = buildTabList(state, 'me').length + ? buildTabList(state, 'me') + : buildAllConversationList(state); - return _state.allConversations.filter(conversation => { + return sourceList.filter(conversation => { const { assignee } = conversation.meta; const isAssignedToMe = assignee && assignee.id === currentUserID; const shouldFilter = applyPageFilters(conversation, activeFilters); @@ -95,22 +157,29 @@ const getters = { const hasAppliedFilters = _state.appliedFilters.length !== 0; return hasAppliedFilters ? filterQueryGenerator(_state.appliedFilters) : []; }, - getUnAssignedChats: _state => activeFilters => { - return _state.allConversations.filter(conversation => { + getUnAssignedChats: state => activeFilters => { + const sourceList = buildTabList(state, 'unassigned').length + ? buildTabList(state, 'unassigned') + : buildAllConversationList(state); + + return sourceList.filter(conversation => { const isUnAssigned = !conversation.meta.assignee; const shouldFilter = applyPageFilters(conversation, activeFilters); return isUnAssigned && shouldFilter; }); }, - getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => { + getAllStatusChats: (state, _, __, rootGetters) => activeFilters => { const currentUser = rootGetters.getCurrentUser; const currentUserId = rootGetters.getCurrentUser.id; const currentAccountId = rootGetters.getCurrentAccountId; const permissions = getUserPermissions(currentUser, currentAccountId); const userRole = getUserRole(currentUser, currentAccountId); + const sourceList = buildTabList(state, DEFAULT_TAB).length + ? buildTabList(state, DEFAULT_TAB) + : buildAllConversationList(state); - return _state.allConversations.filter(conversation => { + return sourceList.filter(conversation => { const shouldFilter = applyPageFilters(conversation, activeFilters); const allowedForRole = applyRoleFilter( conversation, @@ -142,10 +211,8 @@ const getters = { getChatStatusFilter: ({ chatStatusFilter }) => chatStatusFilter, getChatSortFilter: ({ chatSortFilter }) => chatSortFilter, getSelectedInbox: ({ currentInbox }) => currentInbox, - getConversationById: _state => conversationId => { - return _state.allConversations.find( - value => value.id === Number(conversationId) - ); + getConversationById: state => conversationId => { + return state.conversationsById[Number(conversationId)] || null; }, getConversationParticipants: _state => { return _state.conversationParticipants; diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js b/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js index f1c594b26..80864b410 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js @@ -45,7 +45,11 @@ export const buildConversationList = ( filterType ) => { const { payload: conversationList, meta: metaData } = responseData; - context.commit(types.SET_ALL_CONVERSATION, conversationList); + context.commit(types.SET_ALL_CONVERSATION, { + tab: filterType, + page: requestPayload.page || 1, + conversations: conversationList, + }); context.dispatch('conversationStats/set', metaData); context.dispatch( 'conversationLabels/setBulkConversationLabels', @@ -53,10 +57,14 @@ export const buildConversationList = ( ); context.commit(types.CLEAR_LIST_LOADING_STATUS); setContacts(context.commit, conversationList); + context.commit(types.SET_CONVERSATION_TAB_FILTERS, { + tab: filterType, + filters: requestPayload || {}, + }); setPageFilter({ dispatch: context.dispatch, filter: filterType, - page: requestPayload.page, + page: requestPayload.page || 1, markEndReached: !conversationList.length, }); }; diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index 2ee6fd061..0420c5883 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -1,14 +1,151 @@ import types from '../../mutation-types'; import getters, { getSelectedChatConversation } from './getters'; import actions from './actions'; -import { findPendingMessageIndex } from './helpers'; +import { findPendingMessageIndex, sortComparator } from './helpers'; import { MESSAGE_STATUS } from 'shared/constants/messages'; import wootConstants from 'dashboard/constants/globals'; import { BUS_EVENTS } from '../../../../shared/constants/busEvents'; import { emitter } from 'shared/helpers/mitt'; +const DEFAULT_TAB = 'all'; +const DEFAULT_PAGE = 1; +const DEFAULT_PAGE_SIZE = 25; + +const normalizeTabKey = tab => tab || DEFAULT_TAB; + +const clonePagesWithUpdate = (pagesByTab, tabKey, updater) => { + const normalizedTab = normalizeTabKey(tabKey); + const existingTabPages = pagesByTab[normalizedTab] || {}; + const nextTabPages = updater(existingTabPages); + if (nextTabPages === existingTabPages) { + return pagesByTab; + } + return { + ...pagesByTab, + [normalizedTab]: nextTabPages, + }; +}; + +const removeIdFromTabPages = (tabPages, conversationId) => { + let mutated = false; + const nextPages = Object.entries(tabPages).reduce((acc, [pageKey, ids]) => { + const filteredIds = ids.filter(id => id !== conversationId); + if (filteredIds.length !== ids.length) { + mutated = true; + acc[pageKey] = filteredIds; + } else { + acc[pageKey] = ids; + } + return acc; + }, {}); + return mutated ? nextPages : tabPages; +}; + +const removeIdFromAllTabs = (pagesByTab, conversationId) => { + let mutated = false; + const nextPagesByTab = Object.entries(pagesByTab).reduce( + (acc, [tabKey, tabPages]) => { + const updatedTabPages = removeIdFromTabPages(tabPages, conversationId); + if (updatedTabPages !== tabPages) { + mutated = true; + } + acc[tabKey] = updatedTabPages; + return acc; + }, + {} + ); + return mutated ? nextPagesByTab : pagesByTab; +}; + +const rebuildTabPagesWithConversation = (_state, tabKey, conversationId) => { + _state.conversationPagesByTab = clonePagesWithUpdate( + _state.conversationPagesByTab, + tabKey, + tabPages => { + const existingTabPages = Object.keys(tabPages).length + ? tabPages + : { [String(DEFAULT_PAGE)]: [] }; + + const pageNumbers = Object.keys(existingTabPages) + .map(key => Number(key)) + .sort((a, b) => a - b); + + const collectedIds = []; + const seen = new Set(); + + pageNumbers.forEach(pageNumber => { + const ids = existingTabPages[String(pageNumber)] || []; + ids.forEach(id => { + if (id === conversationId) { + return; + } + if (!seen.has(id)) { + seen.add(id); + collectedIds.push(id); + } + }); + }); + + collectedIds.push(conversationId); + + const conversations = collectedIds + .map(id => _state.conversationsById[id]) + .filter(Boolean); + + if (!conversations.length) { + return pageNumbers.reduce((acc, pageNumber) => { + acc[String(pageNumber)] = []; + return acc; + }, {}); + } + + const sorted = conversations + .slice() + .sort((a, b) => sortComparator(a, b, _state.chatSortFilter)); + + const sortedIds = sorted.map(item => item.id); + + const nextPages = {}; + pageNumbers.forEach((pageNumber, index) => { + const startIndex = index * DEFAULT_PAGE_SIZE; + nextPages[String(pageNumber)] = sortedIds.slice( + startIndex, + startIndex + DEFAULT_PAGE_SIZE + ); + }); + + return nextPages; + } + ); +}; + +const upsertConversationEntity = (_state, conversation) => { + const existing = _state.conversationsById[conversation.id]; + let updatedConversation = conversation; + + if (existing) { + if (conversation.id === _state.selectedChatId) { + updatedConversation = { + ...conversation, + allMessagesLoaded: existing.allMessagesLoaded, + messages: existing.messages, + dataFetched: existing.dataFetched, + }; + } else { + updatedConversation = { ...existing, ...conversation }; + } + } + + _state.conversationsById = { + ..._state.conversationsById, + [conversation.id]: updatedConversation, + }; +}; + const state = { - allConversations: [], + conversationsById: {}, + conversationPagesByTab: {}, + filtersByTab: {}, attachments: {}, listLoadingStatus: true, chatStatusFilter: wootConstants.STATUS_TYPE.OPEN, @@ -26,36 +163,27 @@ const state = { // mutations export const mutations = { - [types.SET_ALL_CONVERSATION](_state, conversationList) { - const newAllConversations = [..._state.allConversations]; - conversationList.forEach(conversation => { - const indexInCurrentList = newAllConversations.findIndex( - c => c.id === conversation.id - ); - if (indexInCurrentList < 0) { - newAllConversations.push(conversation); - } else if (conversation.id !== _state.selectedChatId) { - // If the conversation is already in the list, replace it - // Added this to fix the issue of the conversation not being updated - // When reconnecting to the websocket. If the selectedChatId is not the same as - // the conversation.id in the store, replace the existing conversation with the new one - newAllConversations[indexInCurrentList] = conversation; - } else { - // If the conversation is already in the list and selectedChatId is the same, - // replace all data except the messages array, attachments, dataFetched, allMessagesLoaded - const existingConversation = newAllConversations[indexInCurrentList]; - newAllConversations[indexInCurrentList] = { - ...conversation, - allMessagesLoaded: existingConversation.allMessagesLoaded, - messages: existingConversation.messages, - dataFetched: existingConversation.dataFetched, - }; - } + [types.SET_ALL_CONVERSATION](_state, { tab, page, conversations }) { + const pageNumber = page || DEFAULT_PAGE; + const tabKey = normalizeTabKey(tab); + const conversationIds = conversations.map(conversation => { + upsertConversationEntity(_state, conversation); + return conversation.id; }); - _state.allConversations = newAllConversations; + + _state.conversationPagesByTab = clonePagesWithUpdate( + _state.conversationPagesByTab, + tabKey, + tabPages => ({ + ...tabPages, + [String(pageNumber)]: conversationIds, + }) + ); }, [types.EMPTY_ALL_CONVERSATION](_state) { - _state.allConversations = []; + _state.conversationsById = {}; + _state.conversationPagesByTab = {}; + _state.filtersByTab = {}; _state.selectedChatId = null; }, [types.SET_ALL_MESSAGES_LOADED](_state) { @@ -72,17 +200,25 @@ export const mutations = { }, [types.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) { - if (data.length) { - const [chat] = _state.allConversations.filter(c => c.id === id); - chat.messages.unshift(...data); + if (!data.length) { + return; } + + const chat = _state.conversationsById[id]; + if (!chat) { + return; + } + + chat.messages = [...data, ...(chat.messages || [])]; }, [types.SET_ALL_ATTACHMENTS](_state, { id, data }) { _state.attachments[id] = [...data]; }, [types.SET_MISSING_MESSAGES](_state, { id, data }) { - const [chat] = _state.allConversations.filter(c => c.id === id); - if (!chat) return; + const chat = _state.conversationsById[id]; + if (!chat) { + return; + } chat.messages = data; }, @@ -98,22 +234,27 @@ export const mutations = { }, [types.ASSIGN_TEAM](_state, { team, conversationId }) { - const [chat] = _state.allConversations.filter(c => c.id === conversationId); - chat.meta.team = team; + const chat = _state.conversationsById[conversationId]; + if (chat) { + chat.meta.team = team; + } }, [types.UPDATE_CONVERSATION_LAST_ACTIVITY]( _state, { lastActivityAt, conversationId } ) { - const [chat] = _state.allConversations.filter(c => c.id === conversationId); - if (chat) { - chat.last_activity_at = lastActivityAt; + const chat = _state.conversationsById[conversationId]; + if (!chat) { + return; } + chat.last_activity_at = lastActivityAt; }, [types.ASSIGN_PRIORITY](_state, { priority, conversationId }) { - const [chat] = _state.allConversations.filter(c => c.id === conversationId); - chat.priority = priority; + const chat = _state.conversationsById[conversationId]; + if (chat) { + chat.priority = priority; + } }, [types.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](_state, custom_attributes) { @@ -177,59 +318,89 @@ export const mutations = { }); }, - [types.ADD_MESSAGE]({ allConversations, selectedChatId }, message) { + [types.ADD_MESSAGE](_state, message) { const { conversation_id: conversationId } = message; - const [chat] = getSelectedChatConversation({ - allConversations, - selectedChatId: conversationId, - }); - if (!chat) return; + const chat = _state.conversationsById[conversationId]; + if (!chat) { + return; + } const pendingMessageIndex = findPendingMessageIndex(chat, message); if (pendingMessageIndex !== -1) { chat.messages[pendingMessageIndex] = message; - } else { - chat.messages.push(message); - chat.timestamp = message.created_at; - const { conversation: { unread_count: unreadCount = 0 } = {} } = message; - chat.unread_count = unreadCount; - if (selectedChatId === conversationId) { - emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS); - emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE); - } + return; + } + + chat.messages.push(message); + chat.timestamp = message.created_at; + const { conversation: { unread_count: unreadCount = 0 } = {} } = message; + chat.unread_count = unreadCount; + if (_state.selectedChatId === conversationId) { + emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS); + emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE); } }, - [types.ADD_CONVERSATION](_state, conversation) { - _state.allConversations.push(conversation); + [types.ADD_CONVERSATION](_state, { conversation, tabKeys = [DEFAULT_TAB] }) { + upsertConversationEntity(_state, conversation); + + tabKeys.forEach(tabKey => { + rebuildTabPagesWithConversation(_state, tabKey, conversation.id); + }); }, [types.DELETE_CONVERSATION](_state, conversationId) { - _state.allConversations = _state.allConversations.filter( - c => c.id !== conversationId + const { [conversationId]: removed, ...rest } = _state.conversationsById; + if (!removed) { + return; + } + _state.conversationsById = rest; + _state.conversationPagesByTab = removeIdFromAllTabs( + _state.conversationPagesByTab, + conversationId ); }, - [types.UPDATE_CONVERSATION](_state, conversation) { - const { allConversations } = _state; - const index = allConversations.findIndex(c => c.id === conversation.id); + [types.UPDATE_CONVERSATION]( + _state, + { conversation, tabKeys = [DEFAULT_TAB] } + ) { + const existingConversation = _state.conversationsById[conversation.id]; - if (index > -1) { - const selectedConversation = allConversations[index]; - - // ignore out of order events - if (conversation.updated_at < selectedConversation.updated_at) { + if (existingConversation) { + if ( + conversation.updated_at && + existingConversation.updated_at && + conversation.updated_at < existingConversation.updated_at + ) { return; } const { messages, ...updates } = conversation; - allConversations[index] = { ...selectedConversation, ...updates }; - if (_state.selectedChatId === conversation.id) { - emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS); - emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE); - } + const mergedConversation = { + ...existingConversation, + ...updates, + }; + _state.conversationsById = { + ..._state.conversationsById, + [conversation.id]: mergedConversation, + }; } else { - _state.allConversations.push(conversation); + upsertConversationEntity(_state, conversation); + } + + _state.conversationPagesByTab = removeIdFromAllTabs( + _state.conversationPagesByTab, + conversation.id + ); + + tabKeys.forEach(tabKey => { + rebuildTabPagesWithConversation(_state, tabKey, conversation.id); + }); + + if (_state.selectedChatId === conversation.id) { + emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS); + emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE); } }, @@ -245,7 +416,7 @@ export const mutations = { _state, { id, lastSeen, unreadCount = 0 } ) { - const [chat] = _state.allConversations.filter(c => c.id === id); + const chat = _state.conversationsById[id]; if (chat) { chat.agent_last_seen_at = lastSeen; chat.unread_count = unreadCount; @@ -261,12 +432,14 @@ export const mutations = { // Update assignee on action cable message [types.UPDATE_ASSIGNEE](_state, payload) { - const [chat] = _state.allConversations.filter(c => c.id === payload.id); - chat.meta.assignee = payload.assignee; + const chat = _state.conversationsById[payload.id]; + if (chat) { + chat.meta.assignee = payload.assignee; + } }, [types.UPDATE_CONVERSATION_CONTACT](_state, { conversationId, ...payload }) { - const [chat] = _state.allConversations.filter(c => c.id === conversationId); + const chat = _state.conversationsById[conversationId]; if (chat) { chat.meta.sender = payload; } @@ -277,23 +450,49 @@ export const mutations = { }, [types.SET_CONVERSATION_CAN_REPLY](_state, { conversationId, canReply }) { - const [chat] = _state.allConversations.filter(c => c.id === conversationId); + const chat = _state.conversationsById[conversationId]; if (chat) { chat.can_reply = canReply; } }, [types.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) { - const chats = _state.allConversations.filter( - c => c.meta.sender.id !== contactId - ); - _state.allConversations = chats; + const remaining = {}; + const removedIds = []; + Object.values(_state.conversationsById).forEach(conversation => { + if (conversation.meta?.sender?.id === contactId) { + removedIds.push(conversation.id); + } else { + remaining[conversation.id] = conversation; + } + }); + + if (!removedIds.length) { + return; + } + + _state.conversationsById = remaining; + removedIds.forEach(conversationId => { + _state.conversationPagesByTab = removeIdFromAllTabs( + _state.conversationPagesByTab, + conversationId + ); + }); }, [types.SET_CONVERSATION_FILTERS](_state, data) { _state.appliedFilters = data; }, + [types.SET_CONVERSATION_TAB_FILTERS](_state, { tab, filters }) { + const normalizedTab = normalizeTabKey(tab); + const { page, ...rest } = filters || {}; + _state.filtersByTab = { + ..._state.filtersByTab, + [normalizedTab]: rest, + }; + }, + [types.CLEAR_CONVERSATION_FILTERS](_state) { _state.appliedFilters = []; }, diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 4f361e140..f15ec714f 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -22,6 +22,7 @@ export default { UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT', CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS', SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS', + SET_CONVERSATION_TAB_FILTERS: 'SET_CONVERSATION_TAB_FILTERS', CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS', SET_CONVERSATION_LAST_SEEN: 'SET_CONVERSATION_LAST_SEEN', SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION: