From 1a6840cdc67db3feb9238bedca775b2cbfd5bab4 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Wed, 25 Feb 2026 07:14:56 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20optimistic=20message=20send=20=E2=80=94?= =?UTF-8?q?=20no=20full=20timeline=20reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of calling openCommunicationThread() after sending (which triggered a full timeline refetch, destroyed audio waveforms, and caused the chat to jump), we now: - Optimistically append the sent message to clientTimelineItems - Scroll to bottom smoothly - Refresh contacts sidebar for lastMessageText preview - Auto-scroll only fires on thread switch (empty→loaded), not on every timeline update, preserving audio waveform DOM elements Co-Authored-By: Claude Opus 4.6 --- .../components/workspace/CrmWorkspaceApp.vue | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index 8372271..35ecd4f 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -844,17 +844,17 @@ async function sendCommMessage() { const text = commDraft.value.trim(); if (!text || commSending.value || !selectedCommThread.value) return; commSending.value = true; + const contactId = selectedCommThreadId.value; + const contactName = selectedCommThread.value.contact; try { const channel = commSendChannel.value; if (!channel) return; const { useMutation } = await import("@vue/apollo-composable"); - const { CreateCommunicationMutationDocument, CommunicationsQueryDocument, ContactInboxesQueryDocument } = await import("~~/graphql/generated"); - const { mutate: doCreateCommunication } = useMutation(CreateCommunicationMutationDocument, { - refetchQueries: [{ query: CommunicationsQueryDocument }, { query: ContactInboxesQueryDocument }], - }); - await doCreateCommunication({ + const { CreateCommunicationMutationDocument } = await import("~~/graphql/generated"); + const { mutate: doCreateCommunication } = useMutation(CreateCommunicationMutationDocument); + const result = await doCreateCommunication({ input: { - contact: selectedCommThread.value.contact, + contact: contactName, channel, kind: "message", direction: "out", @@ -862,7 +862,36 @@ async function sendCommMessage() { }, }); commDraft.value = ""; - openCommunicationThread(selectedCommThread.value.contact); + + // Optimistically append the sent message to timeline (no full reload) + const newId = result?.data?.createCommunication?.id ?? `temp-${Date.now()}`; + const now = new Date().toISOString(); + clientTimelineItems.value = [ + ...clientTimelineItems.value, + { + id: newId, + contactId, + contentType: "message", + contentId: newId, + datetime: now, + message: { + id: newId, + at: now, + contact: contactName, + contactInboxId: "", + sourceExternalId: "", + sourceTitle: "", + channel: channel as CommItem["channel"], + kind: "message", + direction: "out", + text, + }, + }, + ]; + scrollCommThreadToBottom(); + + // Refresh sidebar preview (lastMessageText) — lightweight + void refetchContacts(); } finally { commSending.value = false; } @@ -1145,9 +1174,9 @@ function scrollCommThreadToBottom() { }); } -// Scroll to bottom whenever timeline items change (thread switch or new message) -watch(clientTimelineItems, (items) => { - if (items.length) scrollCommThreadToBottom(); +// Scroll to bottom when a new thread loads (items go from [] → [items]) +watch(clientTimelineItems, (items, oldItems) => { + if (items.length && (!oldItems || oldItems.length === 0)) scrollCommThreadToBottom(); }); // ---------------------------------------------------------------------------