mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 11:08:04 +00:00 
			
		
		
		
	 5cb88237f5
			
		
	
	5cb88237f5
	
	
	
		
			
			* Chore: Move conversationStats to a seperate module * Move toggleTyping to conversationTypingStatus * Remove unused agentTyping flag * Fix review comments
		
			
				
	
	
		
			319 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			319 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <template>
 | |
|   <div class="reply-box">
 | |
|     <div class="reply-box__top" :class="{ 'is-private': isPrivate }">
 | |
|       <canned-response
 | |
|         v-if="showCannedResponsesList"
 | |
|         v-on-clickaway="hideCannedResponse"
 | |
|         data-dropdown-menu
 | |
|         :on-keyenter="replaceText"
 | |
|         :on-click="replaceText"
 | |
|       />
 | |
|       <emoji-input
 | |
|         v-if="showEmojiPicker"
 | |
|         v-on-clickaway="hideEmojiPicker"
 | |
|         :on-click="emojiOnClick"
 | |
|       />
 | |
|       <textarea
 | |
|         ref="messageInput"
 | |
|         v-model="message"
 | |
|         rows="1"
 | |
|         class="input"
 | |
|         type="text"
 | |
|         :placeholder="$t(messagePlaceHolder())"
 | |
|         @focus="onFocus"
 | |
|         @blur="onBlur"
 | |
|       />
 | |
|       <file-upload
 | |
|         v-if="showFileUpload"
 | |
|         :size="4096 * 4096"
 | |
|         accept="jpg,jpeg,png,mp3,ogg,amr,pdf,mp4"
 | |
|         @input-file="onFileUpload"
 | |
|       >
 | |
|         <i
 | |
|           v-if="!isUploading.image"
 | |
|           class="icon ion-android-attach attachment"
 | |
|         />
 | |
|         <woot-spinner v-if="isUploading.image" />
 | |
|       </file-upload>
 | |
|       <i
 | |
|         class="icon ion-happy-outline"
 | |
|         :class="{ active: showEmojiPicker }"
 | |
|         @click="toggleEmojiPicker()"
 | |
|       />
 | |
|     </div>
 | |
| 
 | |
|     <div class="reply-box__bottom">
 | |
|       <ul class="tabs">
 | |
|         <li class="tabs-title" :class="{ 'is-active': !isPrivate }">
 | |
|           <a href="#" @click="makeReply">{{
 | |
|             $t('CONVERSATION.REPLYBOX.REPLY')
 | |
|           }}</a>
 | |
|         </li>
 | |
|         <li class="tabs-title is-private" :class="{ 'is-active': isPrivate }">
 | |
|           <a href="#" @click="makePrivate">{{
 | |
|             $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE')
 | |
|           }}</a>
 | |
|         </li>
 | |
|         <li v-if="message.length" class="tabs-title message-length">
 | |
|           <a :class="{ 'message-error': message.length > maxLength - 40 }">
 | |
|             {{ message.length }} / {{ maxLength }}
 | |
|           </a>
 | |
|         </li>
 | |
|       </ul>
 | |
|       <button
 | |
|         type="button"
 | |
|         class="button send-button"
 | |
|         :disabled="disableButton()"
 | |
|         :class="{
 | |
|           disabled: message.length === 0 || message.length > maxLength,
 | |
|           warning: isPrivate,
 | |
|         }"
 | |
|         @click="sendMessage"
 | |
|       >
 | |
|         {{ replyButtonLabel }}
 | |
|         <i
 | |
|           class="icon"
 | |
|           :class="{
 | |
|             'ion-android-send': !isPrivate,
 | |
|             'ion-android-lock': isPrivate,
 | |
|           }"
 | |
|         />
 | |
|       </button>
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script>
 | |
| /* eslint no-console: 0 */
 | |
| 
 | |
| import { mapGetters } from 'vuex';
 | |
| import emojione from 'emojione';
 | |
| import { mixin as clickaway } from 'vue-clickaway';
 | |
| import FileUpload from 'vue-upload-component';
 | |
| 
 | |
| import EmojiInput from '../emoji/EmojiInput';
 | |
| import CannedResponse from './CannedResponse';
 | |
| 
 | |
| export default {
 | |
|   components: {
 | |
|     EmojiInput,
 | |
|     CannedResponse,
 | |
|     FileUpload,
 | |
|   },
 | |
|   mixins: [clickaway],
 | |
|   data() {
 | |
|     return {
 | |
|       message: '',
 | |
|       isPrivate: false,
 | |
|       showEmojiPicker: false,
 | |
|       showCannedResponsesList: false,
 | |
|       isUploading: {
 | |
|         audio: false,
 | |
|         video: false,
 | |
|         image: false,
 | |
|       },
 | |
|     };
 | |
|   },
 | |
|   computed: {
 | |
|     ...mapGetters({
 | |
|       currentChat: 'getSelectedChat',
 | |
|     }),
 | |
|     channelType() {
 | |
|       const {
 | |
|         meta: {
 | |
|           sender: { channel },
 | |
|         },
 | |
|       } = this.currentChat;
 | |
|       return channel;
 | |
|     },
 | |
|     conversationType() {
 | |
|       const { additional_attributes: additionalAttributes } = this.currentChat;
 | |
|       const type = additionalAttributes ? additionalAttributes.type : '';
 | |
|       return type || '';
 | |
|     },
 | |
|     maxLength() {
 | |
|       if (this.channelType === 'Channel::FacebookPage') {
 | |
|         return 640;
 | |
|       }
 | |
|       if (this.channelType === 'Channel::TwitterProfile') {
 | |
|         if (this.conversationType === 'tweet') {
 | |
|           return 280;
 | |
|         }
 | |
|       }
 | |
|       return 10000;
 | |
|     },
 | |
|     showFileUpload() {
 | |
|       return (
 | |
|         this.channelType === 'Channel::WebWidget' ||
 | |
|         this.channelType === 'Channel::TwilioSms'
 | |
|       );
 | |
|     },
 | |
|     replyButtonLabel() {
 | |
|       if (this.isPrivate) {
 | |
|         return this.$t('CONVERSATION.REPLYBOX.CREATE');
 | |
|       }
 | |
|       if (this.conversationType === 'tweet') {
 | |
|         return this.$t('CONVERSATION.REPLYBOX.TWEET');
 | |
|       }
 | |
|       return this.$t('CONVERSATION.REPLYBOX.SEND');
 | |
|     },
 | |
|   },
 | |
|   watch: {
 | |
|     message(val) {
 | |
|       if (this.isPrivate) {
 | |
|         return;
 | |
|       }
 | |
|       const isSlashCommand = val[0] === '/';
 | |
|       const hasNextWord = val.includes(' ');
 | |
|       const isShortCodeActive = isSlashCommand && !hasNextWord;
 | |
|       if (isShortCodeActive) {
 | |
|         this.showCannedResponsesList = true;
 | |
|         if (val.length > 1) {
 | |
|           const searchKey = val.substr(1, val.length);
 | |
|           this.$store.dispatch('getCannedResponse', {
 | |
|             searchKey,
 | |
|           });
 | |
|         } else {
 | |
|           this.$store.dispatch('getCannedResponse');
 | |
|         }
 | |
|       } else {
 | |
|         this.showCannedResponsesList = false;
 | |
|       }
 | |
|     },
 | |
|   },
 | |
|   mounted() {
 | |
|     document.addEventListener('keydown', this.handleKeyEvents);
 | |
|   },
 | |
|   destroyed() {
 | |
|     document.removeEventListener('keydown', this.handleKeyEvents);
 | |
|   },
 | |
|   methods: {
 | |
|     handleKeyEvents(e) {
 | |
|       if (this.isEscape(e)) {
 | |
|         this.hideEmojiPicker();
 | |
|         this.hideCannedResponse();
 | |
|       } else if (this.isEnter(e)) {
 | |
|         if (!e.shiftKey) {
 | |
|           e.preventDefault();
 | |
|           this.sendMessage();
 | |
|         }
 | |
|       }
 | |
|     },
 | |
|     isEnter(e) {
 | |
|       return e.keyCode === 13;
 | |
|     },
 | |
|     isEscape(e) {
 | |
|       return e.keyCode === 27; // ESCAPE
 | |
|     },
 | |
|     async sendMessage() {
 | |
|       const isMessageEmpty = !this.message.replace(/\n/g, '').length;
 | |
|       if (isMessageEmpty) return;
 | |
|       if (this.message.length > this.maxLength) {
 | |
|         return;
 | |
|       }
 | |
|       if (!this.showCannedResponsesList) {
 | |
|         try {
 | |
|           await this.$store.dispatch('sendMessage', {
 | |
|             conversationId: this.currentChat.id,
 | |
|             message: this.message,
 | |
|             private: this.isPrivate,
 | |
|           });
 | |
|           this.$emit('scrollToMessage');
 | |
|         } catch (error) {
 | |
|           // Error
 | |
|         }
 | |
|         this.clearMessage();
 | |
|         this.hideEmojiPicker();
 | |
|       }
 | |
|     },
 | |
|     replaceText(message) {
 | |
|       setTimeout(() => {
 | |
|         this.message = message;
 | |
|       }, 100);
 | |
|     },
 | |
|     makePrivate() {
 | |
|       this.isPrivate = true;
 | |
|       this.$refs.messageInput.focus();
 | |
|     },
 | |
|     makeReply() {
 | |
|       this.isPrivate = false;
 | |
|       this.$refs.messageInput.focus();
 | |
|     },
 | |
|     emojiOnClick(emoji) {
 | |
|       this.message = emojione.shortnameToUnicode(
 | |
|         `${this.message}${emoji.shortname} `
 | |
|       );
 | |
|     },
 | |
|     clearMessage() {
 | |
|       this.message = '';
 | |
|     },
 | |
|     toggleEmojiPicker() {
 | |
|       this.showEmojiPicker = !this.showEmojiPicker;
 | |
|     },
 | |
|     hideEmojiPicker() {
 | |
|       if (this.showEmojiPicker) {
 | |
|         this.toggleEmojiPicker();
 | |
|       }
 | |
|     },
 | |
|     hideCannedResponse() {
 | |
|       this.showCannedResponsesList = false;
 | |
|     },
 | |
| 
 | |
|     onBlur() {
 | |
|       this.toggleTyping('off');
 | |
|     },
 | |
|     onFocus() {
 | |
|       this.toggleTyping('on');
 | |
|     },
 | |
| 
 | |
|     toggleTyping(status) {
 | |
|       if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
 | |
|         const conversationId = this.currentChat.id;
 | |
|         this.$store.dispatch('conversationTypingStatus/toggleTyping', {
 | |
|           status,
 | |
|           conversationId,
 | |
|         });
 | |
|       }
 | |
|     },
 | |
|     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) {
 | |
|       if (!file) {
 | |
|         return;
 | |
|       }
 | |
|       this.isUploading.image = true;
 | |
|       this.$store
 | |
|         .dispatch('sendAttachment', [this.currentChat.id, { file: file.file }])
 | |
|         .then(() => {
 | |
|           this.isUploading.image = false;
 | |
|           this.$emit('scrollToMessage');
 | |
|         })
 | |
|         .catch(() => {
 | |
|           this.isUploading.image = false;
 | |
|           this.$emit('scrollToMessage');
 | |
|         });
 | |
|     },
 | |
|   },
 | |
| };
 | |
| </script>
 | |
| 
 | |
| <style lang="scss">
 | |
| .send-button {
 | |
|   margin-bottom: 0;
 | |
| }
 | |
| </style>
 |