fix: Hide deleted messages on widget side (#2614)

This commit is contained in:
Muhsin Keloth 2021-07-15 14:27:37 +05:30 committed by GitHub
parent cf785123a5
commit b56512eb56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 222 additions and 54 deletions

View file

@ -63,7 +63,7 @@
</div> </div>
<div class="context-menu-wrap"> <div class="context-menu-wrap">
<context-menu <context-menu
v-if="isBubble && !isMessageDeleted" v-if="isBubble"
:is-open="showContextMenu" :is-open="showContextMenu"
:show-copy="hasText" :show-copy="hasText"
:menu-position="contextMenuPosition" :menu-position="contextMenuPosition"
@ -207,9 +207,6 @@ export default {
hasText() { hasText() {
return !!this.data.content; return !!this.data.content;
}, },
isMessageDeleted() {
return this.contentAttributes.deleted;
},
sentByMessage() { sentByMessage() {
const { sender } = this; const { sender } = this;
return this.data.message_type === 1 && !isEmptyObject(sender) return this.data.message_type === 1 && !isEmptyObject(sender)

View file

@ -65,6 +65,7 @@ import FileBubble from 'widget/components/FileBubble';
import Thumbnail from 'dashboard/components/widgets/Thumbnail'; import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import { MESSAGE_TYPE } from 'widget/helpers/constants'; import { MESSAGE_TYPE } from 'widget/helpers/constants';
import configMixin from '../mixins/configMixin'; import configMixin from '../mixins/configMixin';
import messageMixin from '../mixins/messageMixin';
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper'; import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
export default { export default {
name: 'AgentMessage', name: 'AgentMessage',
@ -75,7 +76,7 @@ export default {
UserMessage, UserMessage,
FileBubble, FileBubble,
}, },
mixins: [timeMixin, configMixin], mixins: [timeMixin, configMixin, messageMixin],
props: { props: {
message: { message: {
type: Object, type: Object,
@ -94,11 +95,6 @@ export default {
if (!this.message.content) return false; if (!this.message.content) return false;
return true; return true;
}, },
hasAttachments() {
return !!(
this.message.attachments && this.message.attachments.length > 0
);
},
readableTime() { readableTime() {
const { created_at: createdAt = '' } = this.message; const { created_at: createdAt = '' } = this.message;
return this.messageStamp(createdAt, 'LLL d yyyy, h:mm a'); return this.messageStamp(createdAt, 'LLL d yyyy, h:mm a');
@ -111,10 +107,6 @@ export default {
const { content_type: type = '' } = this.message; const { content_type: type = '' } = this.message;
return type; return type;
}, },
messageContentAttributes() {
const { content_attributes: attribute = {} } = this.message;
return attribute;
},
agentName() { agentName() {
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) { if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
return 'Bot'; return 'Bot';
@ -153,9 +145,8 @@ export default {
if (this.messageContentAttributes.submitted_values) { if (this.messageContentAttributes.submitted_values) {
if (this.contentType === 'input_select') { if (this.contentType === 'input_select') {
const [ const [selectionOption = {}] =
selectionOption = {}, this.messageContentAttributes.submitted_values;
] = this.messageContentAttributes.submitted_values;
return { content: selectionOption.title || selectionOption.value }; return { content: selectionOption.title || selectionOption.value };
} }
} }

View file

@ -37,6 +37,7 @@ import UserMessageBubble from 'widget/components/UserMessageBubble';
import ImageBubble from 'widget/components/ImageBubble'; import ImageBubble from 'widget/components/ImageBubble';
import FileBubble from 'widget/components/FileBubble'; import FileBubble from 'widget/components/FileBubble';
import timeMixin from 'dashboard/mixins/time'; import timeMixin from 'dashboard/mixins/time';
import messageMixin from '../mixins/messageMixin';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
@ -46,7 +47,7 @@ export default {
ImageBubble, ImageBubble,
FileBubble, FileBubble,
}, },
mixins: [timeMixin], mixins: [timeMixin, messageMixin],
props: { props: {
message: { message: {
type: Object, type: Object,
@ -62,11 +63,6 @@ export default {
const { status = '' } = this.message; const { status = '' } = this.message;
return status === 'in_progress'; return status === 'in_progress';
}, },
hasAttachments() {
return !!(
this.message.attachments && this.message.attachments.length > 0
);
},
showTextBubble() { showTextBubble() {
const { message } = this; const { message } = this;
return !!message.content; return !!message.content;

View file

@ -18,13 +18,15 @@ class ActionCableConnector extends BaseActionCableConnector {
}; };
onMessageCreated = data => { onMessageCreated = data => {
this.app.$store.dispatch('conversation/addMessage', data).then(() => { this.app.$store
window.bus.$emit('on-agent-message-recieved'); .dispatch('conversation/addOrUpdateMessage', data)
}); .then(() => {
window.bus.$emit('on-agent-message-recieved');
});
}; };
onMessageUpdated = data => { onMessageUpdated = data => {
this.app.$store.dispatch('conversation/updateMessage', data); this.app.$store.dispatch('conversation/addOrUpdateMessage', data);
}; };
onPresenceUpdate = data => { onPresenceUpdate = data => {

View file

@ -0,0 +1,13 @@
export default {
computed: {
messageContentAttributes() {
const { content_attributes: attribute = {} } = this.message;
return attribute;
},
hasAttachments() {
return !!(
this.message.attachments && this.message.attachments.length > 0
);
},
},
};

View file

@ -0,0 +1,27 @@
export default {
nonDeletedMessage: {
content: 'Hey',
content_attributes: {},
attachments: [
{
data_url: 'https://assets.com/kseb-bill.pdf',
extension: null,
file_type: 'file',
},
],
content_type: 'text',
conversation_id: 1,
created_at: 1626111625,
id: 1,
message_type: 0,
},
deletedMessage: {
content: 'This message was deleted',
content_attributes: { deleted: true },
content_type: null,
conversation_id: 1,
created_at: 1626111634,
id: 2,
message_type: 1,
},
};

View file

@ -0,0 +1,37 @@
import { shallowMount } from '@vue/test-utils';
import messageMixin from '../messageMixin';
import messages from './messageFixture';
describe('messageMixin', () => {
let Component = {
render() {},
title: 'TestComponent',
mixins: [messageMixin],
};
it('deleted messages', async () => {
const wrapper = shallowMount(Component, {
data() {
return {
message: messages.deletedMessage,
};
},
});
expect(wrapper.vm.messageContentAttributes).toEqual({
deleted: true,
});
expect(wrapper.vm.hasAttachments).toBe(false);
});
it('non-deleted messages', async () => {
const wrapper = shallowMount(Component, {
data() {
return {
message: messages.nonDeletedMessage,
};
},
});
expect(wrapper.vm.deletedMessage).toBe(undefined);
expect(wrapper.vm.messageContentAttributes).toEqual({});
expect(wrapper.vm.hasAttachments).toBe(true);
});
});

View file

@ -8,7 +8,7 @@ import {
} from 'widget/api/conversation'; } from 'widget/api/conversation';
import { refreshActionCableConnector } from '../../../helpers/actionCable'; import { refreshActionCableConnector } from '../../../helpers/actionCable';
import { createTemporaryMessage } from './helpers'; import { createTemporaryMessage, getNonDeletedMessages } from './helpers';
export const actions = { export const actions = {
createConversation: async ({ commit, dispatch }, params) => { createConversation: async ({ commit, dispatch }, params) => {
@ -60,12 +60,12 @@ export const actions = {
// Show error // Show error
} }
}, },
fetchOldConversations: async ({ commit }, { before } = {}) => { fetchOldConversations: async ({ commit }, { before } = {}) => {
try { try {
commit('setConversationListLoading', true); commit('setConversationListLoading', true);
const { data } = await getMessagesAPI({ before }); const { data } = await getMessagesAPI({ before });
commit('setMessagesInConversation', data); const formattedMessages = getNonDeletedMessages({ messages: data });
commit('setMessagesInConversation', formattedMessages);
commit('setConversationListLoading', false); commit('setConversationListLoading', false);
} catch (error) { } catch (error) {
commit('setConversationListLoading', false); commit('setConversationListLoading', false);
@ -76,11 +76,12 @@ export const actions = {
commit('clearConversations'); commit('clearConversations');
}, },
addMessage: async ({ commit }, data) => { addOrUpdateMessage: async ({ commit }, data) => {
commit('pushMessageToConversation', data); const { id, content_attributes } = data;
}, if (content_attributes && content_attributes.deleted) {
commit('deleteMessage', id);
updateMessage({ commit }, data) { return;
}
commit('pushMessageToConversation', data); commit('pushMessageToConversation', data);
}, },

View file

@ -46,3 +46,9 @@ export const findUndeliveredMessage = (messageInbox, { content }) =>
Object.values(messageInbox).filter( Object.values(messageInbox).filter(
message => message.content === content && message.status === 'in_progress' message => message.content === content && message.status === 'in_progress'
); );
export const getNonDeletedMessages = ({ messages }) => {
return messages.filter(
item => !(item.content_attributes && item.content_attributes.deleted)
);
};

View file

@ -8,6 +8,7 @@ export const mutations = {
}, },
pushMessageToConversation($state, message) { pushMessageToConversation($state, message) {
const { id, status, message_type: type } = message; const { id, status, message_type: type } = message;
const messagesInbox = $state.conversations; const messagesInbox = $state.conversations;
const isMessageIncoming = type === MESSAGE_TYPE.INCOMING; const isMessageIncoming = type === MESSAGE_TYPE.INCOMING;
const isTemporaryMessage = status === 'in_progress'; const isTemporaryMessage = status === 'in_progress';
@ -71,6 +72,11 @@ export const mutations = {
}; };
}, },
deleteMessage($state, id) {
const messagesInbox = $state.conversations;
Vue.delete(messagesInbox, id);
},
toggleAgentTypingStatus($state, { status }) { toggleAgentTypingStatus($state, { status }) {
const isTyping = status === 'on'; const isTyping = status === 'on';
$state.uiFlags.isAgentTyping = isTyping; $state.uiFlags.isAgentTyping = isTyping;

View file

@ -8,21 +8,6 @@ jest.mock('widget/helpers/axios');
const commit = jest.fn(); const commit = jest.fn();
describe('#actions', () => { describe('#actions', () => {
describe('#addMessage', () => {
it('sends correct mutations', () => {
actions.addMessage({ commit }, { id: 1 });
expect(commit).toBeCalledWith('pushMessageToConversation', { id: 1 });
});
it('plays audio when agent sends a message', () => {
actions.addMessage({ commit }, { id: 1, message_type: 1 });
expect(commit).toBeCalledWith('pushMessageToConversation', {
id: 1,
message_type: 1,
});
});
});
describe('#createConversation', () => { describe('#createConversation', () => {
it('sends correct mutations', async () => { it('sends correct mutations', async () => {
API.post.mockResolvedValue({ API.post.mockResolvedValue({
@ -60,10 +45,40 @@ describe('#actions', () => {
}); });
}); });
describe('#updateMessage', () => { describe('#addOrUpdateMessage', () => {
it('sends correct mutations', () => { it('sends correct actions for non-deleted message', () => {
actions.updateMessage({ commit }, { id: 1 }); actions.addOrUpdateMessage(
expect(commit).toBeCalledWith('pushMessageToConversation', { id: 1 }); { commit },
{
id: 1,
content: 'Hey',
content_attributes: {},
}
);
expect(commit).toBeCalledWith('pushMessageToConversation', {
id: 1,
content: 'Hey',
content_attributes: {},
});
});
it('sends correct actions for non-deleted message', () => {
actions.addOrUpdateMessage(
{ commit },
{
id: 1,
content: 'Hey',
content_attributes: { deleted: true },
}
);
expect(commit).toBeCalledWith('deleteMessage', 1);
});
it('plays audio when agent sends a message', () => {
actions.addOrUpdateMessage({ commit }, { id: 1, message_type: 1 });
expect(commit).toBeCalledWith('pushMessageToConversation', {
id: 1,
message_type: 1,
});
}); });
}); });
@ -160,4 +175,38 @@ describe('#actions', () => {
expect(commit).toBeCalledWith('clearConversations'); expect(commit).toBeCalledWith('clearConversations');
}); });
}); });
describe('#fetchOldConversations', () => {
it('sends correct actions', async () => {
API.get.mockResolvedValue({
data: [
{
id: 1,
text: 'hey',
content_attributes: {},
},
{
id: 2,
text: 'welcome',
content_attributes: { deleted: true },
},
],
});
await actions.fetchOldConversations({ commit }, {});
expect(commit.mock.calls).toEqual([
['setConversationListLoading', true],
[
'setMessagesInConversation',
[
{
id: 1,
text: 'hey',
content_attributes: {},
},
],
],
['setConversationListLoading', false],
]);
});
});
}); });

View file

@ -1,6 +1,7 @@
import { import {
findUndeliveredMessage, findUndeliveredMessage,
createTemporaryMessage, createTemporaryMessage,
getNonDeletedMessages,
} from '../../conversation/helpers'; } from '../../conversation/helpers';
describe('#findUndeliveredMessage', () => { describe('#findUndeliveredMessage', () => {
@ -35,3 +36,37 @@ describe('#createTemporaryMessage', () => {
expect(message.status).toBe('in_progress'); expect(message.status).toBe('in_progress');
}); });
}); });
describe('#getNonDeletedMessages', () => {
it('returns non-deleted messages', () => {
const messages = [
{
id: 1,
content: 'Hello',
content_attributes: {},
},
{
id: 2,
content: 'Hey',
content_attributes: { deleted: true },
},
{
id: 3,
content: 'How may I help you',
content_attributes: {},
},
];
expect(getNonDeletedMessages({ messages })).toStrictEqual([
{
id: 1,
content: 'Hello',
content_attributes: {},
},
{
id: 3,
content: 'How may I help you',
content_attributes: {},
},
]);
});
});

View file

@ -175,4 +175,12 @@ describe('#mutations', () => {
expect(state.conversations).toEqual({}); expect(state.conversations).toEqual({});
}); });
}); });
describe('#deleteMessage', () => {
it('delete the message from conversation', () => {
const state = { conversations: { 1: { id: 1 } } };
mutations.deleteMessage(state, 1);
expect(state.conversations).toEqual({});
});
});
}); });