Files
gochat/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue
T
Rogeeandrogee 35727be51a HH-560: localize upstream activity messages (#137)
* fix(HH-560): localize upstream activity messages

* fix(HH-560): preserve unknown activity content

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 22:11:00 +08:00

264 lines
8.8 KiB
Vue

<script setup>
import { computed } from 'vue';
import { zhCN } from 'date-fns/locale';
import { useI18n } from 'vue-i18n';
import { messageTimestamp } from 'shared/helpers/timeHelper';
import BaseBubble from './Base.vue';
import { useMessageContext } from '../provider.js';
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 structuredActivity = contentAttributes.value?.activity;
if (
structuredActivity &&
!Object.hasOwn(ACTIVITY_KEYS, structuredActivity.type)
) {
return normalizedContent;
}
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 (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;
});
const readableTime = computed(() => {
if (locale.value === 'zh_CN') {
return messageTimestamp(createdAt.value, 'M月d日 aaa h:mm', {
locale: zhCN,
fallbackFormat: 'yyyy年M月d日 aaa h:mm',
});
}
return messageTimestamp(createdAt.value, 'LLL d, h:mm a');
});
</script>
<template>
<BaseBubble
v-tooltip.top="readableTime"
class="px-3 py-1 !rounded-xl flex min-w-0 max-w-full items-center gap-2"
data-bubble-name="activity"
>
<span
v-dompurify-html="localizedContent"
:title="localizedContent"
class="min-w-0 max-w-full [overflow-wrap:anywhere]"
/>
</BaseBubble>
</template>