feat(frontend): add compact anonymous conversation labels
Build and publish Docker images / Build and publish images (push) Successful in 2m48s
Build and publish Docker images / Build and publish images (push) Successful in 2m48s
This commit is contained in:
+7
-1
@@ -4,6 +4,7 @@ import { getInboxIconByType } from 'dashboard/helper/inbox';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||
import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
|
||||
import { getConversationContactDisplayName } from 'dashboard/helper/conversationHelper';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
@@ -37,7 +38,12 @@ const cardMessagePreviewWithMetaRef = ref(null);
|
||||
|
||||
const currentContact = computed(() => props.contact);
|
||||
|
||||
const currentContactName = computed(() => currentContact.value?.name);
|
||||
const currentContactName = computed(() =>
|
||||
getConversationContactDisplayName(
|
||||
currentContact.value,
|
||||
props.conversation?.id
|
||||
)
|
||||
);
|
||||
const currentContactThumbnail = computed(() => currentContact.value?.thumbnail);
|
||||
const currentContactStatus = computed(
|
||||
() => currentContact.value?.availabilityStatus
|
||||
|
||||
+8
-2
@@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { getLastMessage } from 'dashboard/helper/conversationHelper';
|
||||
import {
|
||||
getConversationContactDisplayName,
|
||||
getLastMessage,
|
||||
} from 'dashboard/helper/conversationHelper';
|
||||
import CardAvatar from './CardAvatar.vue';
|
||||
import CardContent from './CardContent.vue';
|
||||
import CardLabels from './CardLabelsV5.vue';
|
||||
@@ -33,6 +36,9 @@ const emit = defineEmits([
|
||||
]);
|
||||
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
const currentContactName = computed(() =>
|
||||
getConversationContactDisplayName(props.currentContact, props.chat.id)
|
||||
);
|
||||
const showLabelsSection = computed(() => props.chat.labels?.length > 0);
|
||||
|
||||
const voiceCallData = computed(() => {
|
||||
@@ -155,7 +161,7 @@ const selectedModel = computed({
|
||||
<h4
|
||||
class="text-heading-3 my-0 min-w-0 flex-1 capitalize truncate text-n-slate-12 font-medium"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
{{ currentContactName }}
|
||||
</h4>
|
||||
<TimeAgo
|
||||
:conversation-id="chat.id"
|
||||
|
||||
+9
-2
@@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { getLastMessage } from 'dashboard/helper/conversationHelper';
|
||||
import {
|
||||
getConversationContactDisplayName,
|
||||
getLastMessage,
|
||||
} from 'dashboard/helper/conversationHelper';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import MessagePreview from './MessagePreview.vue';
|
||||
import InboxName from '../InboxName.vue';
|
||||
@@ -34,6 +37,10 @@ const emit = defineEmits([
|
||||
|
||||
const hovered = ref(false);
|
||||
|
||||
const currentContactName = computed(() =>
|
||||
getConversationContactDisplayName(props.currentContact, props.chat.id)
|
||||
);
|
||||
|
||||
const unreadCount = computed(() => props.chat.unread_count);
|
||||
const hasUnread = computed(() => unreadCount.value > 0);
|
||||
const lastMessageInChat = computed(() => getLastMessage(props.chat));
|
||||
@@ -174,7 +181,7 @@ watch(
|
||||
class="conversation--user text-sm my-0 min-w-0 flex-1 capitalize text-ellipsis overflow-hidden whitespace-nowrap text-n-slate-12"
|
||||
:class="hasUnread ? 'font-semibold' : 'font-medium'"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
{{ currentContactName }}
|
||||
</h4>
|
||||
<TimeAgo
|
||||
:last-activity-timestamp="chat.timestamp"
|
||||
|
||||
+6
-1
@@ -10,6 +10,7 @@ import Avatar from 'next/avatar/Avatar.vue';
|
||||
import SLACardLabel from './components/SLACardLabel.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
import { getConversationContactDisplayName } from 'dashboard/helper/conversationHelper';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { useInbox } from 'dashboard/composables/useInbox';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -71,6 +72,10 @@ const currentContact = computed(() =>
|
||||
store.getters['contacts/getContact'](props.chat?.meta?.sender?.id)
|
||||
);
|
||||
|
||||
const currentContactName = computed(() =>
|
||||
getConversationContactDisplayName(currentContact.value, props.chat?.id)
|
||||
);
|
||||
|
||||
const isSnoozed = computed(
|
||||
() => currentChat.value.status === wootConstants.STATUS_TYPE.SNOOZED
|
||||
);
|
||||
@@ -132,7 +137,7 @@ const copyConversationId = async () => {
|
||||
<span
|
||||
class="text-sm font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
{{ currentContactName }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
|
||||
@@ -69,6 +69,81 @@ export const getLastMessage = m => {
|
||||
);
|
||||
};
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
filterDuplicateSourceMessages,
|
||||
getConversationContactDisplayName,
|
||||
getLastMessage,
|
||||
getReadMessages,
|
||||
getUnreadMessages,
|
||||
@@ -37,6 +38,36 @@ describe('conversationHelper', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getConversationContactDisplayName', () => {
|
||||
it('formats a geolocated anonymous visitor with a base36 session code', () => {
|
||||
expect(
|
||||
getConversationContactDisplayName(
|
||||
{
|
||||
name: '河北邯郸客户',
|
||||
additional_attributes: {
|
||||
visitor_name_source: 'ip_geolocation',
|
||||
visitor_province: '河北',
|
||||
visitor_city: '邯郸',
|
||||
},
|
||||
},
|
||||
1296
|
||||
)
|
||||
).toBe('河北邯郸·100');
|
||||
});
|
||||
|
||||
it('uses the anonymous label when location is unavailable', () => {
|
||||
expect(
|
||||
getConversationContactDisplayName({ name: '商务通访客' }, 36)
|
||||
).toBe('匿名10');
|
||||
});
|
||||
|
||||
it('keeps a real contact name unchanged', () => {
|
||||
expect(getConversationContactDisplayName({ name: '张三' }, 1296)).toBe(
|
||||
'张三'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readMessages', () => {
|
||||
it('should return read messages if conversation is passed', () => {
|
||||
expect(
|
||||
|
||||
Reference in New Issue
Block a user