-
-
+
+
+
+
+
+
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;
diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js
index 27ff80dbc..38500f16b 100644
--- a/app/javascript/dashboard/helper/actionCable.js
+++ b/app/javascript/dashboard/helper/actionCable.js
@@ -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 || {};
diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js
index df299fabc..c770c6a46 100644
--- a/app/javascript/dashboard/store/modules/conversations/actions.js
+++ b/app/javascript/dashboard/store/modules/conversations/actions.js
@@ -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
diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js
index 317be297b..f61c3add4 100644
--- a/app/javascript/dashboard/store/modules/conversations/index.js
+++ b/app/javascript/dashboard/store/modules/conversations/index.js
@@ -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;
}
},
diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js
index 542d8a213..3bacc3a36 100755
--- a/app/javascript/widget/api/conversation.js
+++ b/app/javascript/widget/api/conversation.js
@@ -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;
};
diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js
index 4cc4cf726..c86f915e4 100755
--- a/app/javascript/widget/api/endPoints.js
+++ b/app/javascript/widget/api/endPoints.js
@@ -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,
diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue
index 0341e46af..31f33d190 100755
--- a/app/javascript/widget/components/AgentMessage.vue
+++ b/app/javascript/widget/components/AgentMessage.vue
@@ -11,26 +11,26 @@
-
-
-
+
{{ 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;
diff --git a/app/javascript/widget/components/ImageBubble.vue b/app/javascript/widget/components/ImageBubble.vue
index e81661c63..02078398b 100644
--- a/app/javascript/widget/components/ImageBubble.vue
+++ b/app/javascript/widget/components/ImageBubble.vue
@@ -47,6 +47,7 @@ export default {
img {
width: 100%;
+ max-width: 250px;
}
.time {
diff --git a/app/javascript/widget/components/UserMessage.vue b/app/javascript/widget/components/UserMessage.vue
index a07f9b05b..578a9b939 100755
--- a/app/javascript/widget/components/UserMessage.vue
+++ b/app/javascript/widget/components/UserMessage.vue
@@ -6,18 +6,20 @@
:message="message.content"
:status="message.status"
/>
-
@@ -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;
diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js
index a7706bbe5..1518fb6bb 100644
--- a/app/javascript/widget/helpers/actionCable.js
+++ b/app/javascript/widget/helpers/actionCable.js
@@ -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 => {
diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js
index 3a7762a42..c33c84b9c 100755
--- a/app/javascript/widget/store/modules/conversation.js
+++ b/app/javascript/widget/store/modules/conversation.js
@@ -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 });
}
},
diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
index 1cf371e89..87dad5eda 100644
--- a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
+++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
@@ -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',
+ },
+ ],
});
});
});
diff --git a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js
index 3a99b509f..9f29b531b 100644
--- a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js
+++ b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js
@@ -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',
+ },
+ ],
},
});
});
diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb
index ca79ceba3..fc4e16960 100644
--- a/app/listeners/action_cable_listener.rb
+++ b/app/listeners/action_cable_listener.rb
@@ -22,6 +22,15 @@ class ActionCableListener < BaseListener
send_to_contact(contact, MESSAGE_CREATED, message)
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)
conversation, account, timestamp = extract_conversation_and_account(event)
members = conversation.inbox.members.pluck(:pubsub_token)
diff --git a/app/models/message.rb b/app/models/message.rb
index cc323ebd0..218059537 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -35,6 +35,8 @@
class Message < ApplicationRecord
include Events::Types
+ NUMBER_OF_PERMITTED_ATTACHMENTS = 15
+
validates :account_id, presence: true
validates :inbox_id, presence: true
validates :conversation_id, presence: true
@@ -65,7 +67,7 @@ class Message < ApplicationRecord
belongs_to :user, 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,
:dispatch_event,
@@ -85,7 +87,7 @@ class Message < ApplicationRecord
message_type: message_type_before_type_cast,
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
end
@@ -159,4 +161,8 @@ class Message < ApplicationRecord
ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, Time.zone.now)
end
end
+
+ def validate_attachments_limit(_attachment)
+ errors.add(attachments: 'exceeded maximum allowed') if attachments.size >= NUMBER_OF_PERMITTED_ATTACHMENTS
+ end
end
diff --git a/app/services/facebook/send_reply_service.rb b/app/services/facebook/send_reply_service.rb
index 87e07fe8c..df897a1f2 100644
--- a/app/services/facebook/send_reply_service.rb
+++ b/app/services/facebook/send_reply_service.rb
@@ -46,7 +46,7 @@ class Facebook::SendReplyService
attachment: {
type: 'image',
payload: {
- url: message.attachment.file_url
+ url: message.attachments.first.file_url
}
}
}
@@ -54,7 +54,7 @@ class Facebook::SendReplyService
end
def fb_message_params
- if message.attachment.blank?
+ if message.attachments.blank?
fb_text_message_params
else
fb_attachment_message_params
diff --git a/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder b/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
index e01408e43..0dc5b827e 100644
--- a/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
@@ -7,4 +7,4 @@ json.content_type @message.content_type
json.content_attributes @message.content_attributes
json.created_at @message.created_at.to_i
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?
diff --git a/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder b/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder
index f9b88c36c..64333d3ec 100644
--- a/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/messages/index.json.jbuilder
@@ -14,7 +14,7 @@ json.payload do
json.created_at message.created_at.to_i
json.private message.private
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
end
end
diff --git a/app/views/api/v1/widget/messages/create.json.jbuilder b/app/views/api/v1/widget/messages/create.json.jbuilder
index 051a911e7..ff442b304 100644
--- a/app/views/api/v1/widget/messages/create.json.jbuilder
+++ b/app/views/api/v1/widget/messages/create.json.jbuilder
@@ -6,5 +6,5 @@ json.message_type @message.message_type_before_type_cast
json.created_at @message.created_at.to_i
json.private @message.private
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
diff --git a/app/views/api/v1/widget/messages/index.json.jbuilder b/app/views/api/v1/widget/messages/index.json.jbuilder
index c39ed54b8..f6491991c 100644
--- a/app/views/api/v1/widget/messages/index.json.jbuilder
+++ b/app/views/api/v1/widget/messages/index.json.jbuilder
@@ -6,6 +6,6 @@ json.array! @messages do |message|
json.content_attributes message.content_attributes
json.created_at message.created_at.to_i
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
end
diff --git a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb
index 51e486380..5a490de62 100644
--- a/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb
+++ b/app/views/mailers/conversation_reply_mailer/reply_with_summary.html.erb
@@ -9,11 +9,14 @@
<%= message.incoming? ? 'You' : message.user.name %>
:
- <% if message.attachment %>
- attachment [click here to view]
- <% else %>
+ <% if message.content %>
<%= message.content %>
<% end %>
+ <% if message.attachments %>
+ <% message.attachments.each do |attachment| %>
+ attachment [click here to view]
+ <% end %>
+ <% end %>
|
<% end %>
diff --git a/docs/development/environment-setup/mac-os.md b/docs/development/environment-setup/mac-os.md
index 36321ebc6..860291e79 100644
--- a/docs/development/environment-setup/mac-os.md
+++ b/docs/development/environment-setup/mac-os.md
@@ -99,11 +99,14 @@ launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
```
### Install imagemagick
+Chatwoot uses `imagemagick` library to resize images for showing previews and smaller size based on context.
```bash
brew install imagemagick
```
+You can read more on installing imagemagick from source from [here](https://imagemagick.org/script/download.php).
+
### 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).
diff --git a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
index 588a129ab..6137a7298 100644
--- a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
@@ -33,15 +33,15 @@ RSpec.describe 'Conversation Messages API', type: :request do
it 'creates a new outgoing message with attachment' do
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),
params: params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:success)
- expect(conversation.messages.last.attachment.file.present?).to eq(true)
- expect(conversation.messages.last.attachment.file_type).to eq('image')
+ expect(conversation.messages.last.attachments.first.file.present?).to eq(true)
+ expect(conversation.messages.last.attachments.first.file_type).to eq('image')
end
end
diff --git a/spec/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb
index 8ce37e17a..62c374295 100644
--- a/spec/controllers/api/v1/widget/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
it 'creates attachment message in conversation' do
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,
params: { website_token: web_widget.website_token, message: message_params },
headers: { 'X-Auth-Token' => token }
@@ -56,8 +56,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
json_response = JSON.parse(response.body)
expect(json_response['content']).to eq(message_params[:content])
- expect(conversation.messages.last.attachment.file.present?).to eq(true)
- expect(conversation.messages.last.attachment.file_type).to eq('image')
+ expect(conversation.messages.last.attachments.first.file.present?).to eq(true)
+ expect(conversation.messages.last.attachments.first.file_type).to eq('image')
end
end
end
diff --git a/spec/services/facebook/send_reply_service_spec.rb b/spec/services/facebook/send_reply_service_spec.rb
index 11aea9d58..74a06f237 100644
--- a/spec/services/facebook/send_reply_service_spec.rb
+++ b/spec/services/facebook/send_reply_service_spec.rb
@@ -50,8 +50,8 @@ describe Facebook::SendReplyService 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)
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)
- message.attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
+ attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
message.save!
expect(bot).to have_received(:deliver)
end