mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	Chore: Cleanup attachment handling for Facebook & Whatsapp (#1051)
* Chore: Enable file upload for facebook messenger * Chore: Fix attachments * Chore: Fix Specs * Fix ReplyBox file attachment logic * Set default value for message Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
		| @@ -15,9 +15,9 @@ | |||||||
|       /> |       /> | ||||||
|       <resizable-text-area |       <resizable-text-area | ||||||
|         ref="messageInput" |         ref="messageInput" | ||||||
|         v-model="message" |         v-model.trim="message" | ||||||
|         class="input" |         class="input" | ||||||
|         :placeholder="$t(messagePlaceHolder())" |         :placeholder="messagePlaceHolder" | ||||||
|         :min-height="4" |         :min-height="4" | ||||||
|         @focus="onFocus" |         @focus="onFocus" | ||||||
|         @blur="onBlur" |         @blur="onBlur" | ||||||
| @@ -25,46 +25,43 @@ | |||||||
|       <file-upload |       <file-upload | ||||||
|         v-if="showFileUpload" |         v-if="showFileUpload" | ||||||
|         :size="4096 * 4096" |         :size="4096 * 4096" | ||||||
|         accept="jpg,jpeg,png,mp3,ogg,amr,pdf,mp4" |         accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg" | ||||||
|         @input-file="onFileUpload" |         @input-file="onFileUpload" | ||||||
|       > |       > | ||||||
|         <i |         <i v-if="!isUploading" class="icon ion-android-attach attachment" /> | ||||||
|           v-if="!isUploading.image" |         <woot-spinner v-if="isUploading" /> | ||||||
|           class="icon ion-android-attach attachment" |  | ||||||
|         /> |  | ||||||
|         <woot-spinner v-if="isUploading.image" /> |  | ||||||
|       </file-upload> |       </file-upload> | ||||||
|       <i |       <i | ||||||
|         class="icon ion-happy-outline" |         class="icon ion-happy-outline" | ||||||
|         :class="{ active: showEmojiPicker }" |         :class="{ active: showEmojiPicker }" | ||||||
|         @click="toggleEmojiPicker()" |         @click="toggleEmojiPicker" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="reply-box__bottom"> |     <div class="reply-box__bottom"> | ||||||
|       <ul class="tabs"> |       <ul class="tabs"> | ||||||
|         <li class="tabs-title" :class="{ 'is-active': !isPrivate }"> |         <li class="tabs-title" :class="{ 'is-active': !isPrivate }"> | ||||||
|           <a href="#" @click="makeReply">{{ |           <a href="#" @click="setReplyMode">{{ | ||||||
|             $t('CONVERSATION.REPLYBOX.REPLY') |             $t('CONVERSATION.REPLYBOX.REPLY') | ||||||
|           }}</a> |           }}</a> | ||||||
|         </li> |         </li> | ||||||
|         <li class="tabs-title is-private" :class="{ 'is-active': isPrivate }"> |         <li class="tabs-title is-private" :class="{ 'is-active': isPrivate }"> | ||||||
|           <a href="#" @click="makePrivate">{{ |           <a href="#" @click="setPrivateReplyMode">{{ | ||||||
|             $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') |             $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') | ||||||
|           }}</a> |           }}</a> | ||||||
|         </li> |         </li> | ||||||
|         <li v-if="message.length" class="tabs-title message-length"> |         <li v-if="message.length" class="tabs-title message-length"> | ||||||
|           <a :class="{ 'message-error': message.length > maxLength - 40 }"> |           <a :class="{ 'message-error': isMessageLengthReachingThreshold }">{{ | ||||||
|             {{ message.length }} / {{ maxLength }} |             characterCountIndicator | ||||||
|           </a> |           }}</a> | ||||||
|         </li> |         </li> | ||||||
|       </ul> |       </ul> | ||||||
|       <button |       <button | ||||||
|         type="button" |         type="button" | ||||||
|         class="button send-button" |         class="button send-button" | ||||||
|         :disabled="disableButton()" |         :disabled="isReplyButtonDisabled" | ||||||
|         :class="{ |         :class="{ | ||||||
|           disabled: message.length === 0 || message.length > maxLength, |           disabled: isReplyButtonDisabled, | ||||||
|           warning: isPrivate, |           warning: isPrivate, | ||||||
|         }" |         }" | ||||||
|         @click="sendMessage" |         @click="sendMessage" | ||||||
| @@ -93,6 +90,12 @@ import FileUpload from 'vue-upload-component'; | |||||||
| import EmojiInput from '../emoji/EmojiInput'; | import EmojiInput from '../emoji/EmojiInput'; | ||||||
| import CannedResponse from './CannedResponse'; | import CannedResponse from './CannedResponse'; | ||||||
| import ResizableTextArea from 'shared/components/ResizableTextArea'; | import ResizableTextArea from 'shared/components/ResizableTextArea'; | ||||||
|  | import { | ||||||
|  |   isEscape, | ||||||
|  |   isEnter, | ||||||
|  |   hasPressedShift, | ||||||
|  | } from 'shared/helpers/KeyboardHelpers'; | ||||||
|  | import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
| @@ -109,19 +112,38 @@ export default { | |||||||
|       isFocused: false, |       isFocused: false, | ||||||
|       showEmojiPicker: false, |       showEmojiPicker: false, | ||||||
|       showCannedResponsesList: false, |       showCannedResponsesList: false, | ||||||
|       isUploading: { |       isUploading: false, | ||||||
|         audio: false, |  | ||||||
|         video: false, |  | ||||||
|         image: false, |  | ||||||
|       }, |  | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapGetters({ |     ...mapGetters({ currentChat: 'getSelectedChat' }), | ||||||
|       currentChat: 'getSelectedChat', |     inboxId() { | ||||||
|     }), |       return this.currentChat.inbox_id; | ||||||
|  |     }, | ||||||
|  |     inbox() { | ||||||
|  |       return this.$store.getters['inboxes/getInbox'](this.inboxId); | ||||||
|  |     }, | ||||||
|  |     messagePlaceHolder() { | ||||||
|  |       return this.isPrivate | ||||||
|  |         ? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT') | ||||||
|  |         : this.$t('CONVERSATION.FOOTER.MSG_INPUT'); | ||||||
|  |     }, | ||||||
|  |     isMessageLengthReachingThreshold() { | ||||||
|  |       return this.message.length > this.maxLength - 40; | ||||||
|  |     }, | ||||||
|  |     characterCountIndicator() { | ||||||
|  |       return `${this.message.length} / ${this.maxLength}`; | ||||||
|  |     }, | ||||||
|  |     isReplyButtonDisabled() { | ||||||
|  |       const isMessageEmpty = !this.message.replace(/\n/g, '').length; | ||||||
|  |       return ( | ||||||
|  |         isMessageEmpty || | ||||||
|  |         this.message.length === 0 || | ||||||
|  |         this.message.length > this.maxLength | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|     channelType() { |     channelType() { | ||||||
|       return this.currentChat.meta.channel; |       return this.inbox.channel_type; | ||||||
|     }, |     }, | ||||||
|     conversationType() { |     conversationType() { | ||||||
|       const { additional_attributes: additionalAttributes } = this.currentChat; |       const { additional_attributes: additionalAttributes } = this.currentChat; | ||||||
| @@ -129,18 +151,52 @@ export default { | |||||||
|       return type || ''; |       return type || ''; | ||||||
|     }, |     }, | ||||||
|     maxLength() { |     maxLength() { | ||||||
|       if (this.channelType === 'Channel::FacebookPage') { |       if (this.isPrivate) { | ||||||
|         return 640; |         return MESSAGE_MAX_LENGTH.GENERAL; | ||||||
|       } |       } | ||||||
|       if (this.channelType === 'Channel::TwitterProfile') { |  | ||||||
|  |       if (this.isAFacebookInbox) { | ||||||
|  |         return MESSAGE_MAX_LENGTH.FACEBOOK; | ||||||
|  |       } | ||||||
|  |       if (this.isATwilioSMSChannel) { | ||||||
|  |         return MESSAGE_MAX_LENGTH.TWILIO_SMS; | ||||||
|  |       } | ||||||
|  |       if (this.isATwitterInbox) { | ||||||
|         if (this.conversationType === 'tweet') { |         if (this.conversationType === 'tweet') { | ||||||
|           return 280; |           return MESSAGE_MAX_LENGTH.TWEET; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       return 10000; |       return MESSAGE_MAX_LENGTH.GENERAL; | ||||||
|  |     }, | ||||||
|  |     isATwitterInbox() { | ||||||
|  |       return this.channelType === 'Channel::TwitterProfile'; | ||||||
|  |     }, | ||||||
|  |     isAFacebookInbox() { | ||||||
|  |       return this.channelType === 'Channel::FacebookPage'; | ||||||
|  |     }, | ||||||
|  |     isAWebWidgetInbox() { | ||||||
|  |       return this.channelType === 'Channel::WebWidget'; | ||||||
|  |     }, | ||||||
|  |     isATwilioSMSChannel() { | ||||||
|  |       const { phone_number: phoneNumber = '' } = this.inbox; | ||||||
|  |       return ( | ||||||
|  |         this.channelType === 'Channel::TwilioSms' && | ||||||
|  |         !phoneNumber.startsWith('whatsapp') | ||||||
|  |       ); | ||||||
|  |     }, | ||||||
|  |     isATwilioWhatsappChannel() { | ||||||
|  |       const { phone_number: phoneNumber = '' } = this.inbox; | ||||||
|  |       return ( | ||||||
|  |         this.channelType === 'Channel::TwilioSms' && | ||||||
|  |         phoneNumber.startsWith('whatsapp') | ||||||
|  |       ); | ||||||
|     }, |     }, | ||||||
|     showFileUpload() { |     showFileUpload() { | ||||||
|       return this.channelType === 'Channel::WebWidget'; |       return ( | ||||||
|  |         this.isAWebWidgetInbox || | ||||||
|  |         this.isAFacebookInbox || | ||||||
|  |         this.isATwilioWhatsappChannel | ||||||
|  |       ); | ||||||
|     }, |     }, | ||||||
|     replyButtonLabel() { |     replyButtonLabel() { | ||||||
|       if (this.isPrivate) { |       if (this.isPrivate) { | ||||||
| @@ -158,20 +214,18 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     message(val) { |     message(updatedMessage) { | ||||||
|       if (this.isPrivate) { |       if (this.isPrivate) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       const isSlashCommand = val[0] === '/'; |       const isSlashCommand = updatedMessage[0] === '/'; | ||||||
|       const hasNextWord = val.includes(' '); |       const hasNextWord = updatedMessage.includes(' '); | ||||||
|       const isShortCodeActive = isSlashCommand && !hasNextWord; |       const isShortCodeActive = isSlashCommand && !hasNextWord; | ||||||
|       if (isShortCodeActive) { |       if (isShortCodeActive) { | ||||||
|         this.showCannedResponsesList = true; |         this.showCannedResponsesList = true; | ||||||
|         if (val.length > 1) { |         if (updatedMessage.length > 1) { | ||||||
|           const searchKey = val.substr(1, val.length); |           const searchKey = updatedMessage.substr(1, updatedMessage.length); | ||||||
|           this.$store.dispatch('getCannedResponse', { |           this.$store.dispatch('getCannedResponse', { searchKey }); | ||||||
|             searchKey, |  | ||||||
|           }); |  | ||||||
|         } else { |         } else { | ||||||
|           this.$store.dispatch('getCannedResponse'); |           this.$store.dispatch('getCannedResponse'); | ||||||
|         } |         } | ||||||
| @@ -188,26 +242,18 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     handleKeyEvents(e) { |     handleKeyEvents(e) { | ||||||
|       if (this.isEscape(e)) { |       if (isEscape(e)) { | ||||||
|         this.hideEmojiPicker(); |         this.hideEmojiPicker(); | ||||||
|         this.hideCannedResponse(); |         this.hideCannedResponse(); | ||||||
|       } else if (this.isEnter(e)) { |       } else if (isEnter(e)) { | ||||||
|         if (!e.shiftKey) { |         if (!hasPressedShift(e)) { | ||||||
|           e.preventDefault(); |           e.preventDefault(); | ||||||
|           this.sendMessage(); |           this.sendMessage(); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     isEnter(e) { |  | ||||||
|       return e.keyCode === 13; |  | ||||||
|     }, |  | ||||||
|     isEscape(e) { |  | ||||||
|       return e.keyCode === 27; // ESCAPE |  | ||||||
|     }, |  | ||||||
|     async sendMessage() { |     async sendMessage() { | ||||||
|       const isMessageEmpty = !this.message.replace(/\n/g, '').length; |       if (this.isReplyButtonDisabled) { | ||||||
|       if (isMessageEmpty) return; |  | ||||||
|       if (this.message.length > this.maxLength) { |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       const newMessage = this.message; |       const newMessage = this.message; | ||||||
| @@ -231,11 +277,11 @@ export default { | |||||||
|         this.message = message; |         this.message = message; | ||||||
|       }, 100); |       }, 100); | ||||||
|     }, |     }, | ||||||
|     makePrivate() { |     setPrivateReplyMode() { | ||||||
|       this.isPrivate = true; |       this.isPrivate = true; | ||||||
|       this.$refs.messageInput.focus(); |       this.$refs.messageInput.focus(); | ||||||
|     }, |     }, | ||||||
|     makeReply() { |     setReplyMode() { | ||||||
|       this.isPrivate = false; |       this.isPrivate = false; | ||||||
|       this.$refs.messageInput.focus(); |       this.$refs.messageInput.focus(); | ||||||
|     }, |     }, | ||||||
| @@ -258,7 +304,6 @@ export default { | |||||||
|     hideCannedResponse() { |     hideCannedResponse() { | ||||||
|       this.showCannedResponsesList = false; |       this.showCannedResponsesList = false; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     onBlur() { |     onBlur() { | ||||||
|       this.isFocused = false; |       this.isFocused = false; | ||||||
|       this.toggleTyping('off'); |       this.toggleTyping('off'); | ||||||
| @@ -267,9 +312,8 @@ export default { | |||||||
|       this.isFocused = true; |       this.isFocused = true; | ||||||
|       this.toggleTyping('on'); |       this.toggleTyping('on'); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     toggleTyping(status) { |     toggleTyping(status) { | ||||||
|       if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) { |       if (this.isAWebWidgetInbox && !this.isPrivate) { | ||||||
|         const conversationId = this.currentChat.id; |         const conversationId = this.currentChat.id; | ||||||
|         this.$store.dispatch('conversationTypingStatus/toggleTyping', { |         this.$store.dispatch('conversationTypingStatus/toggleTyping', { | ||||||
|           status, |           status, | ||||||
| @@ -277,35 +321,19 @@ export default { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     disableButton() { |  | ||||||
|       const messageHasOnlyNewLines = !this.message.replace(/\n/g, '').length; |  | ||||||
|       return ( |  | ||||||
|         this.message.length === 0 || |  | ||||||
|         this.message.length > 640 || |  | ||||||
|         messageHasOnlyNewLines |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     messagePlaceHolder() { |  | ||||||
|       const placeHolder = this.isPrivate |  | ||||||
|         ? 'CONVERSATION.FOOTER.PRIVATE_MSG_INPUT' |  | ||||||
|         : 'CONVERSATION.FOOTER.MSG_INPUT'; |  | ||||||
|       return placeHolder; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     onFileUpload(file) { |     onFileUpload(file) { | ||||||
|       if (!file) { |       if (!file) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       this.isUploading.image = true; |       this.isUploading = true; | ||||||
|       this.$store |       this.$store | ||||||
|         .dispatch('sendAttachment', [this.currentChat.id, { file: file.file }]) |         .dispatch('sendAttachment', [this.currentChat.id, { file: file.file }]) | ||||||
|         .then(() => { |         .then(() => { | ||||||
|           this.isUploading.image = false; |           this.isUploading = false; | ||||||
|           this.$emit('scrollToMessage'); |           this.$emit('scrollToMessage'); | ||||||
|         }) |         }) | ||||||
|         .catch(() => { |         .catch(() => { | ||||||
|           this.isUploading.image = false; |           this.isUploading = false; | ||||||
|           this.$emit('scrollToMessage'); |           this.$emit('scrollToMessage'); | ||||||
|         }); |         }); | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -12,6 +12,10 @@ const initialSelectedChat = { | |||||||
|   status: null, |   status: null, | ||||||
|   muted: false, |   muted: false, | ||||||
|   seen: false, |   seen: false, | ||||||
|  |   inbox_id: null, | ||||||
|  |   additional_attributes: { | ||||||
|  |     type: '', | ||||||
|  |   }, | ||||||
|   dataFetched: false, |   dataFetched: false, | ||||||
| }; | }; | ||||||
| const state = { | const state = { | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								app/javascript/shared/helpers/KeyboardHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/javascript/shared/helpers/KeyboardHelpers.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | export const isEnter = e => { | ||||||
|  |   return e.keyCode === 13; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const isEscape = e => { | ||||||
|  |   return e.keyCode === 27; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const hasPressedShift = e => { | ||||||
|  |   return e.shiftKey; | ||||||
|  | }; | ||||||
| @@ -2,7 +2,7 @@ import { escapeHtml } from './HTMLSanitizer'; | |||||||
|  |  | ||||||
| class MessageFormatter { | class MessageFormatter { | ||||||
|   constructor(message) { |   constructor(message) { | ||||||
|     this.message = escapeHtml(message) || ''; |     this.message = escapeHtml(message || '') || ''; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   formatMessage() { |   formatMessage() { | ||||||
|   | |||||||
| @@ -1,3 +1,10 @@ | |||||||
| export const isAFormMessage = message => message.content_type === 'form'; | export const isAFormMessage = message => message.content_type === 'form'; | ||||||
| export const isASubmittedFormMessage = (message = {}) => | export const isASubmittedFormMessage = (message = {}) => | ||||||
|   isAFormMessage(message) && !!message.content_attributes?.submitted_values; |   isAFormMessage(message) && !!message.content_attributes?.submitted_values; | ||||||
|  |  | ||||||
|  | export const MESSAGE_MAX_LENGTH = { | ||||||
|  |   GENERAL: 10000, | ||||||
|  |   FACEBOOK: 640, | ||||||
|  |   TWILIO_SMS: 160, | ||||||
|  |   TWEET: 280, | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								app/javascript/shared/helpers/specs/KeyboardHelpers.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/javascript/shared/helpers/specs/KeyboardHelpers.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import { isEnter, isEscape, hasPressedShift } from '../KeyboardHelpers'; | ||||||
|  |  | ||||||
|  | describe('#KeyboardHelpers', () => { | ||||||
|  |   describe('#isEnter', () => { | ||||||
|  |     it('return correct values', () => { | ||||||
|  |       expect(isEnter({ keyCode: 13 })).toEqual(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('#isEscape', () => { | ||||||
|  |     it('return correct values', () => { | ||||||
|  |       expect(isEscape({ keyCode: 27 })).toEqual(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('#hasPressedShift', () => { | ||||||
|  |     it('return correct values', () => { | ||||||
|  |       expect(hasPressedShift({ shiftKey: true })).toEqual(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										15
									
								
								app/jobs/send_reply_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/jobs/send_reply_job.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | class SendReplyJob < ApplicationJob | ||||||
|  |   queue_as :high | ||||||
|  |  | ||||||
|  |   def perform(message_id) | ||||||
|  |     message = Message.find(message_id) | ||||||
|  |     channel_name = message.conversation.inbox.channel.class.to_s | ||||||
|  |     if channel_name == 'Channel::FacebookPage' | ||||||
|  |       ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|  |     elsif channel_name == 'Channel::TwitterProfile' | ||||||
|  |       ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|  |     elsif channel_name == 'Channel::TwilioSms' | ||||||
|  |       ::Twilio::SendOnTwilioService.new(message: message).perform | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -134,14 +134,7 @@ class Message < ApplicationRecord | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def send_reply |   def send_reply | ||||||
|     channel_name = conversation.inbox.channel.class.to_s |     ::SendReplyJob.perform_later(id) | ||||||
|     if channel_name == 'Channel::FacebookPage' |  | ||||||
|       ::Facebook::SendOnFacebookService.new(message: self).perform |  | ||||||
|     elsif channel_name == 'Channel::TwitterProfile' |  | ||||||
|       ::Twitter::SendOnTwitterService.new(message: self).perform |  | ||||||
|     elsif channel_name == 'Channel::TwilioSms' |  | ||||||
|       ::Twilio::SendOnTwilioService.new(message: self).perform |  | ||||||
|     end |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def reopen_conversation |   def reopen_conversation | ||||||
|   | |||||||
| @@ -17,19 +17,26 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def fb_attachment_message_params |   def fb_attachment_message_params | ||||||
|  |     attachment = message.attachments.first | ||||||
|     { |     { | ||||||
|       recipient: { id: contact.get_source_id(inbox.id) }, |       recipient: { id: contact.get_source_id(inbox.id) }, | ||||||
|       message: { |       message: { | ||||||
|         attachment: { |         attachment: { | ||||||
|           type: 'image', |           type: attachment_type(attachment), | ||||||
|           payload: { |           payload: { | ||||||
|             url: message.attachments.first.file_url |             url: attachment.file_url | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def attachment_type(attachment) | ||||||
|  |     return attachment.file_type if %w[image audio video file].include? attachment.file_type | ||||||
|  |  | ||||||
|  |     'file' | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def fb_message_params |   def fb_message_params | ||||||
|     if message.attachments.blank? |     if message.attachments.blank? | ||||||
|       fb_text_message_params |       fb_text_message_params | ||||||
|   | |||||||
| @@ -56,10 +56,10 @@ title: "Supported features on channels" | |||||||
| | Channel | Channel greeting | Attachments | Agent Auto assignment | Slack | | | Channel | Channel greeting | Attachments | Agent Auto assignment | Slack | | ||||||
| | -- | -- | -- | -- | -- | | | -- | -- | -- | -- | -- | | ||||||
| | Website | Yes | Yes | Yes | Yes | | | Website | Yes | Yes | Yes | Yes | | ||||||
| | Facebook | Yes | No | Yes | No | | | Facebook | Yes | Yes | Yes | No | | ||||||
| | Twitter DM | Yes | No | Yes | No | | | Twitter DM | Yes | No | Yes | No | | ||||||
| | Tweets | Yes | No | Yes | No | | | Tweets | Yes | No | Yes | No | | ||||||
| | SMS | Yes | No | Yes | No | | | SMS | Yes | No | Yes | No | | ||||||
| | Whatsapp | Yes | No | Yes | No | | | Whatsapp | Yes | Yes | Yes | No | | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -10,8 +10,8 @@ FactoryBot.define do | |||||||
|  |  | ||||||
|     after(:build) do |message| |     after(:build) do |message| | ||||||
|       message.sender ||= create(:user, account: message.account) |       message.sender ||= create(:user, account: message.account) | ||||||
|       message.conversation ||= create(:conversation, account: message.account) |  | ||||||
|       message.inbox ||= create(:inbox, account: message.account) |       message.inbox ||= create(:inbox, account: message.account) | ||||||
|  |       message.conversation ||= create(:conversation, account: message.account, inbox: message.inbox) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ describe Facebook::SendOnFacebookService do | |||||||
|   before do |   before do | ||||||
|     allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true) |     allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true) | ||||||
|     allow(bot).to receive(:deliver) |     allow(bot).to receive(:deliver) | ||||||
|  |     create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   let!(:account) { create(:account) } |   let!(:account) { create(:account) } | ||||||
| @@ -20,39 +21,43 @@ describe Facebook::SendOnFacebookService do | |||||||
|   describe '#perform' do |   describe '#perform' do | ||||||
|     context 'without reply' do |     context 'without reply' do | ||||||
|       it 'if message is private' do |       it 'if message is private' do | ||||||
|         create(:message, message_type: 'outgoing', private: true, inbox: facebook_inbox, account: account) |         message = create(:message, message_type: 'outgoing', private: true, inbox: facebook_inbox, account: account) | ||||||
|  |         ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|         expect(bot).not_to have_received(:deliver) |         expect(bot).not_to have_received(:deliver) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if inbox channel is not facebook page' do |       it 'if inbox channel is not facebook page' do | ||||||
|         create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account) |         message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account) | ||||||
|  |         expect { ::Facebook::SendOnFacebookService.new(message: message).perform }.to raise_error 'Invalid channel service was called' | ||||||
|         expect(bot).not_to have_received(:deliver) |         expect(bot).not_to have_received(:deliver) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message is not outgoing' do |       it 'if message is not outgoing' do | ||||||
|         create(:message, message_type: 'incoming', inbox: facebook_inbox, account: account) |         message = create(:message, message_type: 'incoming', inbox: facebook_inbox, account: account) | ||||||
|  |         ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|         expect(bot).not_to have_received(:deliver) |         expect(bot).not_to have_received(:deliver) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message has an FB ID' do |       it 'if message has an FB ID' do | ||||||
|         create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, source_id: SecureRandom.uuid) |         message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, source_id: SecureRandom.uuid) | ||||||
|  |         ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|         expect(bot).not_to have_received(:deliver) |         expect(bot).not_to have_received(:deliver) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     context 'with reply' do |     context 'with reply' do | ||||||
|       it 'if message is sent from chatwoot and is outgoing' do |       it 'if message is sent from chatwoot and is outgoing' do | ||||||
|         create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation) |         message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) | ||||||
|         create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) |         ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|         expect(bot).to have_received(:deliver) |         expect(bot).to have_received(:deliver) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       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) |  | ||||||
|         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) | ||||||
|         attachment = message.attachments.new(account_id: message.account_id, file_type: :image) |         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') |         attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') | ||||||
|         message.save! |         message.save! | ||||||
|  |         ::Facebook::SendOnFacebookService.new(message: message).perform | ||||||
|         expect(bot).to have_received(:deliver) |         expect(bot).to have_received(:deliver) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   | |||||||
| @@ -25,22 +25,26 @@ describe Twilio::SendOnTwilioService do | |||||||
|   describe '#perform' do |   describe '#perform' do | ||||||
|     context 'without reply' do |     context 'without reply' do | ||||||
|       it 'if message is private' do |       it 'if message is private' do | ||||||
|         create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account) |         message = create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account) | ||||||
|  |         ::Twilio::SendOnTwilioService.new(message: message).perform | ||||||
|         expect(twilio_client).not_to have_received(:messages) |         expect(twilio_client).not_to have_received(:messages) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if inbox channel is not facebook page' do |       it 'if inbox channel is not twilio' do | ||||||
|         create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account) |         message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account) | ||||||
|  |         expect { ::Twilio::SendOnTwilioService.new(message: message).perform }.to raise_error 'Invalid channel service was called' | ||||||
|         expect(twilio_client).not_to have_received(:messages) |         expect(twilio_client).not_to have_received(:messages) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message is not outgoing' do |       it 'if message is not outgoing' do | ||||||
|         create(:message, message_type: 'incoming', inbox: twilio_inbox, account: account) |         message = create(:message, message_type: 'incoming', inbox: twilio_inbox, account: account) | ||||||
|  |         ::Twilio::SendOnTwilioService.new(message: message).perform | ||||||
|         expect(twilio_client).not_to have_received(:messages) |         expect(twilio_client).not_to have_received(:messages) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message has an source id' do |       it 'if message has an source id' do | ||||||
|         create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, source_id: SecureRandom.uuid) |         message = create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, source_id: SecureRandom.uuid) | ||||||
|  |         ::Twilio::SendOnTwilioService.new(message: message).perform | ||||||
|         expect(twilio_client).not_to have_received(:messages) |         expect(twilio_client).not_to have_received(:messages) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| @@ -53,6 +57,7 @@ describe Twilio::SendOnTwilioService do | |||||||
|         outgoing_message = create( |         outgoing_message = create( | ||||||
|           :message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation |           :message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation | ||||||
|         ) |         ) | ||||||
|  |         ::Twilio::SendOnTwilioService.new(message: outgoing_message).perform | ||||||
|  |  | ||||||
|         expect(outgoing_message.reload.source_id).to eq('1234') |         expect(outgoing_message.reload.source_id).to eq('1234') | ||||||
|       end |       end | ||||||
| @@ -69,6 +74,8 @@ describe Twilio::SendOnTwilioService do | |||||||
|       attachment = message.attachments.new(account_id: message.account_id, file_type: :image) |       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') |       attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') | ||||||
|       message.save! |       message.save! | ||||||
|  |  | ||||||
|  |       ::Twilio::SendOnTwilioService.new(message: message).perform | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -41,22 +41,26 @@ describe Twitter::SendOnTwitterService do | |||||||
|   describe '#perform' do |   describe '#perform' do | ||||||
|     context 'without reply' do |     context 'without reply' do | ||||||
|       it 'if inbox channel is not twitter profile' do |       it 'if inbox channel is not twitter profile' do | ||||||
|         create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account) |         message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account) | ||||||
|  |         expect { ::Twitter::SendOnTwitterService.new(message: message).perform }.to raise_error 'Invalid channel service was called' | ||||||
|         expect(twitter_client).not_to have_received(:send_direct_message) |         expect(twitter_client).not_to have_received(:send_direct_message) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message is private' do |       it 'if message is private' do | ||||||
|         create(:message, message_type: 'outgoing', private: true, inbox: twitter_inbox, account: account) |         message = create(:message, message_type: 'outgoing', private: true, inbox: twitter_inbox, account: account) | ||||||
|  |         ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|         expect(twitter_client).not_to have_received(:send_direct_message) |         expect(twitter_client).not_to have_received(:send_direct_message) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message has source_id' do |       it 'if message has source_id' do | ||||||
|         create(:message, message_type: 'outgoing', source_id: '123', inbox: widget_inbox, account: account) |         message = create(:message, message_type: 'outgoing', source_id: '123', inbox: twitter_inbox, account: account) | ||||||
|  |         ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|         expect(twitter_client).not_to have_received(:send_direct_message) |         expect(twitter_client).not_to have_received(:send_direct_message) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if message is not outgoing' do |       it 'if message is not outgoing' do | ||||||
|         create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account) |         message = create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account) | ||||||
|  |         ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|         expect(twitter_client).not_to have_received(:send_direct_message) |         expect(twitter_client).not_to have_received(:send_direct_message) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| @@ -64,15 +68,17 @@ describe Twitter::SendOnTwitterService do | |||||||
|     context 'with reply' do |     context 'with reply' do | ||||||
|       it 'if conversation is a direct message' do |       it 'if conversation is a direct message' do | ||||||
|         create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: dm_conversation) |         create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: dm_conversation) | ||||||
|         create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: dm_conversation) |         message = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: dm_conversation) | ||||||
|  |         ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|         expect(twitter_client).to have_received(:send_direct_message) |         expect(twitter_client).to have_received(:send_direct_message) | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       it 'if conversation is a tweet' do |       it 'if conversation is a tweet' do | ||||||
|         create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: tweet_conversation) |         create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: tweet_conversation) | ||||||
|         tweet = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: tweet_conversation) |         message = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: tweet_conversation) | ||||||
|  |         ::Twitter::SendOnTwitterService.new(message: message).perform | ||||||
|         expect(twitter_client).to have_received(:send_tweet_reply) |         expect(twitter_client).to have_received(:send_tweet_reply) | ||||||
|         expect(tweet.reload.source_id).to eq '12345' |         expect(message.reload.source_id).to eq '12345' | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose