/** * 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 ); };