mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	 dd8abe975c
			
		
	
	dd8abe975c
	
	
	
		
			
			# Pull Request Template ## Description This PR will replace the usage of `configMixin` with the `useConfig` composable. **Files updated** 1. dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue 2. dashboard/components/widgets/conversation/MessagesView.vue 3. dashboard/routes/dashboard/settings/inbox/Settings.vue **(Not used)** 4. dashboard/routes/dashboard/settings/inbox/FinishSetup.vue **(Not used)** 5. dashboard/routes/dashboard/settings/inbox/settingsPage/CollaboratorsPage.vue 6. dashboard/routes/dashboard/settings/profile/NotificationPreferences.vue **(Not used)** 7. dashboard/routes/dashboard/settings/profile/AudioNotifications.vue **(Not used)** 8. dashboard/routes/dashboard/settings/sla/Index.vue **(Not used)** 9. dashboard/routes/dashboard/settings/account/Index.vue 10. survey/views/Response.vue **(Not used)** Fixes https://linear.app/chatwoot/issue/CW-3464/rewrite-configmixin-mixin-to-a-composable ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Test in the component related pages ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
		
			
				
	
	
		
			589 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			589 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script>
 | |
| import { ref } from 'vue';
 | |
| // composable
 | |
| import { useConfig } from 'dashboard/composables/useConfig';
 | |
| import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
 | |
| 
 | |
| // components
 | |
| import ReplyBox from './ReplyBox.vue';
 | |
| import Message from './Message.vue';
 | |
| import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
 | |
| import Banner from 'dashboard/components/ui/Banner.vue';
 | |
| 
 | |
| // stores and apis
 | |
| import { mapGetters } from 'vuex';
 | |
| 
 | |
| // mixins
 | |
| import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
 | |
| import aiMixin from 'dashboard/mixins/aiMixin';
 | |
| 
 | |
| // utils
 | |
| import { getTypingUsersText } from '../../../helper/commons';
 | |
| import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
 | |
| import { LocalStorage } from 'shared/helpers/localStorage';
 | |
| import {
 | |
|   filterDuplicateSourceMessages,
 | |
|   getReadMessages,
 | |
|   getUnreadMessages,
 | |
| } from 'dashboard/helper/conversationHelper';
 | |
| 
 | |
| // constants
 | |
| import { BUS_EVENTS } from 'shared/constants/busEvents';
 | |
| import { REPLY_POLICY } from 'shared/constants/links';
 | |
| import wootConstants from 'dashboard/constants/globals';
 | |
| import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
 | |
| 
 | |
| export default {
 | |
|   components: {
 | |
|     Message,
 | |
|     ReplyBox,
 | |
|     Banner,
 | |
|     ConversationLabelSuggestion,
 | |
|   },
 | |
|   mixins: [inboxMixin, aiMixin],
 | |
|   props: {
 | |
|     isContactPanelOpen: {
 | |
|       type: Boolean,
 | |
|       default: false,
 | |
|     },
 | |
|     isInboxView: {
 | |
|       type: Boolean,
 | |
|       default: false,
 | |
|     },
 | |
|   },
 | |
|   setup() {
 | |
|     const conversationFooterRef = ref(null);
 | |
|     const isPopOutReplyBox = ref(false);
 | |
|     const { isEnterprise } = useConfig();
 | |
| 
 | |
|     const closePopOutReplyBox = () => {
 | |
|       isPopOutReplyBox.value = false;
 | |
|     };
 | |
| 
 | |
|     const showPopOutReplyBox = () => {
 | |
|       isPopOutReplyBox.value = !isPopOutReplyBox.value;
 | |
|     };
 | |
| 
 | |
|     const keyboardEvents = {
 | |
|       Escape: {
 | |
|         action: closePopOutReplyBox,
 | |
|       },
 | |
|     };
 | |
| 
 | |
|     useKeyboardEvents(keyboardEvents, conversationFooterRef);
 | |
| 
 | |
|     return {
 | |
|       isEnterprise,
 | |
|       conversationFooterRef,
 | |
|       isPopOutReplyBox,
 | |
|       closePopOutReplyBox,
 | |
|       showPopOutReplyBox,
 | |
|     };
 | |
|   },
 | |
|   data() {
 | |
|     return {
 | |
|       isLoadingPrevious: true,
 | |
|       heightBeforeLoad: null,
 | |
|       conversationPanel: null,
 | |
|       hasUserScrolled: false,
 | |
|       isProgrammaticScroll: false,
 | |
|       messageSentSinceOpened: false,
 | |
|       labelSuggestions: [],
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   computed: {
 | |
|     ...mapGetters({
 | |
|       currentChat: 'getSelectedChat',
 | |
|       listLoadingStatus: 'getAllMessagesLoaded',
 | |
|       currentAccountId: 'getCurrentAccountId',
 | |
|     }),
 | |
|     isOpen() {
 | |
|       return this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN;
 | |
|     },
 | |
|     shouldShowLabelSuggestions() {
 | |
|       return (
 | |
|         this.isOpen &&
 | |
|         this.isEnterprise &&
 | |
|         this.isAIIntegrationEnabled &&
 | |
|         !this.messageSentSinceOpened
 | |
|       );
 | |
|     },
 | |
|     inboxId() {
 | |
|       return this.currentChat.inbox_id;
 | |
|     },
 | |
|     inbox() {
 | |
|       return this.$store.getters['inboxes/getInbox'](this.inboxId);
 | |
|     },
 | |
|     typingUsersList() {
 | |
|       const userList = this.$store.getters[
 | |
|         'conversationTypingStatus/getUserList'
 | |
|       ](this.currentChat.id);
 | |
|       return userList;
 | |
|     },
 | |
|     isAnyoneTyping() {
 | |
|       const userList = this.typingUsersList;
 | |
|       return userList.length !== 0;
 | |
|     },
 | |
|     typingUserNames() {
 | |
|       const userList = this.typingUsersList;
 | |
| 
 | |
|       if (this.isAnyoneTyping) {
 | |
|         const userListAsName = getTypingUsersText(userList);
 | |
|         return userListAsName;
 | |
|       }
 | |
| 
 | |
|       return '';
 | |
|     },
 | |
|     getMessages() {
 | |
|       const messages = this.currentChat.messages || [];
 | |
|       if (this.isAWhatsAppChannel) {
 | |
|         return filterDuplicateSourceMessages(messages);
 | |
|       }
 | |
|       return messages;
 | |
|     },
 | |
|     readMessages() {
 | |
|       return getReadMessages(
 | |
|         this.getMessages,
 | |
|         this.currentChat.agent_last_seen_at
 | |
|       );
 | |
|     },
 | |
|     unReadMessages() {
 | |
|       return getUnreadMessages(
 | |
|         this.getMessages,
 | |
|         this.currentChat.agent_last_seen_at
 | |
|       );
 | |
|     },
 | |
|     shouldShowSpinner() {
 | |
|       return (
 | |
|         (this.currentChat && this.currentChat.dataFetched === undefined) ||
 | |
|         (!this.listLoadingStatus && this.isLoadingPrevious)
 | |
|       );
 | |
|     },
 | |
|     conversationType() {
 | |
|       const { additional_attributes: additionalAttributes } = this.currentChat;
 | |
|       const type = additionalAttributes ? additionalAttributes.type : '';
 | |
|       return type || '';
 | |
|     },
 | |
| 
 | |
|     isATweet() {
 | |
|       return this.conversationType === 'tweet';
 | |
|     },
 | |
|     isRightOrLeftIcon() {
 | |
|       if (this.isContactPanelOpen) {
 | |
|         return 'arrow-chevron-right';
 | |
|       }
 | |
|       return 'arrow-chevron-left';
 | |
|     },
 | |
|     getLastSeenAt() {
 | |
|       const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
 | |
|       return contactLastSeenAt;
 | |
|     },
 | |
| 
 | |
|     replyWindowBannerMessage() {
 | |
|       if (this.isAWhatsAppChannel) {
 | |
|         return this.$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY');
 | |
|       }
 | |
|       if (this.isAPIInbox) {
 | |
|         const { additional_attributes: additionalAttributes = {} } = this.inbox;
 | |
|         if (additionalAttributes) {
 | |
|           const {
 | |
|             agent_reply_time_window_message: agentReplyTimeWindowMessage,
 | |
|           } = additionalAttributes;
 | |
|           return agentReplyTimeWindowMessage;
 | |
|         }
 | |
|         return '';
 | |
|       }
 | |
|       return this.$t('CONVERSATION.CANNOT_REPLY');
 | |
|     },
 | |
|     replyWindowLink() {
 | |
|       if (this.isAWhatsAppChannel) {
 | |
|         return REPLY_POLICY.FACEBOOK;
 | |
|       }
 | |
|       if (!this.isAPIInbox) {
 | |
|         return REPLY_POLICY.TWILIO_WHATSAPP;
 | |
|       }
 | |
|       return '';
 | |
|     },
 | |
|     replyWindowLinkText() {
 | |
|       if (this.isAWhatsAppChannel) {
 | |
|         return this.$t('CONVERSATION.24_HOURS_WINDOW');
 | |
|       }
 | |
|       if (!this.isAPIInbox) {
 | |
|         return this.$t('CONVERSATION.TWILIO_WHATSAPP_24_HOURS_WINDOW');
 | |
|       }
 | |
|       return '';
 | |
|     },
 | |
|     unreadMessageCount() {
 | |
|       return this.currentChat.unread_count || 0;
 | |
|     },
 | |
|     isInstagramDM() {
 | |
|       return this.conversationType === 'instagram_direct_message';
 | |
|     },
 | |
|     inboxSupportsReplyTo() {
 | |
|       const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
 | |
|       const outgoing =
 | |
|         this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING) &&
 | |
|         !this.is360DialogWhatsAppChannel;
 | |
| 
 | |
|       return { incoming, outgoing };
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   watch: {
 | |
|     currentChat(newChat, oldChat) {
 | |
|       if (newChat.id === oldChat.id) {
 | |
|         return;
 | |
|       }
 | |
|       this.fetchAllAttachmentsFromCurrentChat();
 | |
|       this.fetchSuggestions();
 | |
|       this.messageSentSinceOpened = false;
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   created() {
 | |
|     this.$emitter.on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
 | |
|     // when a new message comes in, we refetch the label suggestions
 | |
|     this.$emitter.on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
 | |
|     // when a message is sent we set the flag to true this hides the label suggestions,
 | |
|     // until the chat is changed and the flag is reset in the watch for currentChat
 | |
|     this.$emitter.on(BUS_EVENTS.MESSAGE_SENT, () => {
 | |
|       this.messageSentSinceOpened = true;
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   mounted() {
 | |
|     this.addScrollListener();
 | |
|     this.fetchAllAttachmentsFromCurrentChat();
 | |
|     this.fetchSuggestions();
 | |
|   },
 | |
| 
 | |
|   beforeDestroy() {
 | |
|     this.removeBusListeners();
 | |
|     this.removeScrollListener();
 | |
|   },
 | |
| 
 | |
|   methods: {
 | |
|     async fetchSuggestions() {
 | |
|       // start empty, this ensures that the label suggestions are not shown
 | |
|       this.labelSuggestions = [];
 | |
| 
 | |
|       if (this.isLabelSuggestionDismissed()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (!this.isEnterprise) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // method available in mixin, need to ensure that integrations are present
 | |
|       await this.fetchIntegrationsIfRequired();
 | |
| 
 | |
|       if (!this.isLabelSuggestionFeatureEnabled) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this.labelSuggestions = await this.fetchLabelSuggestions({
 | |
|         conversationId: this.currentChat.id,
 | |
|       });
 | |
| 
 | |
|       // once the labels are fetched, we need to scroll to bottom
 | |
|       // but we need to wait for the DOM to be updated
 | |
|       // so we use the nextTick method
 | |
|       this.$nextTick(() => {
 | |
|         // this param is added to route, telling the UI to navigate to the message
 | |
|         // it is triggered by the SCROLL_TO_MESSAGE method
 | |
|         // see setActiveChat on ConversationView.vue for more info
 | |
|         const { messageId } = this.$route.query;
 | |
| 
 | |
|         // only trigger the scroll to bottom if the user has not scrolled
 | |
|         // and there's no active messageId that is selected in view
 | |
|         if (!messageId && !this.hasUserScrolled) {
 | |
|           this.scrollToBottom();
 | |
|         }
 | |
|       });
 | |
|     },
 | |
|     isLabelSuggestionDismissed() {
 | |
|       return LocalStorage.getFlag(
 | |
|         LOCAL_STORAGE_KEYS.DISMISSED_LABEL_SUGGESTIONS,
 | |
|         this.currentAccountId,
 | |
|         this.currentChat.id
 | |
|       );
 | |
|     },
 | |
|     fetchAllAttachmentsFromCurrentChat() {
 | |
|       this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
 | |
|     },
 | |
|     removeBusListeners() {
 | |
|       this.$emitter.off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
 | |
|     },
 | |
|     onScrollToMessage({ messageId = '' } = {}) {
 | |
|       this.$nextTick(() => {
 | |
|         const messageElement = document.getElementById('message' + messageId);
 | |
|         if (messageElement) {
 | |
|           this.isProgrammaticScroll = true;
 | |
|           messageElement.scrollIntoView({ behavior: 'smooth' });
 | |
|           this.fetchPreviousMessages();
 | |
|         } else {
 | |
|           this.scrollToBottom();
 | |
|         }
 | |
|       });
 | |
|       this.makeMessagesRead();
 | |
|     },
 | |
|     addScrollListener() {
 | |
|       this.conversationPanel = this.$el.querySelector('.conversation-panel');
 | |
|       this.setScrollParams();
 | |
|       this.conversationPanel.addEventListener('scroll', this.handleScroll);
 | |
|       this.$nextTick(() => this.scrollToBottom());
 | |
|       this.isLoadingPrevious = false;
 | |
|     },
 | |
|     removeScrollListener() {
 | |
|       this.conversationPanel.removeEventListener('scroll', this.handleScroll);
 | |
|     },
 | |
|     scrollToBottom() {
 | |
|       this.isProgrammaticScroll = true;
 | |
|       let relevantMessages = [];
 | |
| 
 | |
|       // label suggestions are not part of the messages list
 | |
|       // so we need to handle them separately
 | |
|       let labelSuggestions =
 | |
|         this.conversationPanel.querySelector('.label-suggestion');
 | |
| 
 | |
|       // if there are unread messages, scroll to the first unread message
 | |
|       if (this.unreadMessageCount > 0) {
 | |
|         // capturing only the unread messages
 | |
|         relevantMessages =
 | |
|           this.conversationPanel.querySelectorAll('.message--unread');
 | |
|       } else if (labelSuggestions) {
 | |
|         // when scrolling to the bottom, the label suggestions is below the last message
 | |
|         // so we scroll there if there are no unread messages
 | |
|         // Unread messages always take the highest priority
 | |
|         relevantMessages = [labelSuggestions];
 | |
|       } else {
 | |
|         // if there are no unread messages or label suggestion, scroll to the last message
 | |
|         // capturing last message from the messages list
 | |
|         relevantMessages = Array.from(
 | |
|           this.conversationPanel.querySelectorAll('.message--read')
 | |
|         ).slice(-1);
 | |
|       }
 | |
| 
 | |
|       this.conversationPanel.scrollTop = calculateScrollTop(
 | |
|         this.conversationPanel.scrollHeight,
 | |
|         this.$el.scrollHeight,
 | |
|         relevantMessages
 | |
|       );
 | |
|     },
 | |
|     onToggleContactPanel() {
 | |
|       this.$emit('contactPanelToggle');
 | |
|     },
 | |
|     setScrollParams() {
 | |
|       this.heightBeforeLoad = this.conversationPanel.scrollHeight;
 | |
|       this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
 | |
|     },
 | |
| 
 | |
|     async fetchPreviousMessages(scrollTop = 0) {
 | |
|       this.setScrollParams();
 | |
|       const shouldLoadMoreMessages =
 | |
|         this.currentChat.dataFetched === true &&
 | |
|         !this.listLoadingStatus &&
 | |
|         !this.isLoadingPrevious;
 | |
| 
 | |
|       if (
 | |
|         scrollTop < 100 &&
 | |
|         !this.isLoadingPrevious &&
 | |
|         shouldLoadMoreMessages
 | |
|       ) {
 | |
|         this.isLoadingPrevious = true;
 | |
|         try {
 | |
|           await this.$store.dispatch('fetchPreviousMessages', {
 | |
|             conversationId: this.currentChat.id,
 | |
|             before: this.currentChat.messages[0].id,
 | |
|           });
 | |
|           const heightDifference =
 | |
|             this.conversationPanel.scrollHeight - this.heightBeforeLoad;
 | |
|           this.conversationPanel.scrollTop =
 | |
|             this.scrollTopBeforeLoad + heightDifference;
 | |
|           this.setScrollParams();
 | |
|         } catch (error) {
 | |
|           // Ignore Error
 | |
|         } finally {
 | |
|           this.isLoadingPrevious = false;
 | |
|         }
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     handleScroll(e) {
 | |
|       if (this.isProgrammaticScroll) {
 | |
|         // Reset the flag
 | |
|         this.isProgrammaticScroll = false;
 | |
|         this.hasUserScrolled = false;
 | |
|       } else {
 | |
|         this.hasUserScrolled = true;
 | |
|       }
 | |
|       this.$emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
 | |
|       this.fetchPreviousMessages(e.target.scrollTop);
 | |
|     },
 | |
| 
 | |
|     makeMessagesRead() {
 | |
|       this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
 | |
|     },
 | |
| 
 | |
|     getInReplyToMessage(parentMessage) {
 | |
|       if (!parentMessage) return {};
 | |
|       const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
 | |
|       if (!inReplyToMessageId) return {};
 | |
| 
 | |
|       return this.currentChat?.messages.find(message => {
 | |
|         if (message.id === inReplyToMessageId) {
 | |
|           return true;
 | |
|         }
 | |
|         return false;
 | |
|       });
 | |
|     },
 | |
|   },
 | |
| };
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
 | |
|     <Banner
 | |
|       v-if="!currentChat.can_reply"
 | |
|       color-scheme="alert"
 | |
|       :banner-message="replyWindowBannerMessage"
 | |
|       :href-link="replyWindowLink"
 | |
|       :href-link-text="replyWindowLinkText"
 | |
|     />
 | |
|     <div class="flex justify-end">
 | |
|       <woot-button
 | |
|         variant="smooth"
 | |
|         size="tiny"
 | |
|         color-scheme="secondary"
 | |
|         class="box-border fixed z-10 bg-white border border-r-0 border-solid rounded-bl-calc rtl:rotate-180 rounded-tl-calc dark:bg-slate-700 border-slate-50 dark:border-slate-600"
 | |
|         :class="
 | |
|           isInboxView ? 'top-52 md:top-40' : 'top-[9.5rem] md:top-[6.25rem]'
 | |
|         "
 | |
|         :icon="isRightOrLeftIcon"
 | |
|         @click="onToggleContactPanel"
 | |
|       />
 | |
|     </div>
 | |
|     <ul class="conversation-panel">
 | |
|       <transition name="slide-up">
 | |
|         <li class="min-h-[4rem]">
 | |
|           <span v-if="shouldShowSpinner" class="spinner message" />
 | |
|         </li>
 | |
|       </transition>
 | |
|       <Message
 | |
|         v-for="message in readMessages"
 | |
|         :key="message.id"
 | |
|         class="message--read ph-no-capture"
 | |
|         data-clarity-mask="True"
 | |
|         :data="message"
 | |
|         :is-a-tweet="isATweet"
 | |
|         :is-a-whatsapp-channel="isAWhatsAppChannel"
 | |
|         :is-web-widget-inbox="isAWebWidgetInbox"
 | |
|         :is-a-facebook-inbox="isAFacebookInbox"
 | |
|         :is-an-email-inbox="isAnEmailChannel"
 | |
|         :is-instagram="isInstagramDM"
 | |
|         :inbox-supports-reply-to="inboxSupportsReplyTo"
 | |
|         :in-reply-to="getInReplyToMessage(message)"
 | |
|       />
 | |
|       <li v-show="unreadMessageCount != 0" class="unread--toast">
 | |
|         <span>
 | |
|           {{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
 | |
|           {{
 | |
|             unreadMessageCount > 1
 | |
|               ? $t('CONVERSATION.UNREAD_MESSAGES')
 | |
|               : $t('CONVERSATION.UNREAD_MESSAGE')
 | |
|           }}
 | |
|         </span>
 | |
|       </li>
 | |
|       <Message
 | |
|         v-for="message in unReadMessages"
 | |
|         :key="message.id"
 | |
|         class="message--unread ph-no-capture"
 | |
|         data-clarity-mask="True"
 | |
|         :data="message"
 | |
|         :is-a-tweet="isATweet"
 | |
|         :is-a-whatsapp-channel="isAWhatsAppChannel"
 | |
|         :is-web-widget-inbox="isAWebWidgetInbox"
 | |
|         :is-a-facebook-inbox="isAFacebookInbox"
 | |
|         :is-instagram-dm="isInstagramDM"
 | |
|         :inbox-supports-reply-to="inboxSupportsReplyTo"
 | |
|         :in-reply-to="getInReplyToMessage(message)"
 | |
|       />
 | |
|       <ConversationLabelSuggestion
 | |
|         v-if="shouldShowLabelSuggestions"
 | |
|         :suggested-labels="labelSuggestions"
 | |
|         :chat-labels="currentChat.labels"
 | |
|         :conversation-id="currentChat.id"
 | |
|       />
 | |
|     </ul>
 | |
|     <div
 | |
|       ref="conversationFooterRef"
 | |
|       class="conversation-footer"
 | |
|       :class="{ 'modal-mask': isPopOutReplyBox }"
 | |
|     >
 | |
|       <div
 | |
|         v-if="isAnyoneTyping"
 | |
|         class="absolute flex items-center w-full h-0 -top-7"
 | |
|       >
 | |
|         <div
 | |
|           class="flex py-2 pr-4 pl-5 shadow-md rounded-full bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100 text-xs font-semibold my-2.5 mx-auto"
 | |
|         >
 | |
|           {{ typingUserNames }}
 | |
|           <img
 | |
|             class="w-6 ltr:ml-2 rtl:mr-2"
 | |
|             src="~dashboard/assets/images/typing.gif"
 | |
|             alt="Someone is typing"
 | |
|           />
 | |
|         </div>
 | |
|       </div>
 | |
|       <ReplyBox
 | |
|         :conversation-id="currentChat.id"
 | |
|         :popout-reply-box.sync="isPopOutReplyBox"
 | |
|         @click="showPopOutReplyBox"
 | |
|       />
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <style scoped>
 | |
| @tailwind components;
 | |
| @layer components {
 | |
|   .rounded-bl-calc {
 | |
|     border-bottom-left-radius: calc(1.5rem + 1px);
 | |
|   }
 | |
| 
 | |
|   .rounded-tl-calc {
 | |
|     border-top-left-radius: calc(1.5rem + 1px);
 | |
|   }
 | |
| }
 | |
| </style>
 | |
| 
 | |
| <style scoped lang="scss">
 | |
| .modal-mask {
 | |
|   @apply absolute;
 | |
| 
 | |
|   &::v-deep {
 | |
|     .ProseMirror-woot-style {
 | |
|       @apply max-h-[25rem];
 | |
|     }
 | |
| 
 | |
|     .reply-box {
 | |
|       @apply border border-solid border-slate-75 dark:border-slate-600 max-w-[75rem] w-[70%];
 | |
|     }
 | |
| 
 | |
|     .reply-box .reply-box__top {
 | |
|       @apply relative min-h-[27.5rem];
 | |
|     }
 | |
| 
 | |
|     .reply-box__top .input {
 | |
|       @apply min-h-[27.5rem];
 | |
|     }
 | |
| 
 | |
|     .emoji-dialog {
 | |
|       @apply absolute left-auto bottom-1;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| </style>
 |