fix: optimistic message send — no full timeline reload

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 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-25 07:14:56 +07:00
parent 898f0dc0c5
commit 1a6840cdc6

View File

@@ -844,17 +844,17 @@ async function sendCommMessage() {
const text = commDraft.value.trim(); const text = commDraft.value.trim();
if (!text || commSending.value || !selectedCommThread.value) return; if (!text || commSending.value || !selectedCommThread.value) return;
commSending.value = true; commSending.value = true;
const contactId = selectedCommThreadId.value;
const contactName = selectedCommThread.value.contact;
try { try {
const channel = commSendChannel.value; const channel = commSendChannel.value;
if (!channel) return; if (!channel) return;
const { useMutation } = await import("@vue/apollo-composable"); const { useMutation } = await import("@vue/apollo-composable");
const { CreateCommunicationMutationDocument, CommunicationsQueryDocument, ContactInboxesQueryDocument } = await import("~~/graphql/generated"); const { CreateCommunicationMutationDocument } = await import("~~/graphql/generated");
const { mutate: doCreateCommunication } = useMutation(CreateCommunicationMutationDocument, { const { mutate: doCreateCommunication } = useMutation(CreateCommunicationMutationDocument);
refetchQueries: [{ query: CommunicationsQueryDocument }, { query: ContactInboxesQueryDocument }], const result = await doCreateCommunication({
});
await doCreateCommunication({
input: { input: {
contact: selectedCommThread.value.contact, contact: contactName,
channel, channel,
kind: "message", kind: "message",
direction: "out", direction: "out",
@@ -862,7 +862,36 @@ async function sendCommMessage() {
}, },
}); });
commDraft.value = ""; 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 { } finally {
commSending.value = false; commSending.value = false;
} }
@@ -1145,9 +1174,9 @@ function scrollCommThreadToBottom() {
}); });
} }
// Scroll to bottom whenever timeline items change (thread switch or new message) // Scroll to bottom when a new thread loads (items go from [] → [items])
watch(clientTimelineItems, (items) => { watch(clientTimelineItems, (items, oldItems) => {
if (items.length) scrollCommThreadToBottom(); if (items.length && (!oldItems || oldItems.length === 0)) scrollCommThreadToBottom();
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------