# Pull Request Template ## Description This PR includes the following updates: 1. Updated the design system color tokens by introducing new tokens for surfaces, overlays, buttons, labels, and cards, along with refinements to existing shades. 2. Refreshed both light and dark themes with adjusted background, border, and solid colors. 3. Replaced static Inter font files with the Inter variable font (including italic), supporting weights from 100–900. 4. Added custom font weights (420, 440, 460, 520) along with custom typography classes to enable more fine-grained and consistent typography control. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## 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 - [ ] 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 --------- Co-authored-by: Pranav <pranav@chatwoot.com>
187 lines
5.8 KiB
Vue
187 lines
5.8 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,
|
|
stopPaths: ['content_attributes.translations'],
|
|
});
|
|
});
|
|
|
|
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-surface-1">
|
|
<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>
|