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 3528276b..9126cad7 100644
--- a/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue
+++ b/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue
@@ -8,7 +8,49 @@ import { useMessageContext } from '../provider.js';
const { content, createdAt } = useMessageContext();
-const { locale } = useI18n();
+const { locale, t } = useI18n();
+
+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 });
+ }
+ if (
+ normalizedContent ===
+ 'Conversation was reopened due to agent bot webhook failure'
+ ) {
+ return t('CONVERSATION.ACTIVITY.BOT_WEBHOOK_FAILURE');
+ }
+ 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);
+ }
+ if (linear[2] === 'linked') {
+ return t('CONVERSATION.ACTIVITY.LINEAR_LINKED', params);
+ }
+ return t('CONVERSATION.ACTIVITY.LINEAR_UNLINKED', params);
+ }
+ return normalizedContent;
+});
const readableTime = computed(() => {
if (locale.value === 'zh_CN') {
@@ -28,8 +70,8 @@ const readableTime = computed(() => {
data-bubble-name="activity"
>
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 78e4a907..f762ffa7 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,53 +1,108 @@
-import { mount } from '@vue/test-utils';
-import { ref } from 'vue';
-import { describe, expect, it, vi } from 'vitest';
+import { config, mount } from '@vue/test-utils';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import Activity from '../Activity.vue';
-vi.mock('vue-i18n', () => ({
- useI18n: () => ({ locale: ref('zh_CN') }),
+const mocks = vi.hoisted(() => ({
+ content: '',
}));
vi.mock('shared/helpers/timeHelper', () => ({
- messageTimestamp: () => '8月6日 下午 3:12',
+ messageTimestamp: () => '8月6日 下午 3:12',
}));
const longURL =
- 'https://ada.baidu.com/site/wjzt6ah8/agent?imid=e159aa97cd2c403f2fba76899e47615f&trinfo=XzFWIaclcWbzcMY&word=%E4%B8%89%E9%98%B3%E7%97%85%E6%AF%92%E6%90%BA%E5%B8%A6';
+ 'https://ada.baidu.com/site/wjzt6ah8/agent?imid=e159aa97cd2c403f2fba76899e47615f&trinfo=XzFWIaclcWbzcMY&word=%E4%B8%89%E9%98%B3%E7%97%85%E6%AF%92%E6%90%BA%E5%B8%A6';
-vi.mock('../../provider.js', () => ({
- useMessageContext: () => ({
- content: ref(`来源页面:${longURL}`),
- createdAt: ref(1786018000),
- }),
-}));
+const i18n = config.global.plugins.find(plugin => plugin.global?.locale);
+const initialLocale = i18n.global.locale.value;
+
+beforeAll(() => {
+ i18n.global.locale.value = 'zh_CN';
+});
+
+afterAll(() => {
+ i18n.global.locale.value = initialLocale;
+});
+
+vi.mock('../../provider.js', async () => {
+ const { ref } = await vi.importActual('vue');
+ return {
+ useMessageContext: () => ({
+ content: ref(mocks.content),
+ createdAt: ref(1786018000),
+ }),
+ };
+});
+
+const mountActivity = () =>
+ mount(Activity, {
+ global: {
+ directives: {
+ dompurifyHtml: (element, binding) => {
+ element.innerHTML = binding.value;
+ },
+ },
+ stubs: {
+ BaseBubble: {
+ template: '
',
+ },
+ },
+ },
+ });
describe('Activity bubble', () => {
- it('allows long links in system messages to wrap inside the bubble', () => {
- const wrapper = mount(Activity, {
- global: {
- directives: {
- dompurifyHtml: (element, binding) => {
- element.innerHTML = binding.value;
- },
- tooltip: () => {},
- },
- stubs: {
- BaseBubble: {
- template: '
',
- },
- },
- },
- });
+ it('allows long links in system messages to wrap inside the bubble', () => {
+ mocks.content = `来源页面:${longURL}`;
+ const wrapper = mountActivity();
- expect(
- wrapper.get('[data-bubble-name="activity"]').classes()
- ).toContain('max-w-full');
+ expect(wrapper.get('[data-bubble-name="activity"]').classes()).toContain(
+ 'max-w-full'
+ );
- const content = wrapper.get('span');
- expect(content.classes()).toContain('min-w-0');
- expect(content.classes()).toContain('max-w-full');
- expect(content.classes()).toContain('[overflow-wrap:anywhere]');
- expect(content.get('a').text()).toBe(longURL);
- });
+ const content = wrapper.get('span');
+ expect(content.classes()).toContain('min-w-0');
+ expect(content.classes()).toContain('max-w-full');
+ expect(content.classes()).toContain('[overflow-wrap:anywhere]');
+ expect(content.get('a').text()).toBe(longURL);
+ });
+
+ it.each([
+ ['Conversation was resolved by Anonymous Visitor', '会话已由匿名访客解决'],
+ ['Conversation was resolved by Alice', '会话已由Alice解决'],
+ [
+ 'Conversation was reopened due to agent bot webhook failure',
+ '由于客服机器人 Webhook 失败,会话已重新打开',
+ ],
+ [
+ 'CSAT survey not sent due to outgoing message restrictions',
+ '由于发送消息受限,未发送 CSAT 调查',
+ ],
+ [
+ 'Linear issue ENG-1 was created by Alice',
+ 'Linear 问题 ENG-1 已由 Alice 创建',
+ ],
+ [
+ 'Linear issue ENG-1 was linked by Alice',
+ 'Linear 问题 ENG-1 已由 Alice 关联',
+ ],
+ [
+ 'Linear issue ENG-1 was unlinked by Alice',
+ 'Linear 问题 ENG-1 已由 Alice 取消关联',
+ ],
+ ])('localizes known backend activity content: %s', (content, expected) => {
+ mocks.content = content;
+ const wrapper = mountActivity();
+
+ expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe(
+ expected
+ );
+ });
+
+ it('renders an empty activity when content is null', () => {
+ mocks.content = null;
+ const wrapper = mountActivity();
+
+ expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe('');
+ });
});
diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json
index 00a82936..fdfbcad3 100644
--- a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -2,6 +2,15 @@
"CONVERSATION": {
"SELECT_A_CONVERSATION": "Please select a conversation from left pane",
"CSAT_REPLY_MESSAGE": "Please rate the conversation",
+ "ACTIVITY": {
+ "ANONYMOUS_VISITOR": "Anonymous visitor",
+ "RESOLVED_BY": "Conversation was resolved by {name}",
+ "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}"
+ },
"404": "Sorry, we cannot find the conversation. Please try again",
"SWITCH_VIEW_LAYOUT": "Switch the layout",
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
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 67cd8d14..181bdcdd 100644
--- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json
+++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json
@@ -2,6 +2,15 @@
"CONVERSATION": {
"SELECT_A_CONVERSATION": "请从左侧窗格选择一个对话",
"CSAT_REPLY_MESSAGE": "请为会话评分",
+ "ACTIVITY": {
+ "ANONYMOUS_VISITOR": "匿名访客",
+ "RESOLVED_BY": "会话已由{name}解决",
+ "BOT_WEBHOOK_FAILURE": "由于客服机器人 Webhook 失败,会话已重新打开",
+ "CSAT_RESTRICTED": "由于发送消息受限,未发送 CSAT 调查",
+ "LINEAR_CREATED": "Linear 问题 {issueId} 已由 {name} 创建",
+ "LINEAR_LINKED": "Linear 问题 {issueId} 已由 {name} 关联",
+ "LINEAR_UNLINKED": "Linear 问题 {issueId} 已由 {name} 取消关联"
+ },
"404": "抱歉,我们找不到对话。请重试",
"SWITCH_VIEW_LAYOUT": "切换布局",
"DASHBOARD_APP_TAB_MESSAGES": "消息",