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:
Sony Mathew 2020-04-17 21:15:20 +05:30 committed by GitHub
parent 0817414957
commit 818c769bb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 212 additions and 137 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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`,

View file

@ -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>

View file

@ -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;

View file

@ -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 || {};

View file

@ -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

View file

@ -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;
} }
}, },

View file

@ -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;
}; };

View file

@ -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,

View file

@ -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;

View file

@ -47,6 +47,7 @@ export default {
img { img {
width: 100%; width: 100%;
max-width: 250px;
} }
.time { .time {

View file

@ -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;

View file

@ -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 => {

View file

@ -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,
});
} }
}, },

View file

@ -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',
}, },
],
}); });
}); });
}); });

View file

@ -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',
}, },
],
}, },
}); });
}); });

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 %>

View file

@ -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).

View file

@ -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

View file

@ -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

View file

@ -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