Files
gochat/frontend/app/javascript/dashboard/helper/conversationHelper.js
T
rogee f79c80519d
Build and publish Docker images / Build and publish images (push) Successful in 2m48s
feat(frontend): add compact anonymous conversation labels
2026-09-15 13:30:49 +08:00

170 lines
5.4 KiB
JavaScript

/**
* Determines the last non-activity message between store and API messages.
* @param {Object} messageInStore - The last non-activity message from the store.
* @param {Object} messageFromAPI - The last non-activity message from the API.
* @returns {Object} The latest non-activity message.
*/
const getLastNonActivityMessage = (messageInStore, messageFromAPI) => {
// If both API value and store value for last non activity message
// are available, then return the latest one.
if (messageInStore && messageFromAPI) {
return messageInStore.created_at >= messageFromAPI.created_at
? messageInStore
: messageFromAPI;
}
// Otherwise, return whichever is available
return messageInStore || messageFromAPI;
};
/**
* Filters out duplicate source messages from an array of messages.
* @param {Array} messages - The array of messages to filter.
* @returns {Array} An array of messages without duplicates.
*/
export const filterDuplicateSourceMessages = (messages = []) => {
const messagesWithoutDuplicates = [];
// We cannot use Map or any short hand method as it returns the last message with the duplicate ID
// We should return the message with smaller id when there is a duplicate
messages.forEach(m1 => {
if (m1.source_id) {
const index = messagesWithoutDuplicates.findIndex(
m2 => m1.source_id === m2.source_id
);
if (index < 0) {
messagesWithoutDuplicates.push(m1);
}
} else {
messagesWithoutDuplicates.push(m1);
}
});
return messagesWithoutDuplicates;
};
/**
* Retrieves the last message from a conversation, prioritizing non-activity messages.
* @param {Object} m - The conversation object containing messages.
* @returns {Object} The last message of the conversation.
*/
export const getLastMessage = m => {
const lastMessageIncludingActivity = m.messages[m.messages.length - 1];
const nonActivityMessages = m.messages.filter(
message => message.message_type !== 2
);
const lastNonActivityMessageInStore =
nonActivityMessages[nonActivityMessages.length - 1];
const lastNonActivityMessageFromAPI = m.last_non_activity_message;
// If API value and store value for last non activity message
// is empty, then return the last activity message
if (!lastNonActivityMessageInStore && !lastNonActivityMessageFromAPI) {
return lastMessageIncludingActivity;
}
return getLastNonActivityMessage(
lastNonActivityMessageInStore,
lastNonActivityMessageFromAPI
);
};
const anonymousContactNames = new Set([
'',
'匿名',
'匿名客户',
'Anonymous Visitor',
'商务通访客',
]);
const firstAttribute = (attributes, ...keys) => {
const value = keys
.map(key => attributes?.[key])
.find(item => typeof item === 'string' && item.trim());
return value?.trim() || '';
};
const contactAttributes = contact => ({
...(contact?.additional_attributes || {}),
...(contact?.additionalAttributes || {}),
...(contact?.custom_attributes || {}),
...(contact?.customAttributes || {}),
});
const visitorLocation = attributes => {
const province = firstAttribute(attributes, 'visitor_province');
const city = firstAttribute(attributes, 'visitor_city');
if (province || city) {
if (!city || !province || city === province || city.startsWith(province)) {
return city || province;
}
return `${province}${city}`;
}
return firstAttribute(attributes, 'swt_ip_location');
};
const shortConversationCode = conversationId => {
const value = String(conversationId ?? '').trim();
if (!/^[1-9]\d*$/.test(value)) {
return '';
}
const numericValue = Number(value);
return Number.isSafeInteger(numericValue)
? numericValue.toString(36).toUpperCase()
: '';
};
/**
* Adds a stable, compact session code only to generated anonymous names.
* The stored contact name remains unchanged.
*/
export const getConversationContactDisplayName = (contact, conversationId) => {
const name = String(contact?.name || '').trim();
const attributes = contactAttributes(contact);
const source = firstAttribute(attributes, 'visitor_name_source');
const isGeneratedName =
anonymousContactNames.has(name) ||
source === 'ip_geolocation' ||
source === 'anonymous_fallback';
if (!isGeneratedName) {
return name;
}
const code = shortConversationCode(conversationId);
if (!code) {
return name || '匿名';
}
const location =
visitorLocation(attributes) ||
(source === 'ip_geolocation' && name.endsWith('客户')
? name.slice(0, -2)
: '');
return location ? `${location}·${code}` : `匿名${code}`;
};
/**
* Filters messages that have been read by the agent.
* @param {Array} messages - The array of messages to filter.
* @param {number} agentLastSeenAt - The timestamp of when the agent last saw the messages.
* @returns {Array} An array of read messages.
*/
export const getReadMessages = (messages, agentLastSeenAt) => {
return messages.filter(
message => message.created_at * 1000 <= agentLastSeenAt * 1000
);
};
/**
* Filters messages that have not been read by the agent.
* @param {Array} messages - The array of messages to filter.
* @param {number} agentLastSeenAt - The timestamp of when the agent last saw the messages.
* @returns {Array} An array of unread messages.
*/
export const getUnreadMessages = (messages, agentLastSeenAt) => {
return messages.filter(
message => message.created_at * 1000 > agentLastSeenAt * 1000
);
};