feat: Rewrite conversations mixin to a helper (#9931)

This commit is contained in:
Sivin Varghese
2024-08-13 15:15:04 +05:30
committed by GitHub
parent c26490e9c1
commit b33d59d804
9 changed files with 395 additions and 394 deletions

View File

@@ -10,7 +10,6 @@ import ChatListHeader from './ChatListHeader.vue';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
@@ -42,7 +41,7 @@ export default {
IntersectionObserver,
VirtualList,
},
mixins: [conversationMixin, filterMixin],
mixins: [filterMixin],
provide() {
return {
// Actions to be performed on virtual list item and context menu.

View File

@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
import { getLastMessage } from 'dashboard/helper/conversationHelper';
import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations';
import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName.vue';
@@ -24,7 +24,7 @@ export default {
PriorityMark,
SLACardLabel,
},
mixins: [inboxMixin, conversationMixin],
mixins: [inboxMixin],
props: {
activeLabel: {
type: String,
@@ -118,7 +118,7 @@ export default {
},
lastMessageInChat() {
return this.lastMessage(this.chat);
return getLastMessage(this.chat);
},
inbox() {

View File

@@ -13,9 +13,6 @@ import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
// mixins
import conversationMixin, {
filterDuplicateSourceMessages,
} from '../../../mixins/conversations';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import configMixin from 'shared/mixins/configMixin';
import aiMixin from 'dashboard/mixins/aiMixin';
@@ -24,6 +21,11 @@ import aiMixin from 'dashboard/mixins/aiMixin';
import { getTypingUsersText } from '../../../helper/commons';
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
import { LocalStorage } from 'shared/helpers/localStorage';
import {
filterDuplicateSourceMessages,
getReadMessages,
getUnreadMessages,
} from 'dashboard/helper/conversationHelper';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -38,7 +40,7 @@ export default {
Banner,
ConversationLabelSuggestion,
},
mixins: [conversationMixin, inboxMixin, configMixin, aiMixin],
mixins: [inboxMixin, configMixin, aiMixin],
props: {
isContactPanelOpen: {
type: Boolean,
@@ -138,14 +140,14 @@ export default {
}
return messages;
},
getReadMessages() {
return this.readMessages(
readMessages() {
return getReadMessages(
this.getMessages,
this.currentChat.agent_last_seen_at
);
},
getUnReadMessages() {
return this.unReadMessages(
unReadMessages() {
return getUnreadMessages(
this.getMessages,
this.currentChat.agent_last_seen_at
);
@@ -468,7 +470,7 @@ export default {
</li>
</transition>
<Message
v-for="message in getReadMessages"
v-for="message in readMessages"
:key="message.id"
class="message--read ph-no-capture"
data-clarity-mask="True"
@@ -493,7 +495,7 @@ export default {
</span>
</li>
<Message
v-for="message in getUnReadMessages"
v-for="message in unReadMessages"
:key="message.id"
class="message--unread ph-no-capture"
data-clarity-mask="True"

View File

@@ -0,0 +1,93 @@
/**
* Determines the last non-activity message between store and API messages.
* @param {Object} messageInStore - The last non-activity message from the store.
* @param {Object} messageFromAPI - The last non-activity message from the API.
* @returns {Object} The latest non-activity message.
*/
const getLastNonActivityMessage = (messageInStore, messageFromAPI) => {
// If both API value and store value for last non activity message
// are available, then return the latest one.
if (messageInStore && messageFromAPI) {
return messageInStore.created_at >= messageFromAPI.created_at
? messageInStore
: messageFromAPI;
}
// Otherwise, return whichever is available
return messageInStore || messageFromAPI;
};
/**
* Filters out duplicate source messages from an array of messages.
* @param {Array} messages - The array of messages to filter.
* @returns {Array} An array of messages without duplicates.
*/
export const filterDuplicateSourceMessages = (messages = []) => {
const messagesWithoutDuplicates = [];
// We cannot use Map or any short hand method as it returns the last message with the duplicate ID
// We should return the message with smaller id when there is a duplicate
messages.forEach(m1 => {
if (m1.source_id) {
const index = messagesWithoutDuplicates.findIndex(
m2 => m1.source_id === m2.source_id
);
if (index < 0) {
messagesWithoutDuplicates.push(m1);
}
} else {
messagesWithoutDuplicates.push(m1);
}
});
return messagesWithoutDuplicates;
};
/**
* Retrieves the last message from a conversation, prioritizing non-activity messages.
* @param {Object} m - The conversation object containing messages.
* @returns {Object} The last message of the conversation.
*/
export const getLastMessage = m => {
const lastMessageIncludingActivity = m.messages.at(-1);
const nonActivityMessages = m.messages.filter(
message => message.message_type !== 2
);
const lastNonActivityMessageInStore = nonActivityMessages.at(-1);
const lastNonActivityMessageFromAPI = m.last_non_activity_message;
// If API value and store value for last non activity message
// is empty, then return the last activity message
if (!lastNonActivityMessageInStore && !lastNonActivityMessageFromAPI) {
return lastMessageIncludingActivity;
}
return getLastNonActivityMessage(
lastNonActivityMessageInStore,
lastNonActivityMessageFromAPI
);
};
/**
* Filters messages that have been read by the agent.
* @param {Array} messages - The array of messages to filter.
* @param {number} agentLastSeenAt - The timestamp of when the agent last saw the messages.
* @returns {Array} An array of read messages.
*/
export const getReadMessages = (messages, agentLastSeenAt) => {
return messages.filter(
message => message.created_at * 1000 <= agentLastSeenAt * 1000
);
};
/**
* Filters messages that have not been read by the agent.
* @param {Array} messages - The array of messages to filter.
* @param {number} agentLastSeenAt - The timestamp of when the agent last saw the messages.
* @returns {Array} An array of unread messages.
*/
export const getUnreadMessages = (messages, agentLastSeenAt) => {
return messages.filter(
message => message.created_at * 1000 > agentLastSeenAt * 1000
);
};

View File

@@ -0,0 +1,101 @@
import {
filterDuplicateSourceMessages,
getLastMessage,
getReadMessages,
getUnreadMessages,
} from '../conversationHelper';
import {
conversationData,
lastMessageData,
readMessagesData,
unReadMessagesData,
} from './fixtures/conversationFixtures';
describe('conversationHelper', () => {
describe('#filterDuplicateSourceMessages', () => {
it('returns messages without duplicate source_id and all messages without source_id', () => {
const input = [
{ source_id: null, id: 1 },
{ source_id: '', id: 2 },
{ id: 3 },
{ source_id: 'wa_1', id: 4 },
{ source_id: 'wa_1', id: 5 },
{ source_id: 'wa_1', id: 6 },
{ source_id: 'wa_2', id: 7 },
{ source_id: 'wa_2', id: 8 },
{ source_id: 'wa_3', id: 9 },
];
const expected = [
{ source_id: null, id: 1 },
{ source_id: '', id: 2 },
{ id: 3 },
{ source_id: 'wa_1', id: 4 },
{ source_id: 'wa_2', id: 7 },
{ source_id: 'wa_3', id: 9 },
];
expect(filterDuplicateSourceMessages(input)).toEqual(expected);
});
});
describe('#readMessages', () => {
it('should return read messages if conversation is passed', () => {
expect(
getReadMessages(
conversationData.messages,
conversationData.agent_last_seen_at
)
).toEqual(readMessagesData);
});
});
describe('#unReadMessages', () => {
it('should return unread messages if conversation is passed', () => {
expect(
getUnreadMessages(
conversationData.messages,
conversationData.agent_last_seen_at
)
).toEqual(unReadMessagesData);
});
});
describe('#lastMessage', () => {
it("should return last activity message if both api and store doesn't have other messages", () => {
const testConversation = {
messages: [conversationData.messages[0]],
last_non_activity_message: null,
};
expect(getLastMessage(testConversation)).toEqual(
testConversation.messages[0]
);
});
it('should return message from store if store has latest message', () => {
const testConversation = {
messages: [],
last_non_activity_message: lastMessageData,
};
expect(getLastMessage(testConversation)).toEqual(lastMessageData);
});
it('should return last non activity message from store if api value is empty', () => {
const testConversation = {
messages: [conversationData.messages[0], conversationData.messages[1]],
last_non_activity_message: null,
};
expect(getLastMessage(testConversation)).toEqual(
testConversation.messages[1]
);
});
it("should return last non activity message from store if store doesn't have any messages", () => {
const testConversation = {
messages: [conversationData.messages[1], conversationData.messages[2]],
last_non_activity_message: conversationData.messages[0],
};
expect(getLastMessage(testConversation)).toEqual(
testConversation.messages[1]
);
});
});
});

View File

@@ -0,0 +1,185 @@
export const conversationData = {
meta: {
sender: {
additional_attributes: {
created_at_ip: '127.0.0.1',
},
availability_status: 'offline',
email: null,
id: 5017687,
name: 'long-flower-143',
phone_number: null,
thumbnail: '',
custom_attributes: {},
},
channel: 'Channel::WebWidget',
assignee: {
account_id: 1,
availability_status: 'offline',
confirmed: true,
email: 'muhsin@chatwoot.com',
available_name: 'Muhsin Keloth',
id: 21,
name: 'Muhsin Keloth',
role: 'administrator',
thumbnail: 'http://example.com/image.png',
},
},
id: 5815,
messages: [
{
id: 438072,
content: 'Campaign after 5 seconds',
account_id: 1,
inbox_id: 37,
conversation_id: 5811,
message_type: 1,
created_at: 1620980262,
updated_at: '2021-05-14T08:17:42.041Z',
private: false,
status: 'sent',
source_id: null,
content_type: null,
content_attributes: {},
sender_type: 'User',
sender_id: 1,
external_source_ids: {},
},
{
id: 4382131101,
content: 'Hello',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
{
id: 438100,
content: 'Hey',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
],
inbox_id: 37,
status: 'open',
muted: false,
can_reply: true,
timestamp: 1621144123,
contact_last_seen_at: 0,
agent_last_seen_at: 1621144123,
unread_count: 0,
additional_attributes: {
browser: {
device_name: 'Unknown',
browser_name: 'Chrome',
platform_name: 'macOS',
browser_version: '90.0.4430.212',
platform_version: '10.15.7',
},
widget_language: null,
browser_language: 'en',
},
account_id: 1,
labels: [],
};
export const lastMessageData = {
id: 438100,
content: 'Hey',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
};
export const readMessagesData = [
{
id: 438072,
content: 'Campaign after 5 seconds',
account_id: 1,
inbox_id: 37,
conversation_id: 5811,
message_type: 1,
created_at: 1620980262,
updated_at: '2021-05-14T08:17:42.041Z',
private: false,
status: 'sent',
source_id: null,
content_type: null,
content_attributes: {},
sender_type: 'User',
sender_id: 1,
external_source_ids: {},
},
];
export const unReadMessagesData = [
{
id: 4382131101,
content: 'Hello',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
{
id: 438100,
content: 'Hey',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
];

View File

@@ -1,68 +0,0 @@
const getLastNonActivityMessage = (messageInStore, messageFromAPI) => {
// If both API value and store value for last non activity message
// are available, then return the latest one.
if (messageInStore && messageFromAPI) {
if (messageInStore.created_at >= messageFromAPI.created_at) {
return messageInStore;
}
return messageFromAPI;
}
// Otherwise, return whichever is available
return messageInStore || messageFromAPI;
};
export const filterDuplicateSourceMessages = (messages = []) => {
const messagesWithoutDuplicates = [];
// We cannot use Map or any short hand method as it returns the last message with the duplicate ID
// We should return the message with smaller id when there is a duplicate
messages.forEach(m1 => {
if (m1.source_id) {
if (
messagesWithoutDuplicates.findIndex(
m2 => m1.source_id === m2.source_id
) < 0
) {
messagesWithoutDuplicates.push(m1);
}
} else {
messagesWithoutDuplicates.push(m1);
}
});
return messagesWithoutDuplicates;
};
export default {
methods: {
lastMessage(m) {
let lastMessageIncludingActivity = m.messages.last();
const nonActivityMessages = m.messages.filter(
message => message.message_type !== 2
);
let lastNonActivityMessageInStore = nonActivityMessages.last();
let lastNonActivityMessageFromAPI = m.last_non_activity_message;
// If API value and store value for last non activity message
// is empty, then return the last activity message
if (!lastNonActivityMessageInStore && !lastNonActivityMessageFromAPI) {
return lastMessageIncludingActivity;
}
return getLastNonActivityMessage(
lastNonActivityMessageInStore,
lastNonActivityMessageFromAPI
);
},
readMessages(messages, agentLastSeenAt) {
return messages.filter(
message => message.created_at * 1000 <= agentLastSeenAt * 1000
);
},
unReadMessages(messages, agentLastSeenAt) {
return messages.filter(
message => message.created_at * 1000 > agentLastSeenAt * 1000
);
},
},
};

View File

@@ -1,126 +0,0 @@
import conversationMixin, {
filterDuplicateSourceMessages,
} from '../conversations';
import conversationFixture from './conversationFixtures';
import commonHelpers from '../../helper/commons';
commonHelpers();
describe('#filterDuplicateSourceMessages', () => {
it('returns messages without duplicate source_id and all messages without source_id', () => {
expect(
filterDuplicateSourceMessages([
{ source_id: null, id: 1 },
{ source_id: '', id: 2 },
{ id: 3 },
{ source_id: 'wa_1', id: 4 },
{ source_id: 'wa_1', id: 5 },
{ source_id: 'wa_1', id: 6 },
{ source_id: 'wa_2', id: 7 },
{ source_id: 'wa_2', id: 8 },
{ source_id: 'wa_3', id: 9 },
])
).toEqual([
{ source_id: null, id: 1 },
{ source_id: '', id: 2 },
{ id: 3 },
{ source_id: 'wa_1', id: 4 },
{ source_id: 'wa_2', id: 7 },
{ source_id: 'wa_3', id: 9 },
]);
});
});
describe('#conversationMixin', () => {
it('should return read messages if conversation is passed', () => {
expect(
conversationMixin.methods.readMessages(
conversationFixture.conversation.messages,
conversationFixture.conversation.agent_last_seen_at
)
).toEqual(conversationFixture.readMessages);
});
it('should return read messages if conversation is passed', () => {
expect(
conversationMixin.methods.unReadMessages(
conversationFixture.conversation.messages,
conversationFixture.conversation.agent_last_seen_at
)
).toEqual(conversationFixture.unReadMessages);
});
describe('#lastMessage', () => {
it("should return last activity message if both api and store doesn't have other messages", () => {
const conversation = {
messages: [
{ id: 1, created_at: 1654333, message_type: 2, content: 'Hey' },
],
last_non_activity_message: null,
};
const { messages } = conversation;
expect(conversationMixin.methods.lastMessage(conversation)).toEqual(
messages[messages.length - 1]
);
});
it('should return message from store if store has latest message', () => {
const conversation = {
messages: [],
last_non_activity_message: {
id: 2,
created_at: 1654334,
message_type: 2,
content: 'Hey',
},
};
expect(conversationMixin.methods.lastMessage(conversation)).toEqual(
conversation.last_non_activity_message
);
});
it('should return last non activity message from store if api value is empty', () => {
const conversation = {
messages: [
{
id: 1,
created_at: 1654333,
message_type: 1,
content: 'Outgoing Message',
},
{ id: 2, created_at: 1654334, message_type: 2, content: 'Hey' },
],
last_non_activity_message: null,
};
expect(conversationMixin.methods.lastMessage(conversation)).toEqual(
conversation.messages[0]
);
});
it("should return last non activity message from store if store doesn't have any messages", () => {
const conversation = {
messages: [
{
id: 1,
created_at: 1654333,
message_type: 1,
content: 'Outgoing Message',
},
{
id: 3,
created_at: 1654335,
message_type: 0,
content: 'Incoming Message',
},
],
last_non_activity_message: {
id: 2,
created_at: 1654334,
message_type: 2,
content: 'Hey',
},
};
expect(conversationMixin.methods.lastMessage(conversation)).toEqual(
conversation.messages[1]
);
});
});
});

View File

@@ -1,185 +0,0 @@
export default {
conversation: {
meta: {
sender: {
additional_attributes: {
created_at_ip: '127.0.0.1',
},
availability_status: 'offline',
email: null,
id: 5017687,
name: 'long-flower-143',
phone_number: null,
thumbnail: '',
custom_attributes: {},
},
channel: 'Channel::WebWidget',
assignee: {
account_id: 1,
availability_status: 'offline',
confirmed: true,
email: 'muhsin@chatwoot.com',
available_name: 'Muhsin Keloth',
id: 21,
name: 'Muhsin Keloth',
role: 'administrator',
thumbnail:
'http://0.0.0.0:3000/rails/active_storage/representations/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBEQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--7b95641540fadebc733ec9b42117d00bc09600be/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCam9MY21WemFYcGxTU0lNTWpVd2VESTFNQVk2QmtWVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--c13bd5229b2a2a692444606e22f76ad61c634661/me.jpg',
},
},
id: 5815,
messages: [
{
id: 438072,
content: 'Campaign after 5 seconds',
account_id: 1,
inbox_id: 37,
conversation_id: 5811,
message_type: 1,
created_at: 1620980262,
updated_at: '2021-05-14T08:17:42.041Z',
private: false,
status: 'sent',
source_id: null,
content_type: null,
content_attributes: {},
sender_type: 'User',
sender_id: 1,
external_source_ids: {},
},
{
id: 4382131101,
content: 'Hello',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
{
id: 438100,
content: 'Hey',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
],
inbox_id: 37,
status: 'open',
muted: false,
can_reply: true,
timestamp: 1621144123,
contact_last_seen_at: 0,
agent_last_seen_at: 1621144123,
unread_count: 0,
additional_attributes: {
browser: {
device_name: 'Unknown',
browser_name: 'Chrome',
platform_name: 'macOS',
browser_version: '90.0.4430.212',
platform_version: '10.15.7',
},
widget_language: null,
browser_language: 'en',
},
account_id: 1,
labels: [],
},
lastMessage: {
id: 438100,
content: 'Hey',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
readMessages: [
{
id: 438072,
content: 'Campaign after 5 seconds',
account_id: 1,
inbox_id: 37,
conversation_id: 5811,
message_type: 1,
created_at: 1620980262,
updated_at: '2021-05-14T08:17:42.041Z',
private: false,
status: 'sent',
source_id: null,
content_type: null,
content_attributes: {},
sender_type: 'User',
sender_id: 1,
external_source_ids: {},
},
],
unReadMessages: [
{
id: 4382131101,
content: 'Hello',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
{
id: 438100,
content: 'Hey',
account_id: 1,
inbox_id: 37,
conversation_id: 5815,
message_type: 0,
created_at: 1621145476,
updated_at: '2021-05-16T05:48:43.910Z',
private: false,
status: 'sent',
source_id: null,
content_type: 'text',
content_attributes: {},
sender_type: null,
sender_id: null,
external_source_ids: {},
},
],
};