mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			152 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			152 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<script setup>
 | 
						|
import { defineProps, computed, ref } from 'vue';
 | 
						|
import Message from './Message.vue';
 | 
						|
import { MESSAGE_TYPES } from './constants.js';
 | 
						|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
 | 
						|
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * Props definition for the component
 | 
						|
 * @typedef {Object} Props
 | 
						|
 * @property {Array} readMessages - Array of read messages
 | 
						|
 * @property {Array} unReadMessages - Array of unread messages
 | 
						|
 * @property {Number} currentUserId - ID of the current user
 | 
						|
 * @property {Boolean} isAnEmailChannel - Whether this is an email channel
 | 
						|
 * @property {Object} inboxSupportsReplyTo - Inbox reply support configuration
 | 
						|
 * @property {Array} messages - Array of all messages [These are not in camelcase]
 | 
						|
 */
 | 
						|
const props = defineProps({
 | 
						|
  currentUserId: {
 | 
						|
    type: Number,
 | 
						|
    required: true,
 | 
						|
  },
 | 
						|
  firstUnreadId: {
 | 
						|
    type: Number,
 | 
						|
    default: null,
 | 
						|
  },
 | 
						|
  isAnEmailChannel: {
 | 
						|
    type: Boolean,
 | 
						|
    default: false,
 | 
						|
  },
 | 
						|
  inboxSupportsReplyTo: {
 | 
						|
    type: Object,
 | 
						|
    default: () => ({ incoming: false, outgoing: false }),
 | 
						|
  },
 | 
						|
  messages: {
 | 
						|
    type: Array,
 | 
						|
    default: () => [],
 | 
						|
  },
 | 
						|
});
 | 
						|
 | 
						|
const emit = defineEmits(['retry']);
 | 
						|
const store = useStore();
 | 
						|
 | 
						|
const fetchingConversations = ref(new Set());
 | 
						|
const allMessages = computed(() => {
 | 
						|
  return useCamelCase(props.messages, { deep: true });
 | 
						|
});
 | 
						|
 | 
						|
const currentChat = useMapGetter('getSelectedChat');
 | 
						|
 | 
						|
/**
 | 
						|
 * Determines if a message should be grouped with the next message
 | 
						|
 * @param {Number} index - Index of the current message
 | 
						|
 * @param {Array} searchList - Array of messages to check
 | 
						|
 * @returns {Boolean} - Whether the message should be grouped with next
 | 
						|
 */
 | 
						|
const shouldGroupWithNext = (index, searchList) => {
 | 
						|
  if (index === searchList.length - 1) return false;
 | 
						|
 | 
						|
  const current = searchList[index];
 | 
						|
  const next = searchList[index + 1];
 | 
						|
 | 
						|
  if (next.status === 'failed') return false;
 | 
						|
 | 
						|
  const nextSenderId = next.senderId ?? next.sender?.id;
 | 
						|
  const currentSenderId = current.senderId ?? current.sender?.id;
 | 
						|
  const hasSameSender = nextSenderId === currentSenderId;
 | 
						|
 | 
						|
  const nextMessageType = next.messageType;
 | 
						|
  const currentMessageType = current.messageType;
 | 
						|
 | 
						|
  const areBothTemplates =
 | 
						|
    nextMessageType === MESSAGE_TYPES.TEMPLATE &&
 | 
						|
    currentMessageType === MESSAGE_TYPES.TEMPLATE;
 | 
						|
 | 
						|
  if (!hasSameSender || areBothTemplates) return false;
 | 
						|
 | 
						|
  if (currentMessageType !== nextMessageType) return false;
 | 
						|
 | 
						|
  // Check if messages are in the same minute by rounding down to nearest minute
 | 
						|
  return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets the message that was replied to
 | 
						|
 * @param {Object} parentMessage - The message containing the reply reference
 | 
						|
 * @returns {Object|null} - The message being replied to, or null if not found
 | 
						|
 */
 | 
						|
const getInReplyToMessage = parentMessage => {
 | 
						|
  const inReplyToMessageId =
 | 
						|
    parentMessage?.contentAttributes?.inReplyTo ??
 | 
						|
    parentMessage?.content_attributes?.in_reply_to;
 | 
						|
 | 
						|
  if (!inReplyToMessageId) return null;
 | 
						|
 | 
						|
  // 1. Check props messages (already camelCased via allMessages)
 | 
						|
  const foundInProps = allMessages.value?.find(
 | 
						|
    m => m.id === inReplyToMessageId
 | 
						|
  );
 | 
						|
  if (foundInProps) return foundInProps;
 | 
						|
 | 
						|
  // 2. Check store messages
 | 
						|
  const foundInStore = currentChat.value?.messages?.find(
 | 
						|
    m => m.id === inReplyToMessageId
 | 
						|
  );
 | 
						|
  if (foundInStore) return useCamelCase(foundInStore);
 | 
						|
 | 
						|
  // 3. Fetch if not currently fetching for this conversation
 | 
						|
  const conversationId = currentChat.value?.id;
 | 
						|
  if (
 | 
						|
    conversationId &&
 | 
						|
    !currentChat.value.allMessagesLoaded &&
 | 
						|
    !fetchingConversations.value.has(conversationId)
 | 
						|
  ) {
 | 
						|
    fetchingConversations.value.add(conversationId);
 | 
						|
    store
 | 
						|
      .dispatch('fetchPreviousMessages', {
 | 
						|
        conversationId,
 | 
						|
        before: currentChat.value.messages?.[0]?.id ?? null,
 | 
						|
      })
 | 
						|
      .finally(() => {
 | 
						|
        fetchingConversations.value.delete(conversationId);
 | 
						|
      });
 | 
						|
  }
 | 
						|
 | 
						|
  return null;
 | 
						|
};
 | 
						|
</script>
 | 
						|
 | 
						|
<template>
 | 
						|
  <ul class="px-4 bg-n-background">
 | 
						|
    <slot name="beforeAll" />
 | 
						|
    <template v-for="(message, index) in allMessages" :key="message.id">
 | 
						|
      <slot
 | 
						|
        v-if="firstUnreadId && message.id === firstUnreadId"
 | 
						|
        name="unreadBadge"
 | 
						|
      />
 | 
						|
      <Message
 | 
						|
        v-bind="message"
 | 
						|
        :is-email-inbox="isAnEmailChannel"
 | 
						|
        :in-reply-to="getInReplyToMessage(message)"
 | 
						|
        :group-with-next="shouldGroupWithNext(index, allMessages)"
 | 
						|
        :inbox-supports-reply-to="inboxSupportsReplyTo"
 | 
						|
        :current-user-id="currentUserId"
 | 
						|
        data-clarity-mask="True"
 | 
						|
        @retry="emit('retry', message)"
 | 
						|
      />
 | 
						|
    </template>
 | 
						|
    <slot name="after" />
 | 
						|
  </ul>
 | 
						|
</template>
 |