mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 10:42:38 +00:00
# load reply to message ## Description When replayed message is more old, not show content ## Type of change Please delete options that are not relevant. - [X] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? I run in my development and production envinronment with unoapi --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
184 lines
5.7 KiB
Vue
184 lines
5.7 KiB
Vue
<script setup>
|
|
import { defineProps, computed, reactive } from 'vue';
|
|
import Message from './Message.vue';
|
|
import { MESSAGE_TYPES } from './constants.js';
|
|
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
|
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
|
import MessageApi from 'dashboard/api/inbox/message.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 allMessages = computed(() => {
|
|
return useCamelCase(props.messages, { deep: true });
|
|
});
|
|
|
|
const currentChat = useMapGetter('getSelectedChat');
|
|
|
|
// Cache for fetched reply messages to avoid duplicate API calls
|
|
const fetchedReplyMessages = reactive(new Map());
|
|
|
|
/**
|
|
* Fetches a specific message from the API by trying to get messages around it
|
|
* @param {number} messageId - The ID of the message to fetch
|
|
* @param {number} conversationId - The ID of the conversation
|
|
* @returns {Promise<Object|null>} - The fetched message or null if not found/error
|
|
*/
|
|
const fetchReplyMessage = async (messageId, conversationId) => {
|
|
// Return cached result if already fetched
|
|
if (fetchedReplyMessages.has(messageId)) {
|
|
return fetchedReplyMessages.get(messageId);
|
|
}
|
|
|
|
try {
|
|
const response = await MessageApi.getPreviousMessages({
|
|
conversationId,
|
|
before: messageId + 100,
|
|
after: messageId - 100,
|
|
});
|
|
|
|
const messages = response.data?.payload || [];
|
|
const targetMessage = messages.find(msg => msg.id === messageId);
|
|
|
|
if (targetMessage) {
|
|
const camelCaseMessage = useCamelCase(targetMessage);
|
|
fetchedReplyMessages.set(messageId, camelCaseMessage);
|
|
return camelCaseMessage;
|
|
}
|
|
|
|
// Cache null result to avoid repeated API calls
|
|
fetchedReplyMessages.set(messageId, null);
|
|
return null;
|
|
} catch (error) {
|
|
fetchedReplyMessages.set(messageId, null);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 => {
|
|
if (!parentMessage) return null;
|
|
|
|
const inReplyToMessageId =
|
|
parentMessage.contentAttributes?.inReplyTo ??
|
|
parentMessage.content_attributes?.in_reply_to;
|
|
|
|
if (!inReplyToMessageId) return null;
|
|
|
|
// Try to find in current messages first
|
|
let replyMessage = props.messages?.find(msg => msg.id === inReplyToMessageId);
|
|
|
|
// Then try store messages
|
|
if (!replyMessage && currentChat.value?.messages) {
|
|
replyMessage = currentChat.value.messages.find(
|
|
msg => msg.id === inReplyToMessageId
|
|
);
|
|
}
|
|
|
|
// Then check fetch cache
|
|
if (!replyMessage && fetchedReplyMessages.has(inReplyToMessageId)) {
|
|
replyMessage = fetchedReplyMessages.get(inReplyToMessageId);
|
|
}
|
|
|
|
// If still not found and we have conversation context, fetch it
|
|
if (!replyMessage && currentChat.value?.id) {
|
|
fetchReplyMessage(inReplyToMessageId, currentChat.value.id);
|
|
return null; // Let UI handle loading state
|
|
}
|
|
|
|
return replyMessage ? useCamelCase(replyMessage) : 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>
|