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();
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();
});
// ---------------------------------------------------------------------------