546 lines
15 KiB
JavaScript
546 lines
15 KiB
JavaScript
import types from '../../mutation-types';
|
|
import getters, { getSelectedChatConversation } from './getters';
|
|
import actions from './actions';
|
|
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);
|
|
|
|
const totalPagesNeeded = Math.max(
|
|
Math.ceil(conversations.length / DEFAULT_PAGE_SIZE),
|
|
1
|
|
);
|
|
|
|
const basePageNumbers = pageNumbers.length
|
|
? [...pageNumbers]
|
|
: [DEFAULT_PAGE];
|
|
|
|
while (basePageNumbers.length < totalPagesNeeded) {
|
|
const lastPage = basePageNumbers[basePageNumbers.length - 1];
|
|
basePageNumbers.push(lastPage + 1);
|
|
}
|
|
|
|
if (basePageNumbers.length > totalPagesNeeded) {
|
|
basePageNumbers.length = totalPagesNeeded;
|
|
}
|
|
|
|
if (!conversations.length) {
|
|
return basePageNumbers.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 = {};
|
|
basePageNumbers.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 = {
|
|
conversationsById: {},
|
|
conversationPagesByTab: {},
|
|
filtersByTab: {},
|
|
attachments: {},
|
|
listLoadingStatus: true,
|
|
chatStatusFilter: wootConstants.STATUS_TYPE.OPEN,
|
|
chatSortFilter: wootConstants.SORT_BY_TYPE.LATEST,
|
|
currentInbox: null,
|
|
selectedChatId: null,
|
|
appliedFilters: [],
|
|
contextMenuChatId: null,
|
|
conversationParticipants: [],
|
|
conversationLastSeen: null,
|
|
syncConversationsMessages: {},
|
|
conversationFilters: {},
|
|
copilotAssistant: {},
|
|
};
|
|
|
|
// mutations
|
|
export const mutations = {
|
|
[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.conversationPagesByTab = clonePagesWithUpdate(
|
|
_state.conversationPagesByTab,
|
|
tabKey,
|
|
tabPages => ({
|
|
...tabPages,
|
|
[String(pageNumber)]: conversationIds,
|
|
})
|
|
);
|
|
},
|
|
[types.EMPTY_ALL_CONVERSATION](_state) {
|
|
_state.conversationsById = {};
|
|
_state.conversationPagesByTab = {};
|
|
_state.filtersByTab = {};
|
|
_state.selectedChatId = null;
|
|
},
|
|
[types.SET_ALL_MESSAGES_LOADED](_state) {
|
|
const [chat] = getSelectedChatConversation(_state);
|
|
chat.allMessagesLoaded = true;
|
|
},
|
|
|
|
[types.CLEAR_ALL_MESSAGES_LOADED](_state) {
|
|
const [chat] = getSelectedChatConversation(_state);
|
|
chat.allMessagesLoaded = false;
|
|
},
|
|
[types.CLEAR_CURRENT_CHAT_WINDOW](_state) {
|
|
_state.selectedChatId = null;
|
|
},
|
|
|
|
[types.SET_PREVIOUS_CONVERSATIONS](_state, { id, 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.conversationsById[id];
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
chat.messages = data;
|
|
},
|
|
|
|
[types.SET_CURRENT_CHAT_WINDOW](_state, activeChat) {
|
|
if (activeChat) {
|
|
_state.selectedChatId = activeChat.id;
|
|
}
|
|
},
|
|
|
|
[types.ASSIGN_AGENT](_state, assignee) {
|
|
const [chat] = getSelectedChatConversation(_state);
|
|
chat.meta.assignee = assignee;
|
|
},
|
|
|
|
[types.ASSIGN_TEAM](_state, { team, conversationId }) {
|
|
const chat = _state.conversationsById[conversationId];
|
|
if (chat) {
|
|
chat.meta.team = team;
|
|
}
|
|
},
|
|
|
|
[types.UPDATE_CONVERSATION_LAST_ACTIVITY](
|
|
_state,
|
|
{ lastActivityAt, conversationId }
|
|
) {
|
|
const chat = _state.conversationsById[conversationId];
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
chat.last_activity_at = lastActivityAt;
|
|
},
|
|
[types.ASSIGN_PRIORITY](_state, { priority, conversationId }) {
|
|
const chat = _state.conversationsById[conversationId];
|
|
if (chat) {
|
|
chat.priority = priority;
|
|
}
|
|
},
|
|
|
|
[types.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](_state, custom_attributes) {
|
|
const [chat] = getSelectedChatConversation(_state);
|
|
chat.custom_attributes = custom_attributes;
|
|
},
|
|
|
|
[types.CHANGE_CONVERSATION_STATUS](
|
|
_state,
|
|
{ conversationId, status, snoozedUntil }
|
|
) {
|
|
const conversation =
|
|
getters.getConversationById(_state)(conversationId) || {};
|
|
conversation.snoozed_until = snoozedUntil;
|
|
conversation.status = status;
|
|
},
|
|
|
|
[types.MUTE_CONVERSATION](_state) {
|
|
const [chat] = getSelectedChatConversation(_state);
|
|
chat.muted = true;
|
|
},
|
|
|
|
[types.UNMUTE_CONVERSATION](_state) {
|
|
const [chat] = getSelectedChatConversation(_state);
|
|
chat.muted = false;
|
|
},
|
|
|
|
[types.ADD_CONVERSATION_ATTACHMENTS](_state, message) {
|
|
// early return if the message has not been sent, or has no attachments
|
|
if (
|
|
message.status !== MESSAGE_STATUS.SENT ||
|
|
!message.attachments?.length
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const id = message.conversation_id;
|
|
const existingAttachments = _state.attachments[id] || [];
|
|
|
|
const attachmentsToAdd = message.attachments.filter(attachment => {
|
|
// if the attachment is not already in the store, add it
|
|
// this is to prevent duplicates
|
|
return !existingAttachments.some(
|
|
existingAttachment => existingAttachment.id === attachment.id
|
|
);
|
|
});
|
|
|
|
// replace the attachments in the store
|
|
_state.attachments[id] = [...existingAttachments, ...attachmentsToAdd];
|
|
},
|
|
|
|
[types.DELETE_CONVERSATION_ATTACHMENTS](_state, message) {
|
|
if (message.status !== MESSAGE_STATUS.SENT) return;
|
|
|
|
const { conversation_id: id } = message;
|
|
const existingAttachments = _state.attachments[id] || [];
|
|
if (!existingAttachments.length) return;
|
|
|
|
_state.attachments[id] = existingAttachments.filter(attachment => {
|
|
return attachment.message_id !== message.id;
|
|
});
|
|
},
|
|
|
|
[types.ADD_MESSAGE](_state, message) {
|
|
const { conversation_id: conversationId } = message;
|
|
const chat = _state.conversationsById[conversationId];
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const pendingMessageIndex = findPendingMessageIndex(chat, message);
|
|
if (pendingMessageIndex !== -1) {
|
|
chat.messages[pendingMessageIndex] = 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, tabKeys = [DEFAULT_TAB] }) {
|
|
upsertConversationEntity(_state, conversation);
|
|
|
|
tabKeys.forEach(tabKey => {
|
|
rebuildTabPagesWithConversation(_state, tabKey, conversation.id);
|
|
});
|
|
},
|
|
|
|
[types.DELETE_CONVERSATION](_state, conversationId) {
|
|
const { [conversationId]: removed, ...rest } = _state.conversationsById;
|
|
if (!removed) {
|
|
return;
|
|
}
|
|
_state.conversationsById = rest;
|
|
_state.conversationPagesByTab = removeIdFromAllTabs(
|
|
_state.conversationPagesByTab,
|
|
conversationId
|
|
);
|
|
},
|
|
|
|
[types.UPDATE_CONVERSATION](
|
|
_state,
|
|
{ conversation, tabKeys = [DEFAULT_TAB] }
|
|
) {
|
|
const existingConversation = _state.conversationsById[conversation.id];
|
|
|
|
if (existingConversation) {
|
|
if (
|
|
conversation.updated_at &&
|
|
existingConversation.updated_at &&
|
|
conversation.updated_at < existingConversation.updated_at
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const { messages, ...updates } = conversation;
|
|
const mergedConversation = {
|
|
...existingConversation,
|
|
...updates,
|
|
};
|
|
_state.conversationsById = {
|
|
..._state.conversationsById,
|
|
[conversation.id]: mergedConversation,
|
|
};
|
|
} else {
|
|
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);
|
|
}
|
|
},
|
|
|
|
[types.SET_LIST_LOADING_STATUS](_state) {
|
|
_state.listLoadingStatus = true;
|
|
},
|
|
|
|
[types.CLEAR_LIST_LOADING_STATUS](_state) {
|
|
_state.listLoadingStatus = false;
|
|
},
|
|
|
|
[types.UPDATE_MESSAGE_UNREAD_COUNT](
|
|
_state,
|
|
{ id, lastSeen, unreadCount = 0 }
|
|
) {
|
|
const chat = _state.conversationsById[id];
|
|
if (chat) {
|
|
chat.agent_last_seen_at = lastSeen;
|
|
chat.unread_count = unreadCount;
|
|
}
|
|
},
|
|
[types.CHANGE_CHAT_STATUS_FILTER](_state, data) {
|
|
_state.chatStatusFilter = data;
|
|
},
|
|
|
|
[types.CHANGE_CHAT_SORT_FILTER](_state, data) {
|
|
_state.chatSortFilter = data;
|
|
},
|
|
|
|
// Update assignee on action cable message
|
|
[types.UPDATE_ASSIGNEE](_state, payload) {
|
|
const chat = _state.conversationsById[payload.id];
|
|
if (chat) {
|
|
chat.meta.assignee = payload.assignee;
|
|
}
|
|
},
|
|
|
|
[types.UPDATE_CONVERSATION_CONTACT](_state, { conversationId, ...payload }) {
|
|
const chat = _state.conversationsById[conversationId];
|
|
if (chat) {
|
|
chat.meta.sender = payload;
|
|
}
|
|
},
|
|
|
|
[types.SET_ACTIVE_INBOX](_state, inboxId) {
|
|
_state.currentInbox = inboxId ? parseInt(inboxId, 10) : null;
|
|
},
|
|
|
|
[types.SET_CONVERSATION_CAN_REPLY](_state, { conversationId, canReply }) {
|
|
const chat = _state.conversationsById[conversationId];
|
|
if (chat) {
|
|
chat.can_reply = canReply;
|
|
}
|
|
},
|
|
|
|
[types.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
|
|
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 = [];
|
|
},
|
|
|
|
[types.SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION](
|
|
_state,
|
|
{ conversationId, messageId }
|
|
) {
|
|
_state.syncConversationsMessages[conversationId] = messageId;
|
|
},
|
|
|
|
[types.SET_CONTEXT_MENU_CHAT_ID](_state, chatId) {
|
|
_state.contextMenuChatId = chatId;
|
|
},
|
|
|
|
[types.SET_CHAT_LIST_FILTERS](_state, data) {
|
|
_state.conversationFilters = data;
|
|
},
|
|
[types.UPDATE_CHAT_LIST_FILTERS](_state, data) {
|
|
_state.conversationFilters = { ..._state.conversationFilters, ...data };
|
|
},
|
|
[types.SET_INBOX_CAPTAIN_ASSISTANT](_state, data) {
|
|
_state.copilotAssistant = data.assistant;
|
|
},
|
|
};
|
|
|
|
export default {
|
|
state,
|
|
getters,
|
|
actions,
|
|
mutations,
|
|
};
|