mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	 12916ceca6
			
		
	
	12916ceca6
	
	
	
		
			
			The system did not detect the delivery errors earlier, resulting in some false positives. The user was not informed when an email failed to be delivered. While we do handle failure status in other channels, we were not able to capture the actual delivery status for the email channel. This pull request makes the following changes: - Updated the class EmailReplyWorker to use the deliver_now method instead of deliver_later. This change is made to raise any errors that may occur with the SMTP connection. The errors are then captured and sent to Sentry, and the email is marked as failed. Previously, we did not consider the case of retries in the email channel, so this feature is currently not functioning. So, I have disabled the retry option. We will address this in a follow-up ticket. - Downgraded the net-smtp gem to version 0.3.4. This change is made to avoid an argument error when using XOAUTH2. Fixes: https://linear.app/chatwoot/issue/CW-3032/argumenterror-wrong-authentication-type-xoauth2-argumenterror
		
			
				
	
	
		
			566 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			566 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <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="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
 | |
|         :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 getReadMessages"
 | |
|         :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 getUnReadMessages"
 | |
|         :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)"
 | |
|       />
 | |
|       <conversation-label-suggestion
 | |
|         v-if="shouldShowLabelSuggestions"
 | |
|         :suggested-labels="labelSuggestions"
 | |
|         :chat-labels="currentChat.labels"
 | |
|         :conversation-id="currentChat.id"
 | |
|       />
 | |
|     </ul>
 | |
|     <div
 | |
|       class="conversation-footer"
 | |
|       :class="{ 'modal-mask': isPopoutReplyBox }"
 | |
|     >
 | |
|       <div v-if="isAnyoneTyping" class="typing-indicator-wrap">
 | |
|         <div class="typing-indicator">
 | |
|           {{ typingUserNames }}
 | |
|           <img
 | |
|             class="gif"
 | |
|             src="~dashboard/assets/images/typing.gif"
 | |
|             alt="Someone is typing"
 | |
|           />
 | |
|         </div>
 | |
|       </div>
 | |
|       <reply-box
 | |
|         :conversation-id="currentChat.id"
 | |
|         :popout-reply-box.sync="isPopoutReplyBox"
 | |
|         @click="showPopoutReplyBox"
 | |
|       />
 | |
|     </div>
 | |
|   </div>
 | |
| </template>
 | |
| 
 | |
| <script>
 | |
| // 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 conversationMixin, {
 | |
|   filterDuplicateSourceMessages,
 | |
| } from '../../../mixins/conversations';
 | |
| import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
 | |
| import configMixin from 'shared/mixins/configMixin';
 | |
| import eventListenerMixins from 'shared/mixins/eventListenerMixins';
 | |
| import aiMixin from 'dashboard/mixins/aiMixin';
 | |
| 
 | |
| // utils
 | |
| import { getTypingUsersText } from '../../../helper/commons';
 | |
| import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
 | |
| import { isEscape } from 'shared/helpers/KeyboardHelpers';
 | |
| import { LocalStorage } from 'shared/helpers/localStorage';
 | |
| 
 | |
| // 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: [
 | |
|     conversationMixin,
 | |
|     inboxMixin,
 | |
|     eventListenerMixins,
 | |
|     configMixin,
 | |
|     aiMixin,
 | |
|   ],
 | |
|   props: {
 | |
|     isContactPanelOpen: {
 | |
|       type: Boolean,
 | |
|       default: false,
 | |
|     },
 | |
|   },
 | |
| 
 | |
|   data() {
 | |
|     return {
 | |
|       isLoadingPrevious: true,
 | |
|       heightBeforeLoad: null,
 | |
|       conversationPanel: null,
 | |
|       hasUserScrolled: false,
 | |
|       isProgrammaticScroll: false,
 | |
|       isPopoutReplyBox: false,
 | |
|       messageSentSinceOpened: false,
 | |
|       labelSuggestions: [],
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   computed: {
 | |
|     ...mapGetters({
 | |
|       accountId: 'getCurrentAccountId',
 | |
|       currentChat: 'getSelectedChat',
 | |
|       allConversations: 'getAllConversations',
 | |
|       inboxesList: 'inboxes/getInboxes',
 | |
|       listLoadingStatus: 'getAllMessagesLoaded',
 | |
|       loadingChatList: 'getChatListLoadingStatus',
 | |
|       appIntegrations: 'integrations/getAppIntegrations',
 | |
|       isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
 | |
|       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;
 | |
|     },
 | |
|     getReadMessages() {
 | |
|       return this.readMessages(
 | |
|         this.getMessages,
 | |
|         this.currentChat.agent_last_seen_at
 | |
|       );
 | |
|     },
 | |
|     getUnReadMessages() {
 | |
|       return this.unReadMessages(
 | |
|         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() {
 | |
|     bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
 | |
|     // when a new message comes in, we refetch the label suggestions
 | |
|     bus.$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
 | |
|     bus.$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() {
 | |
|       bus.$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();
 | |
|     },
 | |
|     showPopoutReplyBox() {
 | |
|       this.isPopoutReplyBox = !this.isPopoutReplyBox;
 | |
|     },
 | |
|     closePopoutReplyBox() {
 | |
|       this.isPopoutReplyBox = false;
 | |
|     },
 | |
|     handleKeyEvents(e) {
 | |
|       if (isEscape(e)) {
 | |
|         this.closePopoutReplyBox();
 | |
|       }
 | |
|     },
 | |
|     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('contact-panel-toggle');
 | |
|     },
 | |
|     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;
 | |
|       }
 | |
|       bus.$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>
 | |
| 
 | |
| <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 {
 | |
|   &::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>
 |