diff --git a/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue b/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue
index 9126cad7..f95dd14d 100644
--- a/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue
+++ b/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue
@@ -6,49 +6,234 @@ import { messageTimestamp } from 'shared/helpers/timeHelper';
import BaseBubble from './Base.vue';
import { useMessageContext } from '../provider.js';
-const { content, createdAt } = useMessageContext();
+const { content, contentAttributes, createdAt } = useMessageContext();
const { locale, t } = useI18n();
+// Mirrors Chatwoot v4.14 conversations.activity. Unknown/custom activity types
+// and the unreachable upstream sla.updated branch intentionally use content.
+const ACTIVITY_KEYS = {
+ 'status.resolved': 'STATUS_RESOLVED',
+ 'status.contact_resolved': 'RESOLVED_BY',
+ 'status.open': 'STATUS_OPEN',
+ 'status.pending': 'STATUS_PENDING',
+ 'status.snoozed': 'STATUS_SNOOZED',
+ 'status.auto_resolved_days': 'STATUS_AUTO_RESOLVED_DAYS',
+ 'status.auto_resolved_hours': 'STATUS_AUTO_RESOLVED_HOURS',
+ 'status.auto_resolved_minutes': 'STATUS_AUTO_RESOLVED_MINUTES',
+ 'status.system_auto_open': 'STATUS_SYSTEM_AUTO_OPEN',
+ 'priority.added': 'PRIORITY_ADDED',
+ 'priority.updated': 'PRIORITY_UPDATED',
+ 'priority.removed': 'PRIORITY_REMOVED',
+ 'assignee.self_assigned': 'ASSIGNEE_SELF_ASSIGNED',
+ 'assignee.assigned': 'ASSIGNEE_ASSIGNED',
+ 'assignee.removed': 'ASSIGNEE_REMOVED',
+ 'team.assigned': 'TEAM_ASSIGNED',
+ 'team.assigned_with_assignee': 'TEAM_ASSIGNED_WITH_ASSIGNEE',
+ 'team.removed': 'TEAM_REMOVED',
+ 'labels.added': 'LABELS_ADDED',
+ 'labels.removed': 'LABELS_REMOVED',
+ 'sla.added': 'SLA_ADDED',
+ 'sla.removed': 'SLA_REMOVED',
+ muted: 'MUTED',
+ unmuted: 'UNMUTED',
+ 'captain.resolved': 'CAPTAIN_RESOLVED',
+ 'captain.resolved_with_reason': 'CAPTAIN_RESOLVED_WITH_REASON',
+ 'captain.resolved_by_tool': 'CAPTAIN_RESOLVED_BY_TOOL',
+ 'captain.open': 'CAPTAIN_OPEN',
+ 'captain.open_with_reason': 'CAPTAIN_OPEN_WITH_REASON',
+ 'captain.auto_opened_after_agent_reply':
+ 'CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY',
+ 'agent_bot.error_moved_to_open': 'BOT_WEBHOOK_FAILURE',
+ 'csat.not_sent_due_to_messaging_window': 'CSAT_RESTRICTED',
+ 'auto_resolve.not_sent_due_to_messaging_window': 'AUTO_RESOLVE_RESTRICTED',
+ auto_resolution_message: 'AUTO_RESOLUTION_MESSAGE',
+ 'linear.issue_created': 'LINEAR_CREATED',
+ 'linear.issue_linked': 'LINEAR_LINKED',
+ 'linear.issue_unlinked': 'LINEAR_UNLINKED',
+ 'whatsapp_call.permission_requested': 'WHATSAPP_CALL_PERMISSION_REQUESTED',
+ 'whatsapp_call.permission_granted': 'WHATSAPP_CALL_PERMISSION_GRANTED',
+};
+
+const FIXED_CONTENT_KEYS = {
+ 'Conversation was reopened due to agent bot webhook failure':
+ 'BOT_WEBHOOK_FAILURE',
+ 'Conversation was marked open by system due to an error with the agent bot.':
+ 'BOT_WEBHOOK_FAILURE',
+ 'CSAT survey not sent due to outgoing message restrictions':
+ 'CSAT_RESTRICTED',
+ 'Auto-resolve message not sent due to outgoing message restrictions':
+ 'AUTO_RESOLVE_RESTRICTED',
+ 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.':
+ 'AUTO_RESOLUTION_MESSAGE',
+ 'System reopened the conversation due to a new incoming message.':
+ 'STATUS_SYSTEM_AUTO_OPEN',
+ 'Conversation was marked open automatically after an agent reply':
+ 'CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY',
+};
+
+const CONTENT_PATTERNS = [
+ [
+ /^Conversation was marked resolved by (.+) due to inactivity$/,
+ 'CAPTAIN_RESOLVED',
+ ['userName'],
+ ],
+ [
+ /^Conversation was marked resolved by (.+) \((.+)\)$/,
+ 'CAPTAIN_RESOLVED_WITH_REASON',
+ ['userName', 'reason'],
+ ],
+ [
+ /^Conversation was marked resolved by (.+): (.+)$/,
+ 'CAPTAIN_RESOLVED_BY_TOOL',
+ ['userName', 'reason'],
+ ],
+ [
+ /^Conversation was marked open by (.+) \((.+)\)$/,
+ 'CAPTAIN_OPEN_WITH_REASON',
+ ['userName', 'reason'],
+ ],
+ [/^Conversation was marked open by (.+)$/, 'CAPTAIN_OPEN', ['userName']],
+ [/^Conversation was resolved by (.+)$/, 'RESOLVED_BY', ['contactName']],
+ [
+ /^Conversation was marked resolved by system due to (\d+) days of inactivity$/,
+ 'STATUS_AUTO_RESOLVED_DAYS',
+ ['count'],
+ ],
+ [
+ /^Conversation was marked resolved by system due to (\d+) hours of inactivity$/,
+ 'STATUS_AUTO_RESOLVED_HOURS',
+ ['count'],
+ ],
+ [
+ /^Conversation was marked resolved by system due to (\d+) minutes of inactivity$/,
+ 'STATUS_AUTO_RESOLVED_MINUTES',
+ ['count'],
+ ],
+ [
+ /^Conversation was marked resolved by (.+)$/,
+ 'STATUS_RESOLVED',
+ ['userName'],
+ ],
+ [/^Conversation was reopened by (.+)$/, 'STATUS_OPEN', ['userName']],
+ [
+ /^Conversation was marked as pending by (.+)$/,
+ 'STATUS_PENDING',
+ ['userName'],
+ ],
+ [/^Conversation was snoozed by (.+)$/, 'STATUS_SNOOZED', ['userName']],
+ [
+ /^(.+) set the priority to (.+)$/,
+ 'PRIORITY_ADDED',
+ ['userName', 'newPriority'],
+ ],
+ [
+ /^(.+) changed the priority from (.+) to (.+)$/,
+ 'PRIORITY_UPDATED',
+ ['userName', 'oldPriority', 'newPriority'],
+ ],
+ [/^(.+) removed the priority$/, 'PRIORITY_REMOVED', ['userName']],
+ [
+ /^(.+) self-assigned this conversation$/,
+ 'ASSIGNEE_SELF_ASSIGNED',
+ ['userName'],
+ ],
+ [
+ /^Assigned to (.+) via (.+) by (.+)$/,
+ 'TEAM_ASSIGNED_WITH_ASSIGNEE',
+ ['assigneeName', 'teamName', 'userName'],
+ ],
+ [
+ /^Assigned to (.+) by (.+)$/,
+ 'ASSIGNEE_ASSIGNED',
+ ['assigneeName', 'userName'],
+ ],
+ [/^Conversation unassigned by (.+)$/, 'ASSIGNEE_REMOVED', ['userName']],
+ [/^Unassigned from (.+) by (.+)$/, 'TEAM_REMOVED', ['teamName', 'userName']],
+ [/^(.+) added SLA policy (.+)$/, 'SLA_ADDED', ['userName', 'slaName']],
+ [/^(.+) removed SLA policy (.*)$/, 'SLA_REMOVED', ['userName', 'slaName']],
+ [/^(.+) added (.+)$/, 'LABELS_ADDED', ['userName', 'labels']],
+ [/^(.+) removed (.+)$/, 'LABELS_REMOVED', ['userName', 'labels']],
+ [/^(.+) has muted the conversation$/, 'MUTED', ['userName']],
+ [/^(.+) has unmuted the conversation$/, 'UNMUTED', ['userName']],
+ [
+ /^Linear issue (\S+) was created by (.+)$/,
+ 'LINEAR_CREATED',
+ ['issueId', 'userName'],
+ ],
+ [
+ /^Linear issue (\S+) was linked by (.+)$/,
+ 'LINEAR_LINKED',
+ ['issueId', 'userName'],
+ ],
+ [
+ /^Linear issue (\S+) was unlinked by (.+)$/,
+ 'LINEAR_UNLINKED',
+ ['issueId', 'userName'],
+ ],
+ [
+ /^Sent a call permission request to (.+)\.$/,
+ 'WHATSAPP_CALL_PERMISSION_REQUESTED',
+ ['contactName'],
+ ],
+ [
+ /^(.+) accepted the call permission request\.$/,
+ 'WHATSAPP_CALL_PERMISSION_GRANTED',
+ ['contactName'],
+ ],
+];
+
const localizedContent = computed(() => {
const normalizedContent = String(content.value ?? '');
- const resolvedBy = normalizedContent.match(
- /^Conversation was resolved by (.+)$/
- );
- if (resolvedBy) {
- const name = ['Anonymous Visitor', 'visitor'].includes(resolvedBy[1])
- ? t('CONVERSATION.ACTIVITY.ANONYMOUS_VISITOR')
- : resolvedBy[1];
- return t('CONVERSATION.ACTIVITY.RESOLVED_BY', { name });
- }
+ const structuredActivity = contentAttributes.value?.activity;
if (
- normalizedContent ===
- 'Conversation was reopened due to agent bot webhook failure'
+ structuredActivity &&
+ !Object.hasOwn(ACTIVITY_KEYS, structuredActivity.type)
) {
- return t('CONVERSATION.ACTIVITY.BOT_WEBHOOK_FAILURE');
+ return normalizedContent;
}
- if (
- normalizedContent ===
- 'CSAT survey not sent due to outgoing message restrictions'
- ) {
- return t('CONVERSATION.ACTIVITY.CSAT_RESTRICTED');
- }
- const linear = normalizedContent.match(
- /^Linear issue (\S+) was (created|linked|unlinked) by (.+)$/
- );
- if (linear) {
- const params = {
- issueId: linear[1],
- name: linear[3],
- };
- if (linear[2] === 'created') {
- return t('CONVERSATION.ACTIVITY.LINEAR_CREATED', params);
+
+ let key = structuredActivity
+ ? ACTIVITY_KEYS[structuredActivity.type]
+ : FIXED_CONTENT_KEYS[normalizedContent];
+ let params = structuredActivity;
+
+ if (!structuredActivity && !key) {
+ const matchedPattern = CONTENT_PATTERNS.find(([pattern]) =>
+ pattern.test(normalizedContent)
+ );
+ if (matchedPattern) {
+ const [pattern, patternKey, paramNames] = matchedPattern;
+ const match = normalizedContent.match(pattern);
+ key = patternKey;
+ params = Object.fromEntries(
+ paramNames.map((paramName, index) => [paramName, match[index + 1]])
+ );
}
- if (linear[2] === 'linked') {
- return t('CONVERSATION.ACTIVITY.LINEAR_LINKED', params);
- }
- return t('CONVERSATION.ACTIVITY.LINEAR_UNLINKED', params);
}
+
+ if (key) {
+ params = { ...params };
+ if (Array.isArray(params.labels)) params.labels = params.labels.join(', ');
+ if (['Anonymous Visitor', 'visitor'].includes(params.contactName)) {
+ params.contactName = t('CONVERSATION.ACTIVITY.ANONYMOUS_VISITOR');
+ }
+ if (params.userName === 'Automation System') {
+ params.userName = t('CONVERSATION.ACTIVITY.AUTOMATION_SYSTEM');
+ }
+ ['oldPriority', 'newPriority'].forEach(priorityKey => {
+ const priority = params[priorityKey];
+ if (['urgent', 'high', 'medium', 'low'].includes(priority)) {
+ // eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys
+ params[priorityKey] = t(
+ `CONVERSATION.PRIORITY.OPTIONS.${priority.toUpperCase()}`
+ );
+ }
+ });
+ // eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys
+ return t(`CONVERSATION.ACTIVITY.${key}`, params);
+ }
+
return normalizedContent;
});
diff --git a/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js b/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js
index f762ffa7..50a5e968 100644
--- a/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js
+++ b/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js
@@ -1,10 +1,20 @@
import { config, mount } from '@vue/test-utils';
-import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
+import {
+ afterAll,
+ beforeAll,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ vi,
+} from 'vitest';
+import VueDOMPurifyHTML from 'vue-dompurify-html';
import Activity from '../Activity.vue';
const mocks = vi.hoisted(() => ({
content: '',
+ contentAttributes: {},
}));
vi.mock('shared/helpers/timeHelper', () => ({
@@ -30,6 +40,7 @@ vi.mock('../../provider.js', async () => {
return {
useMessageContext: () => ({
content: ref(mocks.content),
+ contentAttributes: ref(mocks.contentAttributes),
createdAt: ref(1786018000),
}),
};
@@ -38,11 +49,7 @@ vi.mock('../../provider.js', async () => {
const mountActivity = () =>
mount(Activity, {
global: {
- directives: {
- dompurifyHtml: (element, binding) => {
- element.innerHTML = binding.value;
- },
- },
+ plugins: [VueDOMPurifyHTML],
stubs: {
BaseBubble: {
template: '
',
@@ -52,6 +59,12 @@ const mountActivity = () =>
});
describe('Activity bubble', () => {
+ beforeEach(() => {
+ i18n.global.locale.value = 'zh_CN';
+ mocks.content = '';
+ mocks.contentAttributes = {};
+ });
+
it('allows long links in system messages to wrap inside the bubble', () => {
mocks.content = `来源页面:${longURL}`;
const wrapper = mountActivity();
@@ -68,8 +81,36 @@ describe('Activity bubble', () => {
});
it.each([
- ['Conversation was resolved by Anonymous Visitor', '会话已由匿名访客解决'],
- ['Conversation was resolved by Alice', '会话已由Alice解决'],
+ [
+ 'Conversation was resolved by Anonymous Visitor',
+ '会话已由 匿名访客 解决',
+ ],
+ [
+ 'Conversation was marked resolved by Alice',
+ '会话已由 Alice 标记为已解决',
+ ],
+ ['Conversation was reopened by Alice', '会话已由 Alice 重新打开'],
+ [
+ 'Conversation was marked as pending by Alice',
+ '会话已由 Alice 标记为待处理',
+ ],
+ ['Conversation was snoozed by Alice', '会话已由 Alice 推迟'],
+ [
+ 'Conversation was marked resolved by system due to 2 hours of inactivity',
+ '会话因 2 小时无活动已由系统自动解决',
+ ],
+ [
+ 'Alice changed the priority from low to high',
+ 'Alice 将优先级从 低 更改为 高',
+ ],
+ ['Assigned to Bob by Alice', 'Alice 将会话分配给 Bob'],
+ [
+ 'Assigned to Bob via Support by Alice',
+ 'Alice 通过团队 Support 将会话分配给 Bob',
+ ],
+ ['Alice added vip, billing', 'Alice 添加了标签:vip, billing'],
+ ['Alice removed SLA policy Gold', 'Alice 移除了 SLA 策略 Gold'],
+ ['Alice has muted the conversation', 'Alice 已将会话静音'],
[
'Conversation was reopened due to agent bot webhook failure',
'由于客服机器人 Webhook 失败,会话已重新打开',
@@ -78,19 +119,20 @@ describe('Activity bubble', () => {
'CSAT survey not sent due to outgoing message restrictions',
'由于发送消息受限,未发送 CSAT 调查',
],
+ [
+ 'Auto-resolve message not sent due to outgoing message restrictions',
+ '由于发送消息受限,未发送自动解决消息',
+ ],
[
'Linear issue ENG-1 was created by Alice',
'Linear 问题 ENG-1 已由 Alice 创建',
],
[
- 'Linear issue ENG-1 was linked by Alice',
- 'Linear 问题 ENG-1 已由 Alice 关联',
+ 'Conversation was marked resolved by Captain due to inactivity',
+ 'Captain 因无活动将会话标记为已解决',
],
- [
- 'Linear issue ENG-1 was unlinked by Alice',
- 'Linear 问题 ENG-1 已由 Alice 取消关联',
- ],
- ])('localizes known backend activity content: %s', (content, expected) => {
+ ['Sent a call permission request to Bob.', '已向 Bob 发送通话权限请求。'],
+ ])('localizes known legacy activity content: %s', (content, expected) => {
mocks.content = content;
const wrapper = mountActivity();
@@ -99,6 +141,215 @@ describe('Activity bubble', () => {
);
});
+ it.each([
+ [
+ { type: 'status.resolved', userName: 'Alice' },
+ '会话已由 Alice 标记为已解决',
+ ],
+ [
+ { type: 'status.contact_resolved', contactName: 'visitor' },
+ '会话已由 匿名访客 解决',
+ ],
+ [{ type: 'status.open', userName: 'Alice' }, '会话已由 Alice 重新打开'],
+ [
+ { type: 'status.pending', userName: 'Alice' },
+ '会话已由 Alice 标记为待处理',
+ ],
+ [{ type: 'status.snoozed', userName: 'Alice' }, '会话已由 Alice 推迟'],
+ [
+ { type: 'status.auto_resolved_days', count: 2 },
+ '会话因 2 天无活动已由系统自动解决',
+ ],
+ [
+ { type: 'status.auto_resolved_hours', count: 3 },
+ '会话因 3 小时无活动已由系统自动解决',
+ ],
+ [
+ { type: 'status.auto_resolved_minutes', count: 15 },
+ '会话因 15 分钟无活动已由系统自动解决',
+ ],
+ [
+ { type: 'status.system_auto_open' },
+ '收到新的传入消息后,系统重新打开了会话。',
+ ],
+ [
+ { type: 'priority.added', userName: 'Alice', newPriority: 'urgent' },
+ 'Alice 将优先级设为 紧急的',
+ ],
+ [
+ {
+ type: 'priority.updated',
+ userName: 'Alice',
+ oldPriority: 'low',
+ newPriority: 'high',
+ },
+ 'Alice 将优先级从 低 更改为 高',
+ ],
+ [{ type: 'priority.removed', userName: 'Alice' }, 'Alice 移除了优先级'],
+ [
+ { type: 'assignee.self_assigned', userName: 'Alice' },
+ 'Alice 将此会话分配给自己',
+ ],
+ [
+ { type: 'assignee.assigned', userName: 'Alice', assigneeName: 'Bob' },
+ 'Alice 将会话分配给 Bob',
+ ],
+ [{ type: 'assignee.removed', userName: 'Alice' }, 'Alice 取消了会话分配'],
+ [
+ { type: 'team.assigned', userName: 'Alice', teamName: 'Support' },
+ 'Alice 将会话分配给团队 Support',
+ ],
+ [
+ {
+ type: 'team.assigned_with_assignee',
+ userName: 'Alice',
+ teamName: 'Support',
+ assigneeName: 'Bob',
+ },
+ 'Alice 通过团队 Support 将会话分配给 Bob',
+ ],
+ [
+ { type: 'team.removed', userName: 'Alice', teamName: 'Support' },
+ 'Alice 将会话从团队 Support 中移除',
+ ],
+ [
+ { type: 'labels.added', userName: 'Alice', labels: ['vip', 'billing'] },
+ 'Alice 添加了标签:vip, billing',
+ ],
+ [
+ { type: 'labels.removed', userName: 'Alice', labels: 'vip' },
+ 'Alice 移除了标签:vip',
+ ],
+ [
+ { type: 'sla.added', userName: 'Alice', slaName: 'Gold' },
+ 'Alice 添加了 SLA 策略 Gold',
+ ],
+ [
+ { type: 'sla.removed', userName: 'Alice', slaName: 'Gold' },
+ 'Alice 移除了 SLA 策略 Gold',
+ ],
+ [{ type: 'muted', userName: 'Alice' }, 'Alice 已将会话静音'],
+ [{ type: 'unmuted', userName: 'Alice' }, 'Alice 已取消会话静音'],
+ [
+ { type: 'captain.resolved', userName: 'Captain' },
+ 'Captain 因无活动将会话标记为已解决',
+ ],
+ [
+ {
+ type: 'captain.resolved_with_reason',
+ userName: 'Captain',
+ reason: 'Done',
+ },
+ 'Captain 将会话标记为已解决(Done)',
+ ],
+ [
+ { type: 'captain.resolved_by_tool', userName: 'Captain', reason: 'Done' },
+ 'Captain 将会话标记为已解决:Done',
+ ],
+ [{ type: 'captain.open', userName: 'Captain' }, 'Captain 将会话标记为打开'],
+ [
+ {
+ type: 'captain.open_with_reason',
+ userName: 'Captain',
+ reason: 'Reply',
+ },
+ 'Captain 将会话标记为打开(Reply)',
+ ],
+ [
+ { type: 'captain.auto_opened_after_agent_reply' },
+ '客服回复后,会话已自动标记为打开',
+ ],
+ [
+ { type: 'agent_bot.error_moved_to_open' },
+ '由于客服机器人 Webhook 失败,会话已重新打开',
+ ],
+ [
+ { type: 'csat.not_sent_due_to_messaging_window' },
+ '由于发送消息受限,未发送 CSAT 调查',
+ ],
+ [
+ { type: 'auto_resolve.not_sent_due_to_messaging_window' },
+ '由于发送消息受限,未发送自动解决消息',
+ ],
+ [
+ { type: 'auto_resolution_message' },
+ '会话已有一段时间没有活动,现将自动解决。如需进一步帮助,请发起新会话。',
+ ],
+ [
+ { type: 'linear.issue_created', issueId: 'ENG-1', userName: 'Alice' },
+ 'Linear 问题 ENG-1 已由 Alice 创建',
+ ],
+ [
+ { type: 'linear.issue_linked', issueId: 'ENG-1', userName: 'Alice' },
+ 'Linear 问题 ENG-1 已由 Alice 关联',
+ ],
+ [
+ { type: 'linear.issue_unlinked', issueId: 'ENG-1', userName: 'Alice' },
+ 'Linear 问题 ENG-1 已由 Alice 取消关联',
+ ],
+ [
+ { type: 'whatsapp_call.permission_requested', contactName: 'Bob' },
+ '已向 Bob 发送通话权限请求。',
+ ],
+ [
+ { type: 'whatsapp_call.permission_granted', contactName: 'Bob' },
+ 'Bob 已接受通话权限请求。',
+ ],
+ ])('localizes a structured activity type', (activity, expected) => {
+ mocks.content = 'Untranslated fallback';
+ mocks.contentAttributes = { activity };
+ const wrapper = mountActivity();
+
+ expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe(
+ expected
+ );
+ });
+
+ it.each([
+ ['custom.unknown', 'Conversation was resolved by Alice'],
+ ['sla.updated', 'Alice added SLA policy Gold'],
+ ['toString', 'Conversation was reopened by Alice'],
+ ['constructor', 'Conversation was marked as pending by Alice'],
+ ])(
+ 'falls back to stored content for unsupported structured type %s',
+ (type, content) => {
+ mocks.content = content;
+ mocks.contentAttributes = { activity: { type } };
+
+ expect(mountActivity().get('span').text()).toBe(content);
+ }
+ );
+
+ it('uses the English activity catalog when the dashboard locale is English', () => {
+ i18n.global.locale.value = 'en';
+ mocks.contentAttributes = {
+ activity: {
+ type: 'priority.updated',
+ userName: 'Automation System',
+ oldPriority: 'low',
+ newPriority: 'high',
+ },
+ };
+
+ expect(mountActivity().get('span').text()).toBe(
+ 'Automation System changed the priority from Low to High'
+ );
+ });
+
+ it('sanitizes malicious dynamic activity names with the real directive', () => {
+ mocks.contentAttributes = {
+ activity: {
+ type: 'status.resolved',
+ userName: 'Alice
',
+ },
+ };
+ const content = mountActivity().get('span');
+
+ expect(content.text()).toBe('会话已由 Alice 标记为已解决');
+ expect(content.get('img').attributes('onerror')).toBeUndefined();
+ expect(window.activityXss).toBeUndefined();
+ });
+
it('renders an empty activity when content is null', () => {
mocks.content = null;
const wrapper = mountActivity();
diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json
index fdfbcad3..71b0b70b 100644
--- a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -4,12 +4,46 @@
"CSAT_REPLY_MESSAGE": "Please rate the conversation",
"ACTIVITY": {
"ANONYMOUS_VISITOR": "Anonymous visitor",
- "RESOLVED_BY": "Conversation was resolved by {name}",
+ "AUTOMATION_SYSTEM": "Automation System",
+ "RESOLVED_BY": "Conversation was resolved by {contactName}",
+ "STATUS_RESOLVED": "Conversation was marked resolved by {userName}",
+ "STATUS_OPEN": "Conversation was reopened by {userName}",
+ "STATUS_PENDING": "Conversation was marked as pending by {userName}",
+ "STATUS_SNOOZED": "Conversation was snoozed by {userName}",
+ "STATUS_AUTO_RESOLVED_DAYS": "Conversation was marked resolved by system due to {count} days of inactivity",
+ "STATUS_AUTO_RESOLVED_HOURS": "Conversation was marked resolved by system due to {count} hours of inactivity",
+ "STATUS_AUTO_RESOLVED_MINUTES": "Conversation was marked resolved by system due to {count} minutes of inactivity",
+ "STATUS_SYSTEM_AUTO_OPEN": "System reopened the conversation due to a new incoming message.",
+ "PRIORITY_ADDED": "{userName} set the priority to {newPriority}",
+ "PRIORITY_UPDATED": "{userName} changed the priority from {oldPriority} to {newPriority}",
+ "PRIORITY_REMOVED": "{userName} removed the priority",
+ "ASSIGNEE_SELF_ASSIGNED": "{userName} self-assigned this conversation",
+ "ASSIGNEE_ASSIGNED": "Assigned to {assigneeName} by {userName}",
+ "ASSIGNEE_REMOVED": "Conversation unassigned by {userName}",
+ "TEAM_ASSIGNED": "Assigned to {teamName} by {userName}",
+ "TEAM_ASSIGNED_WITH_ASSIGNEE": "Assigned to {assigneeName} via {teamName} by {userName}",
+ "TEAM_REMOVED": "Unassigned from {teamName} by {userName}",
+ "LABELS_ADDED": "{userName} added {labels}",
+ "LABELS_REMOVED": "{userName} removed {labels}",
+ "SLA_ADDED": "{userName} added SLA policy {slaName}",
+ "SLA_REMOVED": "{userName} removed SLA policy {slaName}",
+ "MUTED": "{userName} has muted the conversation",
+ "UNMUTED": "{userName} has unmuted the conversation",
+ "CAPTAIN_RESOLVED": "Conversation was marked resolved by {userName} due to inactivity",
+ "CAPTAIN_RESOLVED_WITH_REASON": "Conversation was marked resolved by {userName} ({reason})",
+ "CAPTAIN_RESOLVED_BY_TOOL": "Conversation was marked resolved by {userName}: {reason}",
+ "CAPTAIN_OPEN": "Conversation was marked open by {userName}",
+ "CAPTAIN_OPEN_WITH_REASON": "Conversation was marked open by {userName} ({reason})",
+ "CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY": "Conversation was marked open automatically after an agent reply",
"BOT_WEBHOOK_FAILURE": "Conversation was reopened because the agent bot webhook failed",
"CSAT_RESTRICTED": "CSAT survey was not sent due to outgoing message restrictions",
- "LINEAR_CREATED": "Linear issue {issueId} was created by {name}",
- "LINEAR_LINKED": "Linear issue {issueId} was linked by {name}",
- "LINEAR_UNLINKED": "Linear issue {issueId} was unlinked by {name}"
+ "AUTO_RESOLVE_RESTRICTED": "Auto-resolve message was not sent due to outgoing message restrictions",
+ "AUTO_RESOLUTION_MESSAGE": "Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.",
+ "LINEAR_CREATED": "Linear issue {issueId} was created by {userName}",
+ "LINEAR_LINKED": "Linear issue {issueId} was linked by {userName}",
+ "LINEAR_UNLINKED": "Linear issue {issueId} was unlinked by {userName}",
+ "WHATSAPP_CALL_PERMISSION_REQUESTED": "Sent a call permission request to {contactName}.",
+ "WHATSAPP_CALL_PERMISSION_GRANTED": "{contactName} accepted the call permission request."
},
"404": "Sorry, we cannot find the conversation. Please try again",
"SWITCH_VIEW_LAYOUT": "Switch the layout",
diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json
index 181bdcdd..d4e80997 100644
--- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json
+++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json
@@ -4,12 +4,46 @@
"CSAT_REPLY_MESSAGE": "请为会话评分",
"ACTIVITY": {
"ANONYMOUS_VISITOR": "匿名访客",
- "RESOLVED_BY": "会话已由{name}解决",
+ "AUTOMATION_SYSTEM": "自动化系统",
+ "RESOLVED_BY": "会话已由 {contactName} 解决",
+ "STATUS_RESOLVED": "会话已由 {userName} 标记为已解决",
+ "STATUS_OPEN": "会话已由 {userName} 重新打开",
+ "STATUS_PENDING": "会话已由 {userName} 标记为待处理",
+ "STATUS_SNOOZED": "会话已由 {userName} 推迟",
+ "STATUS_AUTO_RESOLVED_DAYS": "会话因 {count} 天无活动已由系统自动解决",
+ "STATUS_AUTO_RESOLVED_HOURS": "会话因 {count} 小时无活动已由系统自动解决",
+ "STATUS_AUTO_RESOLVED_MINUTES": "会话因 {count} 分钟无活动已由系统自动解决",
+ "STATUS_SYSTEM_AUTO_OPEN": "收到新的传入消息后,系统重新打开了会话。",
+ "PRIORITY_ADDED": "{userName} 将优先级设为 {newPriority}",
+ "PRIORITY_UPDATED": "{userName} 将优先级从 {oldPriority} 更改为 {newPriority}",
+ "PRIORITY_REMOVED": "{userName} 移除了优先级",
+ "ASSIGNEE_SELF_ASSIGNED": "{userName} 将此会话分配给自己",
+ "ASSIGNEE_ASSIGNED": "{userName} 将会话分配给 {assigneeName}",
+ "ASSIGNEE_REMOVED": "{userName} 取消了会话分配",
+ "TEAM_ASSIGNED": "{userName} 将会话分配给团队 {teamName}",
+ "TEAM_ASSIGNED_WITH_ASSIGNEE": "{userName} 通过团队 {teamName} 将会话分配给 {assigneeName}",
+ "TEAM_REMOVED": "{userName} 将会话从团队 {teamName} 中移除",
+ "LABELS_ADDED": "{userName} 添加了标签:{labels}",
+ "LABELS_REMOVED": "{userName} 移除了标签:{labels}",
+ "SLA_ADDED": "{userName} 添加了 SLA 策略 {slaName}",
+ "SLA_REMOVED": "{userName} 移除了 SLA 策略 {slaName}",
+ "MUTED": "{userName} 已将会话静音",
+ "UNMUTED": "{userName} 已取消会话静音",
+ "CAPTAIN_RESOLVED": "{userName} 因无活动将会话标记为已解决",
+ "CAPTAIN_RESOLVED_WITH_REASON": "{userName} 将会话标记为已解决({reason})",
+ "CAPTAIN_RESOLVED_BY_TOOL": "{userName} 将会话标记为已解决:{reason}",
+ "CAPTAIN_OPEN": "{userName} 将会话标记为打开",
+ "CAPTAIN_OPEN_WITH_REASON": "{userName} 将会话标记为打开({reason})",
+ "CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY": "客服回复后,会话已自动标记为打开",
"BOT_WEBHOOK_FAILURE": "由于客服机器人 Webhook 失败,会话已重新打开",
"CSAT_RESTRICTED": "由于发送消息受限,未发送 CSAT 调查",
- "LINEAR_CREATED": "Linear 问题 {issueId} 已由 {name} 创建",
- "LINEAR_LINKED": "Linear 问题 {issueId} 已由 {name} 关联",
- "LINEAR_UNLINKED": "Linear 问题 {issueId} 已由 {name} 取消关联"
+ "AUTO_RESOLVE_RESTRICTED": "由于发送消息受限,未发送自动解决消息",
+ "AUTO_RESOLUTION_MESSAGE": "会话已有一段时间没有活动,现将自动解决。如需进一步帮助,请发起新会话。",
+ "LINEAR_CREATED": "Linear 问题 {issueId} 已由 {userName} 创建",
+ "LINEAR_LINKED": "Linear 问题 {issueId} 已由 {userName} 关联",
+ "LINEAR_UNLINKED": "Linear 问题 {issueId} 已由 {userName} 取消关联",
+ "WHATSAPP_CALL_PERMISSION_REQUESTED": "已向 {contactName} 发送通话权限请求。",
+ "WHATSAPP_CALL_PERMISSION_GRANTED": "{contactName} 已接受通话权限请求。"
},
"404": "抱歉,我们找不到对话。请重试",
"SWITCH_VIEW_LAYOUT": "切换布局",