Files
leadchat/app/javascript/dashboard/store/modules/conversations/index.js
Shivam Mishra e393bcf125 fix: Update the logic to handle attachments in a conversation (#9784)
When the chat is viewed, a function `fetchAllAttachments` is run to get all attachments for a particular conversation. This function, before updating the record creates the `attachments` property on the `chat` object in the store.
If in any case this function fails, the `attachments` property is not created, and when the code reaches the `dashboard/store/modules/conversations/index.js` the error occurs

This PR fixes it by ensuring that `SET_ALL_ATTACHMENTS` is always run. And it handles the default case as well


---

Sentry Issue:
[CHATWOOT-FRONTEND-APP-5Y](https://chatwoot-p3.sentry.io/issues/5459056982/)

```
TypeError: Cannot read properties of undefined (reading 'some')
  at forEach(./app/javascript/dashboard/store/modules/conversations/index.js:160:31)
  at Array.forEach(<anonymous>)
  at mutations(./app/javascript/dashboard/store/modules/conversations/index.js:159:27)
  at handler(./node_modules/vuex/dist/vuex.js:771:7)
  at forEach(./node_modules/vuex/dist/vuex.js:470:9)
  at Array.forEach(<anonymous>)
  at fn(./node_modules/vuex/dist/vuex.js:469:13)
  at Store.prototype._withCommit(./node_modules/vuex/dist/vuex.js:574:5)
  at Store.prototype.commit(./node_modules/vuex/dist/vuex.js:468:10)
  at this.commit(./node_modules/vuex/dist/vuex.js:420:21)
  at call(./app/javascript/dashboard/store/modules/conversations/actions.js:273:7)
  at tryCatch(./node_modules/videojs-record/dist/videojs.record.js:2868:27)
  at _invoke(./node_modules/videojs-record/dist/videojs.record.js:3088:32)
  at prototype[method](./node_modules/videojs-record/dist/videojs.record.js:2921:31)
  at as(/packs/js/application-cf716bca3c984faeb095.js:4:76)
  at as(/packs/js/application-cf716bca3c984faeb095.js:4:76)
  at nrWrapper(/app/accounts/81898/conversations/95:6:17817)
```

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-08-06 18:13:41 -07:00

318 lines
10 KiB
JavaScript

import Vue from 'vue';
import types from '../../mutation-types';
import getters, { getSelectedChatConversation } from './getters';
import actions from './actions';
import { findPendingMessageIndex } 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 state = {
allConversations: [],
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: {},
};
// 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,
};
}
});
_state.allConversations = newAllConversations;
},
[types.EMPTY_ALL_CONVERSATION](_state) {
_state.allConversations = [];
_state.selectedChatId = null;
},
[types.SET_ALL_MESSAGES_LOADED](_state) {
const [chat] = getSelectedChatConversation(_state);
Vue.set(chat, 'allMessagesLoaded', true);
},
[types.CLEAR_ALL_MESSAGES_LOADED](_state) {
const [chat] = getSelectedChatConversation(_state);
Vue.set(chat, 'allMessagesLoaded', false);
},
[types.CLEAR_CURRENT_CHAT_WINDOW](_state) {
_state.selectedChatId = null;
},
[types.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) {
if (data.length) {
const [chat] = _state.allConversations.filter(c => c.id === id);
chat.messages.unshift(...data);
}
},
[types.SET_ALL_ATTACHMENTS](_state, { id, data }) {
const attachments = _state.attachments[id] || [];
attachments.push(...data);
_state.attachments[id] = [...attachments];
},
[types.SET_MISSING_MESSAGES](_state, { id, data }) {
const [chat] = _state.allConversations.filter(c => c.id === id);
if (!chat) return;
Vue.set(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);
Vue.set(chat.meta, 'assignee', assignee);
},
[types.ASSIGN_TEAM](_state, { team, conversationId }) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
Vue.set(chat.meta, 'team', team);
},
[types.UPDATE_CONVERSATION_LAST_ACTIVITY](
_state,
{ lastActivityAt, conversationId }
) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
if (chat) {
Vue.set(chat, 'last_activity_at', lastActivityAt);
}
},
[types.ASSIGN_PRIORITY](_state, { priority, conversationId }) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
Vue.set(chat, 'priority', priority);
},
[types.UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES](_state, custom_attributes) {
const [chat] = getSelectedChatConversation(_state);
Vue.set(chat, 'custom_attributes', custom_attributes);
},
[types.CHANGE_CONVERSATION_STATUS](
_state,
{ conversationId, status, snoozedUntil }
) {
const conversation =
getters.getConversationById(_state)(conversationId) || {};
Vue.set(conversation, 'snoozed_until', snoozedUntil);
Vue.set(conversation, 'status', status);
},
[types.MUTE_CONVERSATION](_state) {
const [chat] = getSelectedChatConversation(_state);
Vue.set(chat, 'muted', true);
},
[types.UNMUTE_CONVERSATION](_state) {
const [chat] = getSelectedChatConversation(_state);
Vue.set(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]({ allConversations, selectedChatId }, message) {
const { conversation_id: conversationId } = message;
const [chat] = getSelectedChatConversation({
allConversations,
selectedChatId: conversationId,
});
if (!chat) return;
const pendingMessageIndex = findPendingMessageIndex(chat, message);
if (pendingMessageIndex !== -1) {
Vue.set(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);
}
}
},
[types.ADD_CONVERSATION](_state, conversation) {
_state.allConversations.push(conversation);
},
[types.UPDATE_CONVERSATION](_state, conversation) {
const { allConversations } = _state;
const currentConversationIndex = allConversations.findIndex(
c => c.id === conversation.id
);
if (currentConversationIndex > -1) {
const { messages, ...conversationAttributes } = conversation;
const currentConversation = {
...allConversations[currentConversationIndex],
...conversationAttributes,
};
Vue.set(allConversations, currentConversationIndex, currentConversation);
if (_state.selectedChatId === conversation.id) {
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
}
} else {
_state.allConversations.push(conversation);
}
},
[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.allConversations.filter(c => c.id === id);
if (chat) {
Vue.set(chat, 'agent_last_seen_at', lastSeen);
Vue.set(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.allConversations.filter(c => c.id === payload.id);
Vue.set(chat.meta, 'assignee', payload.assignee);
},
[types.UPDATE_CONVERSATION_CONTACT](_state, { conversationId, ...payload }) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
if (chat) {
Vue.set(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.allConversations.filter(c => c.id === conversationId);
if (chat) {
Vue.set(chat, 'can_reply', canReply);
}
},
[types.CLEAR_CONTACT_CONVERSATIONS](_state, contactId) {
const chats = _state.allConversations.filter(
c => c.meta.sender.id !== contactId
);
Vue.set(_state, 'allConversations', chats);
},
[types.SET_CONVERSATION_FILTERS](_state, data) {
_state.appliedFilters = data;
},
[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 };
},
};
export default {
state,
getters,
actions,
mutations,
};