fix: hydrate realtime conversations before rendering
Build and publish Docker images / Build and publish images (push) Successful in 3m16s

This commit is contained in:
2026-09-14 21:39:56 +08:00
parent 9b8d575657
commit 5a0e9ecada
4 changed files with 149 additions and 10 deletions
@@ -24,6 +24,8 @@ class ActionCableConnector extends BaseActionCableConnector {
'user:logout': this.onLogout,
'page:reload': this.onReload,
'assignee.changed': this.onAssigneeChanged,
'conversation.assigned': this.onConversationAssigned,
'conversation.unassigned': this.onConversationAssigned,
'conversation.typing_on': this.onTypingOn,
'conversation.typing_off': this.onTypingOff,
'conversation.contact_changed': this.onConversationContactChange,
@@ -88,8 +90,18 @@ class ActionCableConnector extends BaseActionCableConnector {
this.fetchConversationStats();
};
onConversationCreated = data => {
this.app.$store.dispatch('addConversation', data);
onConversationAssigned = data => {
if (data?.id) {
this.app.$store.dispatch('getConversation', data.id);
}
this.fetchConversationStats();
};
onConversationCreated = async data => {
const added = await this.app.$store.dispatch('addConversation', data);
if (added && !data.messages?.length) {
await this.app.$store.dispatch('getConversation', data.id);
}
this.fetchConversationStats();
};
@@ -105,8 +117,24 @@ class ActionCableConnector extends BaseActionCableConnector {
conversation: { last_activity_at: lastActivityAt },
conversation_id: conversationId,
} = data;
const hasConversation =
this.app.$store.state?.conversations?.allConversations?.some(
conversation => Number(conversation.id) === Number(conversationId)
);
const applied = await this.app.$store.dispatch('addMessage', data);
if (!applied) return;
if (!applied) {
if (!hasConversation) {
const conversation = await this.app.$store.dispatch(
'getConversation',
conversationId
);
if (conversation) {
DashboardAudioNotificationHelper.onNewMessage(data);
this.fetchConversationStats();
}
}
return;
}
DashboardAudioNotificationHelper.onNewMessage(data);
this.app.$store.dispatch('updateConversationLastActivity', {
@@ -102,6 +102,62 @@ describe('ActionCableConnector - Copilot Tests', () => {
});
});
it('handles native assignment events and refreshes authoritative conversation data', async () => {
expect(actionCable.events['conversation.assigned']).toBe(
actionCable.onConversationAssigned
);
expect(actionCable.events['conversation.unassigned']).toBe(
actionCable.onConversationAssigned
);
actionCable.onReceived({
event: 'conversation.assigned',
data: { account_id: 1, id: 42 },
});
expect(mockDispatch).toHaveBeenCalledWith('getConversation', 42);
});
it('hydrates a message whose realtime event arrives before conversation creation', async () => {
const message = {
id: 42,
account_id: 1,
conversation_id: 7,
conversation: { last_activity_at: 123 },
};
mockDispatch.mockImplementation(action => {
if (action === 'addMessage') return Promise.resolve(false);
if (action === 'getConversation') {
return Promise.resolve({ id: 7, messages: [message] });
}
return undefined;
});
await actionCable.onMessageCreated(message);
expect(mockDispatch).toHaveBeenCalledWith('getConversation', 7);
expect(DashboardAudioNotificationHelper.onNewMessage).toHaveBeenCalledWith(
message
);
});
it('hydrates a conversation-created event that has no embedded messages', async () => {
mockDispatch.mockImplementation(action => {
if (action === 'addConversation') return Promise.resolve(true);
if (action === 'getConversation') return Promise.resolve({ id: 7 });
return undefined;
});
await actionCable.onConversationCreated({
id: 7,
inbox_id: 1,
messages: [],
meta: { sender: {} },
});
expect(mockDispatch).toHaveBeenCalledWith('getConversation', 7);
});
it('plays new message audio only once when realtime replays a message', async () => {
const message = {
id: 42,