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:
parent
0817414957
commit
818c769bb7
31 changed files with 212 additions and 137 deletions
|
@ -41,7 +41,7 @@ class Messages::MessageBuilder
|
||||||
def build_message
|
def build_message
|
||||||
@message = conversation.messages.create!(message_params)
|
@message = conversation.messages.create!(message_params)
|
||||||
(response.attachments || []).each do |attachment|
|
(response.attachments || []).each do |attachment|
|
||||||
attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
|
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||||
attachment_obj.save!
|
attachment_obj.save!
|
||||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,19 +10,22 @@ class Messages::Outgoing::NormalBuilder
|
||||||
@fb_id = params[:fb_id]
|
@fb_id = params[:fb_id]
|
||||||
@content_type = params[:content_type]
|
@content_type = params[:content_type]
|
||||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||||
@attachment = params[:attachment]
|
@attachments = params[:attachments]
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@message = @conversation.messages.build(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
if @attachment
|
@message.save
|
||||||
@message.attachment = Attachment.new(
|
if @attachments.present?
|
||||||
account_id: message.account_id,
|
@attachments.each do |uploaded_attachment|
|
||||||
file_type: file_type(@attachment[:file]&.content_type)
|
attachment = @message.attachments.new(
|
||||||
|
account_id: @message.account_id,
|
||||||
|
file_type: file_type(uploaded_attachment&.content_type)
|
||||||
)
|
)
|
||||||
@message.attachment.file.attach(@attachment[:file])
|
attachment.file.attach(uploaded_attachment)
|
||||||
end
|
end
|
||||||
@message.save
|
@message.save
|
||||||
|
end
|
||||||
@message
|
@message
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@message = conversation.messages.new(message_params)
|
@message = conversation.messages.new(message_params)
|
||||||
|
@message.save
|
||||||
build_attachment
|
build_attachment
|
||||||
@message.save!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -28,13 +28,16 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_attachment
|
def build_attachment
|
||||||
return if params[:message][:attachment].blank?
|
return if params[:message][:attachments].blank?
|
||||||
|
|
||||||
@message.attachment = Attachment.new(
|
params[:message][:attachments].each do |uploaded_attachment|
|
||||||
|
attachment = @message.attachments.new(
|
||||||
account_id: @message.account_id,
|
account_id: @message.account_id,
|
||||||
file_type: helpers.file_type(params[:message][:attachment][:file]&.content_type)
|
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||||
)
|
)
|
||||||
@message.attachment.file.attach(params[:message][:attachment][:file])
|
attachment.file.attach(uploaded_attachment)
|
||||||
|
end
|
||||||
|
@message.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_conversation
|
def set_conversation
|
||||||
|
|
|
@ -11,7 +11,7 @@ class MessageFinder
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation_messages
|
def conversation_messages
|
||||||
@conversation.messages.includes(:attachment, user: { avatar_attachment: :blob })
|
@conversation.messages.includes(:attachments, user: { avatar_attachment: :blob })
|
||||||
end
|
end
|
||||||
|
|
||||||
def messages
|
def messages
|
||||||
|
|
|
@ -22,7 +22,7 @@ class MessageApi extends ApiClient {
|
||||||
|
|
||||||
sendAttachment([conversationId, { file }]) {
|
sendAttachment([conversationId, { file }]) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('attachment[file]', file);
|
formData.append('attachments[]', file, file.name);
|
||||||
return axios({
|
return axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `${this.url}/${conversationId}/messages`,
|
url: `${this.url}/${conversationId}/messages`,
|
||||||
|
|
|
@ -105,15 +105,17 @@ export default {
|
||||||
router.push({ path: frontendURL(path) });
|
router.push({ path: frontendURL(path) });
|
||||||
},
|
},
|
||||||
extractMessageText(chatItem) {
|
extractMessageText(chatItem) {
|
||||||
if (chatItem.content) {
|
const { content, attachments } = chatItem;
|
||||||
return chatItem.content;
|
|
||||||
|
if (content) {
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
let fileType = '';
|
if (!attachments) {
|
||||||
if (chatItem.attachment) {
|
|
||||||
fileType = chatItem.attachment.file_type;
|
|
||||||
} else {
|
|
||||||
return ' ';
|
return ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [attachment] = attachments;
|
||||||
|
const { file_type: fileType } = attachment;
|
||||||
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
|
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
|
||||||
return `
|
return `
|
||||||
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
|
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<li v-if="data.attachment || data.content" :class="alignBubble">
|
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||||
<div :class="wrapClass">
|
<div :class="wrapClass">
|
||||||
<p v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
<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
|
<bubble-text
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
:message="message"
|
:message="message"
|
||||||
:readable-time="readableTime"
|
: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
|
<i
|
||||||
v-if="isPrivate"
|
v-if="isPrivate"
|
||||||
v-tooltip.top-start="toolTipMessage"
|
v-tooltip.top-start="toolTipMessage"
|
||||||
|
@ -71,10 +75,16 @@ export default {
|
||||||
isBubble() {
|
isBubble() {
|
||||||
return [0, 1, 3].includes(this.data.message_type);
|
return [0, 1, 3].includes(this.data.message_type);
|
||||||
},
|
},
|
||||||
|
hasAttachments() {
|
||||||
|
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||||
|
},
|
||||||
hasImageAttachment() {
|
hasImageAttachment() {
|
||||||
const { attachment = {} } = this.data;
|
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||||
const { file_type: fileType } = attachment;
|
const { attachments = [{}] } = this.data;
|
||||||
|
const { file_type: fileType } = attachments[0];
|
||||||
return fileType === 'image';
|
return fileType === 'image';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
isPrivate() {
|
isPrivate() {
|
||||||
return this.data.private;
|
return this.data.private;
|
||||||
|
|
|
@ -6,6 +6,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
super(app, pubsubToken);
|
super(app, pubsubToken);
|
||||||
this.events = {
|
this.events = {
|
||||||
'message.created': this.onMessageCreated,
|
'message.created': this.onMessageCreated,
|
||||||
|
'message.updated': this.onMessageUpdated,
|
||||||
'conversation.created': this.onConversationCreated,
|
'conversation.created': this.onConversationCreated,
|
||||||
'status_change:conversation': this.onStatusChange,
|
'status_change:conversation': this.onStatusChange,
|
||||||
'user:logout': this.onLogout,
|
'user:logout': this.onLogout,
|
||||||
|
@ -14,6 +15,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMessageUpdated = data => {
|
||||||
|
this.app.$store.dispatch('updateMessage', data);
|
||||||
|
};
|
||||||
|
|
||||||
onAssigneeChanged = payload => {
|
onAssigneeChanged = payload => {
|
||||||
const { meta = {}, id } = payload;
|
const { meta = {}, id } = payload;
|
||||||
const { assignee } = meta || {};
|
const { assignee } = meta || {};
|
||||||
|
|
|
@ -145,6 +145,10 @@ const actions = {
|
||||||
commit(types.default.ADD_MESSAGE, message);
|
commit(types.default.ADD_MESSAGE, message);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateMessage({ commit }, message) {
|
||||||
|
commit(types.default.ADD_MESSAGE, message);
|
||||||
|
},
|
||||||
|
|
||||||
addConversation({ commit }, conversation) {
|
addConversation({ commit }, conversation) {
|
||||||
commit(types.default.ADD_CONVERSATION, conversation);
|
commit(types.default.ADD_CONVERSATION, conversation);
|
||||||
},
|
},
|
||||||
|
@ -192,7 +196,7 @@ const actions = {
|
||||||
|
|
||||||
sendAttachment: async ({ commit }, data) => {
|
sendAttachment: async ({ commit }, data) => {
|
||||||
try {
|
try {
|
||||||
const response = MessageApi.sendAttachment(data);
|
const response = await MessageApi.sendAttachment(data);
|
||||||
commit(types.default.SEND_MESSAGE, response.data);
|
commit(types.default.SEND_MESSAGE, response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle error
|
// Handle error
|
||||||
|
|
|
@ -122,12 +122,13 @@ const mutations = {
|
||||||
_state.selectedChat.status = status;
|
_state.selectedChat.status = status;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SEND_MESSAGE](_state, data) {
|
[types.default.SEND_MESSAGE](_state, currentMessage) {
|
||||||
const [chat] = getSelectedChatConversation(_state);
|
const [chat] = getSelectedChatConversation(_state);
|
||||||
const previousMessageIds = chat.messages.map(m => m.id);
|
const allMessagesExceptCurrent = (chat.messages || []).filter(
|
||||||
if (!previousMessageIds.includes(data.id)) {
|
message => message.id !== currentMessage.id
|
||||||
chat.messages.push(data);
|
);
|
||||||
}
|
allMessagesExceptCurrent.push(currentMessage);
|
||||||
|
chat.messages = allMessagesExceptCurrent;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.ADD_MESSAGE](_state, message) {
|
[types.default.ADD_MESSAGE](_state, message) {
|
||||||
|
@ -135,12 +136,16 @@ const mutations = {
|
||||||
c => c.id === message.conversation_id
|
c => c.id === message.conversation_id
|
||||||
);
|
);
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
const previousMessageIds = chat.messages.map(m => m.id);
|
const previousMessageIndex = chat.messages.findIndex(
|
||||||
if (!previousMessageIds.includes(message.id)) {
|
m => m.id === message.id
|
||||||
|
);
|
||||||
|
if (previousMessageIndex === -1) {
|
||||||
chat.messages.push(message);
|
chat.messages.push(message);
|
||||||
if (_state.selectedChat.id === message.conversation_id) {
|
if (_state.selectedChat.id === message.conversation_id) {
|
||||||
window.bus.$emit('scrollToMessage');
|
window.bus.$emit('scrollToMessage');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
chat.messages[previousMessageIndex] = message;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ const sendMessageAPI = async content => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendAttachmentAPI = async attachment => {
|
const sendAttachmentAPI = async attachment => {
|
||||||
const urlData = endPoints.sendAttachmnet(attachment);
|
const urlData = endPoints.sendAttachment(attachment);
|
||||||
const result = await API.post(urlData.url, urlData.params);
|
const result = await API.post(urlData.url, urlData.params);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,14 +9,13 @@ const sendMessage = content => ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendAttachmnet = ({ attachment }) => {
|
const sendAttachment = ({ attachment }) => {
|
||||||
const { refererURL = '' } = window;
|
const { refererURL = '' } = window;
|
||||||
const timestamp = new Date().toString();
|
const timestamp = new Date().toString();
|
||||||
const { file, file_type: fileType } = attachment;
|
const { file } = attachment;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('message[attachment][file]', file);
|
formData.append('message[attachments][]', file, file.name);
|
||||||
formData.append('message[attachment][file_type]', fileType);
|
|
||||||
formData.append('message[referer_url]', refererURL);
|
formData.append('message[referer_url]', refererURL);
|
||||||
formData.append('message[timestamp]', timestamp);
|
formData.append('message[timestamp]', timestamp);
|
||||||
return {
|
return {
|
||||||
|
@ -43,7 +42,7 @@ const getAvailableAgents = token => ({
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendAttachmnet,
|
sendAttachment,
|
||||||
getConversation,
|
getConversation,
|
||||||
updateMessage,
|
updateMessage,
|
||||||
getAvailableAgents,
|
getAvailableAgents,
|
||||||
|
|
|
@ -11,27 +11,27 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="message-wrap">
|
<div class="message-wrap">
|
||||||
<AgentMessageBubble
|
<AgentMessageBubble
|
||||||
v-if="showTextBubble && shouldDisplayAgentMessage"
|
v-if="!hasAttachments && shouldDisplayAgentMessage"
|
||||||
:content-type="contentType"
|
:content-type="contentType"
|
||||||
:message-content-attributes="messageContentAttributes"
|
:message-content-attributes="messageContentAttributes"
|
||||||
:message-id="message.id"
|
:message-id="message.id"
|
||||||
:message-type="messageType"
|
:message-type="messageType"
|
||||||
:message="message.content"
|
:message="message.content"
|
||||||
/>
|
/>
|
||||||
<div v-if="hasAttachment" class="chat-bubble has-attachment agent">
|
<div v-if="hasAttachments" class="chat-bubble has-attachment agent">
|
||||||
|
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||||
<file-bubble
|
<file-bubble
|
||||||
v-if="
|
v-if="attachment.file_type !== 'image'"
|
||||||
message.attachment && message.attachment.file_type !== 'image'
|
:url="attachment.data_url"
|
||||||
"
|
|
||||||
:url="message.attachment.data_url"
|
|
||||||
/>
|
/>
|
||||||
<image-bubble
|
<image-bubble
|
||||||
v-else
|
v-else
|
||||||
:url="message.attachment.data_url"
|
:url="attachment.data_url"
|
||||||
:thumb="message.attachment.thumb_url"
|
:thumb="attachment.thumb_url"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
|
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
|
||||||
{{ agentName }}
|
{{ agentName }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -78,12 +78,10 @@ export default {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
hasAttachment() {
|
hasAttachments() {
|
||||||
return !!this.message.attachment;
|
return !!(
|
||||||
},
|
this.message.attachments && this.message.attachments.length > 0
|
||||||
showTextBubble() {
|
);
|
||||||
const { message } = this;
|
|
||||||
return !message.attachment;
|
|
||||||
},
|
},
|
||||||
readableTime() {
|
readableTime() {
|
||||||
const { created_at: createdAt = '' } = this.message;
|
const { created_at: createdAt = '' } = this.message;
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default {
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
|
|
|
@ -6,21 +6,23 @@
|
||||||
:message="message.content"
|
:message="message.content"
|
||||||
:status="message.status"
|
:status="message.status"
|
||||||
/>
|
/>
|
||||||
<div v-if="hasAttachment" class="chat-bubble has-attachment user">
|
<div v-if="hasAttachments" class="chat-bubble has-attachment user">
|
||||||
|
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||||
<file-bubble
|
<file-bubble
|
||||||
v-if="message.attachment && message.attachment.file_type !== 'image'"
|
v-if="attachment.file_type !== 'image'"
|
||||||
:url="message.attachment.data_url"
|
:url="attachment.data_url"
|
||||||
:is-in-progress="isInProgress"
|
:is-in-progress="isInProgress"
|
||||||
/>
|
/>
|
||||||
<image-bubble
|
<image-bubble
|
||||||
v-else
|
v-else
|
||||||
:url="message.attachment.data_url"
|
:url="attachment.data_url"
|
||||||
:thumb="message.attachment.thumb_url"
|
:thumb="attachment.thumb_url"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -48,8 +50,10 @@ export default {
|
||||||
const { status = '' } = this.message;
|
const { status = '' } = this.message;
|
||||||
return status === 'in_progress';
|
return status === 'in_progress';
|
||||||
},
|
},
|
||||||
hasAttachment() {
|
hasAttachments() {
|
||||||
return !!this.message.attachment;
|
return !!(
|
||||||
|
this.message.attachments && this.message.attachments.length > 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
showTextBubble() {
|
showTextBubble() {
|
||||||
const { message } = this;
|
const { message } = this;
|
||||||
|
|
|
@ -5,12 +5,17 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
super(app, pubsubToken);
|
super(app, pubsubToken);
|
||||||
this.events = {
|
this.events = {
|
||||||
'message.created': this.onMessageCreated,
|
'message.created': this.onMessageCreated,
|
||||||
|
'message.updated': this.onMessageUpdated,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageCreated = data => {
|
onMessageCreated = data => {
|
||||||
this.app.$store.dispatch('conversation/addMessage', data);
|
this.app.$store.dispatch('conversation/addMessage', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMessageUpdated = data => {
|
||||||
|
this.app.$store.dispatch('conversation/updateMessage', data);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const refreshActionCableConnector = pubsubToken => {
|
export const refreshActionCableConnector = pubsubToken => {
|
||||||
|
|
|
@ -12,12 +12,12 @@ import DateHelper from '../../../shared/helpers/DateHelper';
|
||||||
|
|
||||||
const groupBy = require('lodash.groupby');
|
const groupBy = require('lodash.groupby');
|
||||||
|
|
||||||
export const createTemporaryMessage = ({ attachment, content }) => {
|
export const createTemporaryMessage = ({ attachments, content }) => {
|
||||||
const timestamp = new Date().getTime() / 1000;
|
const timestamp = new Date().getTime() / 1000;
|
||||||
return {
|
return {
|
||||||
id: getUuid(),
|
id: getUuid(),
|
||||||
content,
|
content,
|
||||||
attachment,
|
attachments,
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
created_at: timestamp,
|
created_at: timestamp,
|
||||||
message_type: MESSAGE_TYPE.INCOMING,
|
message_type: MESSAGE_TYPE.INCOMING,
|
||||||
|
@ -97,11 +97,14 @@ export const actions = {
|
||||||
file_type: fileType,
|
file_type: fileType,
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
};
|
};
|
||||||
const tempMessage = createTemporaryMessage({ attachment });
|
const tempMessage = createTemporaryMessage({ attachments: [attachment] });
|
||||||
commit('pushMessageToConversation', tempMessage);
|
commit('pushMessageToConversation', tempMessage);
|
||||||
try {
|
try {
|
||||||
const { data } = await sendAttachmentAPI(params);
|
const { data } = await sendAttachmentAPI(params);
|
||||||
commit('setMessageStatus', { message: data, tempId: tempMessage.id });
|
commit('updateAttachmentMessageStatus', {
|
||||||
|
message: data,
|
||||||
|
tempId: tempMessage.id,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Show error
|
// Show error
|
||||||
}
|
}
|
||||||
|
@ -125,6 +128,10 @@ export const actions = {
|
||||||
|
|
||||||
commit('pushMessageToConversation', data);
|
commit('pushMessageToConversation', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateMessage({ commit }, data) {
|
||||||
|
commit('pushMessageToConversation', data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
@ -151,24 +158,15 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setMessageStatus($state, { message, tempId }) {
|
updateAttachmentMessageStatus($state, { message, tempId }) {
|
||||||
const { status, id } = message;
|
const { id } = message;
|
||||||
const messagesInbox = $state.conversations;
|
const messagesInbox = $state.conversations;
|
||||||
|
|
||||||
const messageInConversation = messagesInbox[tempId];
|
const messageInConversation = messagesInbox[tempId];
|
||||||
|
|
||||||
if (messageInConversation) {
|
if (messageInConversation) {
|
||||||
Vue.delete(messagesInbox, tempId);
|
Vue.delete(messagesInbox, tempId);
|
||||||
const { attachment } = messageInConversation;
|
Vue.set(messagesInbox, id, { ...message });
|
||||||
if (attachment.file_type === 'file') {
|
|
||||||
attachment.data_url = message.attachment.data_url;
|
|
||||||
}
|
|
||||||
Vue.set(messagesInbox, id, {
|
|
||||||
...messageInConversation,
|
|
||||||
attachment,
|
|
||||||
id,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
describe('#sendMessage', () => {
|
||||||
it('sends correct mutations', () => {
|
it('sends correct mutations', () => {
|
||||||
const mockDate = new Date(1466424490000);
|
const mockDate = new Date(1466424490000);
|
||||||
|
@ -59,12 +66,14 @@ describe('#actions', () => {
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
created_at: 1466424490,
|
created_at: 1466424490,
|
||||||
message_type: 0,
|
message_type: 0,
|
||||||
attachment: {
|
attachments: [
|
||||||
|
{
|
||||||
thumb_url: '',
|
thumb_url: '',
|
||||||
data_url: '',
|
data_url: '',
|
||||||
file_type: 'file',
|
file_type: 'file',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -93,7 +93,7 @@ describe('#mutations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#setMessageStatus', () => {
|
describe('#updateAttachmentMessageStatus', () => {
|
||||||
it('Updates status of loading messages if payload is not empty', () => {
|
it('Updates status of loading messages if payload is not empty', () => {
|
||||||
const state = {
|
const state = {
|
||||||
conversations: {
|
conversations: {
|
||||||
|
@ -113,12 +113,18 @@ describe('#mutations', () => {
|
||||||
id: '1',
|
id: '1',
|
||||||
content: '',
|
content: '',
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
attachment: {
|
message_type: 0,
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
file: '',
|
file: '',
|
||||||
file_type: 'image',
|
file_type: 'image',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
mutations.setMessageStatus(state, { message, tempId: 'rand_id_123' });
|
mutations.updateAttachmentMessageStatus(state, {
|
||||||
|
message,
|
||||||
|
tempId: 'rand_id_123',
|
||||||
|
});
|
||||||
|
|
||||||
expect(state.conversations).toEqual({
|
expect(state.conversations).toEqual({
|
||||||
1: {
|
1: {
|
||||||
|
@ -126,10 +132,12 @@ describe('#mutations', () => {
|
||||||
content: '',
|
content: '',
|
||||||
message_type: 0,
|
message_type: 0,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
attachment: {
|
attachments: [
|
||||||
|
{
|
||||||
file: '',
|
file: '',
|
||||||
file_type: 'image',
|
file_type: 'image',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,15 @@ class ActionCableListener < BaseListener
|
||||||
send_to_contact(contact, MESSAGE_CREATED, message)
|
send_to_contact(contact, MESSAGE_CREATED, message)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_updated(event)
|
||||||
|
message, account, timestamp = extract_message_and_account(event)
|
||||||
|
conversation = message.conversation
|
||||||
|
contact = conversation.contact
|
||||||
|
members = conversation.inbox.members.pluck(:pubsub_token)
|
||||||
|
send_to_members(members, MESSAGE_UPDATED, message.push_event_data)
|
||||||
|
send_to_contact(contact, MESSAGE_UPDATED, message)
|
||||||
|
end
|
||||||
|
|
||||||
def conversation_reopened(event)
|
def conversation_reopened(event)
|
||||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||||
members = conversation.inbox.members.pluck(:pubsub_token)
|
members = conversation.inbox.members.pluck(:pubsub_token)
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
class Message < ApplicationRecord
|
class Message < ApplicationRecord
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
|
||||||
|
NUMBER_OF_PERMITTED_ATTACHMENTS = 15
|
||||||
|
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :inbox_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
validates :conversation_id, presence: true
|
validates :conversation_id, presence: true
|
||||||
|
@ -65,7 +67,7 @@ class Message < ApplicationRecord
|
||||||
belongs_to :user, required: false
|
belongs_to :user, required: false
|
||||||
belongs_to :contact, required: false
|
belongs_to :contact, required: false
|
||||||
|
|
||||||
has_one :attachment, dependent: :destroy, autosave: true
|
has_many :attachments, dependent: :destroy, autosave: true, before_add: :validate_attachments_limit
|
||||||
|
|
||||||
after_create :reopen_conversation,
|
after_create :reopen_conversation,
|
||||||
:dispatch_event,
|
:dispatch_event,
|
||||||
|
@ -85,7 +87,7 @@ class Message < ApplicationRecord
|
||||||
message_type: message_type_before_type_cast,
|
message_type: message_type_before_type_cast,
|
||||||
conversation_id: conversation.display_id
|
conversation_id: conversation.display_id
|
||||||
)
|
)
|
||||||
data.merge!(attachment: attachment.push_event_data) if attachment
|
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
||||||
data.merge!(sender: user.push_event_data) if user
|
data.merge!(sender: user.push_event_data) if user
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
|
@ -159,4 +161,8 @@ class Message < ApplicationRecord
|
||||||
ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, Time.zone.now)
|
ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, Time.zone.now)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_attachments_limit(_attachment)
|
||||||
|
errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,7 +46,7 @@ class Facebook::SendReplyService
|
||||||
attachment: {
|
attachment: {
|
||||||
type: 'image',
|
type: 'image',
|
||||||
payload: {
|
payload: {
|
||||||
url: message.attachment.file_url
|
url: message.attachments.first.file_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class Facebook::SendReplyService
|
||||||
end
|
end
|
||||||
|
|
||||||
def fb_message_params
|
def fb_message_params
|
||||||
if message.attachment.blank?
|
if message.attachments.blank?
|
||||||
fb_text_message_params
|
fb_text_message_params
|
||||||
else
|
else
|
||||||
fb_attachment_message_params
|
fb_attachment_message_params
|
||||||
|
|
|
@ -7,4 +7,4 @@ json.content_type @message.content_type
|
||||||
json.content_attributes @message.content_attributes
|
json.content_attributes @message.content_attributes
|
||||||
json.created_at @message.created_at.to_i
|
json.created_at @message.created_at.to_i
|
||||||
json.private @message.private
|
json.private @message.private
|
||||||
json.attachment @message.attachment.push_event_data if @message.attachment
|
json.attachments @message.attachments.map(&:push_event_data) if @message.attachments.present?
|
||||||
|
|
|
@ -14,7 +14,7 @@ json.payload do
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.private message.private
|
json.private message.private
|
||||||
json.source_id message.source_id
|
json.source_id message.source_id
|
||||||
json.attachment message.attachment.push_event_data if message.attachment
|
json.attachments message.attachments.map(&:push_event_data) if message.attachments.present?
|
||||||
json.sender message.user.push_event_data if message.user
|
json.sender message.user.push_event_data if message.user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,5 +6,5 @@ json.message_type @message.message_type_before_type_cast
|
||||||
json.created_at @message.created_at.to_i
|
json.created_at @message.created_at.to_i
|
||||||
json.private @message.private
|
json.private @message.private
|
||||||
json.source_id @message.source_id
|
json.source_id @message.source_id
|
||||||
json.attachment @message.attachment.push_event_data if @message.attachment
|
json.attachments @message.attachments.map(&:push_event_data) if @message.attachments.present?
|
||||||
json.sender @message.user.push_event_data if @message.user
|
json.sender @message.user.push_event_data if @message.user
|
||||||
|
|
|
@ -6,6 +6,6 @@ json.array! @messages do |message|
|
||||||
json.content_attributes message.content_attributes
|
json.content_attributes message.content_attributes
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.conversation_id message.conversation.display_id
|
json.conversation_id message.conversation.display_id
|
||||||
json.attachment message.attachment.push_event_data if message.attachment
|
json.attachments message.attachments.map(&:push_event_data) if message.attachments.present?
|
||||||
json.sender message.user.push_event_data if message.user
|
json.sender message.user.push_event_data if message.user
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,11 +9,14 @@
|
||||||
<b><%= message.incoming? ? 'You' : message.user.name %></b>
|
<b><%= message.incoming? ? 'You' : message.user.name %></b>
|
||||||
</td>
|
</td>
|
||||||
<td>:
|
<td>:
|
||||||
<% if message.attachment %>
|
<% if message.content %>
|
||||||
attachment [<a href="<%= message.attachment.file_url %>" _target="blank">click here to view</a>]
|
|
||||||
<% else %>
|
|
||||||
<%= message.content %>
|
<%= message.content %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if message.attachments %>
|
||||||
|
<% message.attachments.each do |attachment| %>
|
||||||
|
attachment [<a href="<%= attachment.file_url %>" _target="blank">click here to view</a>]
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -99,11 +99,14 @@ launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install imagemagick
|
### Install imagemagick
|
||||||
|
Chatwoot uses `imagemagick` library to resize images for showing previews and smaller size based on context.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install imagemagick
|
brew install imagemagick
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can read more on installing imagemagick from source from [here](https://imagemagick.org/script/download.php).
|
||||||
|
|
||||||
### Install Docker
|
### Install Docker
|
||||||
|
|
||||||
This is an optional step. Those who are doing development can install docker from [Docker Desktop](https://www.docker.com/products/docker-desktop).
|
This is an optional step. Those who are doing development can install docker from [Docker Desktop](https://www.docker.com/products/docker-desktop).
|
||||||
|
|
|
@ -33,15 +33,15 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
||||||
|
|
||||||
it 'creates a new outgoing message with attachment' do
|
it 'creates a new outgoing message with attachment' do
|
||||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
params = { content: 'test-message', attachment: { file: file } }
|
params = { content: 'test-message', attachments: [file] }
|
||||||
|
|
||||||
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
||||||
params: params,
|
params: params,
|
||||||
headers: agent.create_new_auth_token
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(conversation.messages.last.attachment.file.present?).to eq(true)
|
expect(conversation.messages.last.attachments.first.file.present?).to eq(true)
|
||||||
expect(conversation.messages.last.attachment.file_type).to eq('image')
|
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||||
|
|
||||||
it 'creates attachment message in conversation' do
|
it 'creates attachment message in conversation' do
|
||||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
message_params = { content: 'hello world', timestamp: Time.current, attachment: { file: file } }
|
message_params = { content: 'hello world', timestamp: Time.current, attachments: [file] }
|
||||||
post api_v1_widget_messages_url,
|
post api_v1_widget_messages_url,
|
||||||
params: { website_token: web_widget.website_token, message: message_params },
|
params: { website_token: web_widget.website_token, message: message_params },
|
||||||
headers: { 'X-Auth-Token' => token }
|
headers: { 'X-Auth-Token' => token }
|
||||||
|
@ -56,8 +56,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
expect(json_response['content']).to eq(message_params[:content])
|
expect(json_response['content']).to eq(message_params[:content])
|
||||||
|
|
||||||
expect(conversation.messages.last.attachment.file.present?).to eq(true)
|
expect(conversation.messages.last.attachments.first.file.present?).to eq(true)
|
||||||
expect(conversation.messages.last.attachment.file_type).to eq('image')
|
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,8 +50,8 @@ describe Facebook::SendReplyService do
|
||||||
it 'if message with attachment is sent from chatwoot and is outgoing' do
|
it 'if message with attachment is sent from chatwoot and is outgoing' do
|
||||||
create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
|
create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
|
||||||
message = build(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
|
message = build(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
|
||||||
message.attachment = Attachment.new(account_id: message.account_id, file_type: :image)
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||||
message.attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||||
message.save!
|
message.save!
|
||||||
expect(bot).to have_received(:deliver)
|
expect(bot).to have_received(:deliver)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue