Chore: Message to support multiple attachments (#730)
* Changes for the message to have multiple attachments * changed the message association to attachments from has_one to has_many * changed all the references of this association in building and fetching to reflect this change * Added number of attachments validation to the message model * Modified the backend responses and endpoints to reflect multiple attachment support (#737) * Changing the frontend components for multiple attachments * changed the request structure to reflect the multiple attachment structures * changed the message bubbles to support multiple attachments * bugfix: agent side attachment was not showing because of a missing await * broken message was shown because of the store filtering * Added documentation for ImageMagick * spec fixes * refactored code to reflect more apt namings * Added updated message listener for the dashboard (#727) * Added the publishing for message updated event * Implemented the listener for dashboard Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
@@ -22,7 +22,7 @@ class MessageApi extends ApiClient {
|
||||
|
||||
sendAttachment([conversationId, { file }]) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment[file]', file);
|
||||
formData.append('attachments[]', file, file.name);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
|
||||
@@ -105,15 +105,17 @@ export default {
|
||||
router.push({ path: frontendURL(path) });
|
||||
},
|
||||
extractMessageText(chatItem) {
|
||||
if (chatItem.content) {
|
||||
return chatItem.content;
|
||||
const { content, attachments } = chatItem;
|
||||
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
let fileType = '';
|
||||
if (chatItem.attachment) {
|
||||
fileType = chatItem.attachment.file_type;
|
||||
} else {
|
||||
if (!attachments) {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
const [attachment] = attachments;
|
||||
const { file_type: fileType } = attachment;
|
||||
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
|
||||
return `
|
||||
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<li v-if="data.attachment || data.content" :class="alignBubble">
|
||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||
<div :class="wrapClass">
|
||||
<p v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<bubble-image
|
||||
v-if="data.attachment && data.attachment.file_type === 'image'"
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-file
|
||||
v-if="data.attachment && data.attachment.file_type !== 'image'"
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<span v-if="hasAttachments">
|
||||
<span v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<bubble-image
|
||||
v-if="attachment.file_type === 'image'"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-file
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isPrivate"
|
||||
v-tooltip.top-start="toolTipMessage"
|
||||
@@ -71,10 +75,16 @@ export default {
|
||||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
hasImageAttachment() {
|
||||
const { attachment = {} } = this.data;
|
||||
const { file_type: fileType } = attachment;
|
||||
return fileType === 'image';
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === 'image';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isPrivate() {
|
||||
return this.data.private;
|
||||
|
||||
@@ -6,6 +6,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
super(app, pubsubToken);
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
'message.updated': this.onMessageUpdated,
|
||||
'conversation.created': this.onConversationCreated,
|
||||
'status_change:conversation': this.onStatusChange,
|
||||
'user:logout': this.onLogout,
|
||||
@@ -14,6 +15,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
};
|
||||
}
|
||||
|
||||
onMessageUpdated = data => {
|
||||
this.app.$store.dispatch('updateMessage', data);
|
||||
};
|
||||
|
||||
onAssigneeChanged = payload => {
|
||||
const { meta = {}, id } = payload;
|
||||
const { assignee } = meta || {};
|
||||
|
||||
@@ -145,6 +145,10 @@ const actions = {
|
||||
commit(types.default.ADD_MESSAGE, message);
|
||||
},
|
||||
|
||||
updateMessage({ commit }, message) {
|
||||
commit(types.default.ADD_MESSAGE, message);
|
||||
},
|
||||
|
||||
addConversation({ commit }, conversation) {
|
||||
commit(types.default.ADD_CONVERSATION, conversation);
|
||||
},
|
||||
@@ -192,7 +196,7 @@ const actions = {
|
||||
|
||||
sendAttachment: async ({ commit }, data) => {
|
||||
try {
|
||||
const response = MessageApi.sendAttachment(data);
|
||||
const response = await MessageApi.sendAttachment(data);
|
||||
commit(types.default.SEND_MESSAGE, response.data);
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
|
||||
@@ -122,12 +122,13 @@ const mutations = {
|
||||
_state.selectedChat.status = status;
|
||||
},
|
||||
|
||||
[types.default.SEND_MESSAGE](_state, data) {
|
||||
[types.default.SEND_MESSAGE](_state, currentMessage) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
const previousMessageIds = chat.messages.map(m => m.id);
|
||||
if (!previousMessageIds.includes(data.id)) {
|
||||
chat.messages.push(data);
|
||||
}
|
||||
const allMessagesExceptCurrent = (chat.messages || []).filter(
|
||||
message => message.id !== currentMessage.id
|
||||
);
|
||||
allMessagesExceptCurrent.push(currentMessage);
|
||||
chat.messages = allMessagesExceptCurrent;
|
||||
},
|
||||
|
||||
[types.default.ADD_MESSAGE](_state, message) {
|
||||
@@ -135,12 +136,16 @@ const mutations = {
|
||||
c => c.id === message.conversation_id
|
||||
);
|
||||
if (!chat) return;
|
||||
const previousMessageIds = chat.messages.map(m => m.id);
|
||||
if (!previousMessageIds.includes(message.id)) {
|
||||
const previousMessageIndex = chat.messages.findIndex(
|
||||
m => m.id === message.id
|
||||
);
|
||||
if (previousMessageIndex === -1) {
|
||||
chat.messages.push(message);
|
||||
if (_state.selectedChat.id === message.conversation_id) {
|
||||
window.bus.$emit('scrollToMessage');
|
||||
}
|
||||
} else {
|
||||
chat.messages[previousMessageIndex] = message;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const sendMessageAPI = async content => {
|
||||
};
|
||||
|
||||
const sendAttachmentAPI = async attachment => {
|
||||
const urlData = endPoints.sendAttachmnet(attachment);
|
||||
const urlData = endPoints.sendAttachment(attachment);
|
||||
const result = await API.post(urlData.url, urlData.params);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -9,14 +9,13 @@ const sendMessage = content => ({
|
||||
},
|
||||
});
|
||||
|
||||
const sendAttachmnet = ({ attachment }) => {
|
||||
const sendAttachment = ({ attachment }) => {
|
||||
const { refererURL = '' } = window;
|
||||
const timestamp = new Date().toString();
|
||||
const { file, file_type: fileType } = attachment;
|
||||
const { file } = attachment;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('message[attachment][file]', file);
|
||||
formData.append('message[attachment][file_type]', fileType);
|
||||
formData.append('message[attachments][]', file, file.name);
|
||||
formData.append('message[referer_url]', refererURL);
|
||||
formData.append('message[timestamp]', timestamp);
|
||||
return {
|
||||
@@ -43,7 +42,7 @@ const getAvailableAgents = token => ({
|
||||
|
||||
export default {
|
||||
sendMessage,
|
||||
sendAttachmnet,
|
||||
sendAttachment,
|
||||
getConversation,
|
||||
updateMessage,
|
||||
getAvailableAgents,
|
||||
|
||||
@@ -11,26 +11,26 @@
|
||||
</div>
|
||||
<div class="message-wrap">
|
||||
<AgentMessageBubble
|
||||
v-if="showTextBubble && shouldDisplayAgentMessage"
|
||||
v-if="!hasAttachments && shouldDisplayAgentMessage"
|
||||
:content-type="contentType"
|
||||
:message-content-attributes="messageContentAttributes"
|
||||
:message-id="message.id"
|
||||
:message-type="messageType"
|
||||
:message="message.content"
|
||||
/>
|
||||
<div v-if="hasAttachment" class="chat-bubble has-attachment agent">
|
||||
<file-bubble
|
||||
v-if="
|
||||
message.attachment && message.attachment.file_type !== 'image'
|
||||
"
|
||||
:url="message.attachment.data_url"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
:url="message.attachment.data_url"
|
||||
:thumb="message.attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<div v-if="hasAttachments" class="chat-bubble has-attachment agent">
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||
<file-bubble
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
:url="attachment.data_url"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
:thumb="attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
|
||||
{{ agentName }}
|
||||
@@ -78,12 +78,10 @@ export default {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
hasAttachment() {
|
||||
return !!this.message.attachment;
|
||||
},
|
||||
showTextBubble() {
|
||||
const { message } = this;
|
||||
return !message.attachment;
|
||||
hasAttachments() {
|
||||
return !!(
|
||||
this.message.attachments && this.message.attachments.length > 0
|
||||
);
|
||||
},
|
||||
readableTime() {
|
||||
const { created_at: createdAt = '' } = this.message;
|
||||
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.time {
|
||||
|
||||
@@ -6,18 +6,20 @@
|
||||
:message="message.content"
|
||||
:status="message.status"
|
||||
/>
|
||||
<div v-if="hasAttachment" class="chat-bubble has-attachment user">
|
||||
<file-bubble
|
||||
v-if="message.attachment && message.attachment.file_type !== 'image'"
|
||||
:url="message.attachment.data_url"
|
||||
:is-in-progress="isInProgress"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
:url="message.attachment.data_url"
|
||||
:thumb="message.attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<div v-if="hasAttachments" class="chat-bubble has-attachment user">
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||
<file-bubble
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
:url="attachment.data_url"
|
||||
:is-in-progress="isInProgress"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
:thumb="attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,8 +50,10 @@ export default {
|
||||
const { status = '' } = this.message;
|
||||
return status === 'in_progress';
|
||||
},
|
||||
hasAttachment() {
|
||||
return !!this.message.attachment;
|
||||
hasAttachments() {
|
||||
return !!(
|
||||
this.message.attachments && this.message.attachments.length > 0
|
||||
);
|
||||
},
|
||||
showTextBubble() {
|
||||
const { message } = this;
|
||||
|
||||
@@ -5,12 +5,17 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
super(app, pubsubToken);
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
'message.updated': this.onMessageUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
onMessageCreated = data => {
|
||||
this.app.$store.dispatch('conversation/addMessage', data);
|
||||
};
|
||||
|
||||
onMessageUpdated = data => {
|
||||
this.app.$store.dispatch('conversation/updateMessage', data);
|
||||
};
|
||||
}
|
||||
|
||||
export const refreshActionCableConnector = pubsubToken => {
|
||||
|
||||
@@ -12,12 +12,12 @@ import DateHelper from '../../../shared/helpers/DateHelper';
|
||||
|
||||
const groupBy = require('lodash.groupby');
|
||||
|
||||
export const createTemporaryMessage = ({ attachment, content }) => {
|
||||
export const createTemporaryMessage = ({ attachments, content }) => {
|
||||
const timestamp = new Date().getTime() / 1000;
|
||||
return {
|
||||
id: getUuid(),
|
||||
content,
|
||||
attachment,
|
||||
attachments,
|
||||
status: 'in_progress',
|
||||
created_at: timestamp,
|
||||
message_type: MESSAGE_TYPE.INCOMING,
|
||||
@@ -97,11 +97,14 @@ export const actions = {
|
||||
file_type: fileType,
|
||||
status: 'in_progress',
|
||||
};
|
||||
const tempMessage = createTemporaryMessage({ attachment });
|
||||
const tempMessage = createTemporaryMessage({ attachments: [attachment] });
|
||||
commit('pushMessageToConversation', tempMessage);
|
||||
try {
|
||||
const { data } = await sendAttachmentAPI(params);
|
||||
commit('setMessageStatus', { message: data, tempId: tempMessage.id });
|
||||
commit('updateAttachmentMessageStatus', {
|
||||
message: data,
|
||||
tempId: tempMessage.id,
|
||||
});
|
||||
} catch (error) {
|
||||
// Show error
|
||||
}
|
||||
@@ -125,6 +128,10 @@ export const actions = {
|
||||
|
||||
commit('pushMessageToConversation', data);
|
||||
},
|
||||
|
||||
updateMessage({ commit }, data) {
|
||||
commit('pushMessageToConversation', data);
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -151,24 +158,15 @@ export const mutations = {
|
||||
}
|
||||
},
|
||||
|
||||
setMessageStatus($state, { message, tempId }) {
|
||||
const { status, id } = message;
|
||||
updateAttachmentMessageStatus($state, { message, tempId }) {
|
||||
const { id } = message;
|
||||
const messagesInbox = $state.conversations;
|
||||
|
||||
const messageInConversation = messagesInbox[tempId];
|
||||
|
||||
if (messageInConversation) {
|
||||
Vue.delete(messagesInbox, tempId);
|
||||
const { attachment } = messageInConversation;
|
||||
if (attachment.file_type === 'file') {
|
||||
attachment.data_url = message.attachment.data_url;
|
||||
}
|
||||
Vue.set(messagesInbox, id, {
|
||||
...messageInConversation,
|
||||
attachment,
|
||||
id,
|
||||
status,
|
||||
});
|
||||
Vue.set(messagesInbox, id, { ...message });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -26,6 +26,13 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateMessage', () => {
|
||||
it('sends correct mutations', () => {
|
||||
actions.updateMessage({ commit }, { id: 1 });
|
||||
expect(commit).toBeCalledWith('pushMessageToConversation', { id: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendMessage', () => {
|
||||
it('sends correct mutations', () => {
|
||||
const mockDate = new Date(1466424490000);
|
||||
@@ -59,12 +66,14 @@ describe('#actions', () => {
|
||||
status: 'in_progress',
|
||||
created_at: 1466424490,
|
||||
message_type: 0,
|
||||
attachment: {
|
||||
thumb_url: '',
|
||||
data_url: '',
|
||||
file_type: 'file',
|
||||
status: 'in_progress',
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
thumb_url: '',
|
||||
data_url: '',
|
||||
file_type: 'file',
|
||||
status: 'in_progress',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setMessageStatus', () => {
|
||||
describe('#updateAttachmentMessageStatus', () => {
|
||||
it('Updates status of loading messages if payload is not empty', () => {
|
||||
const state = {
|
||||
conversations: {
|
||||
@@ -113,12 +113,18 @@ describe('#mutations', () => {
|
||||
id: '1',
|
||||
content: '',
|
||||
status: 'sent',
|
||||
attachment: {
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
message_type: 0,
|
||||
attachments: [
|
||||
{
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
],
|
||||
};
|
||||
mutations.setMessageStatus(state, { message, tempId: 'rand_id_123' });
|
||||
mutations.updateAttachmentMessageStatus(state, {
|
||||
message,
|
||||
tempId: 'rand_id_123',
|
||||
});
|
||||
|
||||
expect(state.conversations).toEqual({
|
||||
1: {
|
||||
@@ -126,10 +132,12 @@ describe('#mutations', () => {
|
||||
content: '',
|
||||
message_type: 0,
|
||||
status: 'sent',
|
||||
attachment: {
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
file: '',
|
||||
file_type: 'image',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user