Show sent messages before the server confirms them

The composer now puts the bubble and clears the input as soon as you send, so a slow connection never holds either. The bot's status row reads "<name> is getting your message..." until the saved message arrives (by response or push), then switches to thinking. A failed send removes the bubble and restores the text and files.
This commit is contained in:
2026-10-20 14:50:20 +01:00
parent 4f7e0a6bf0
commit 8425631b3a
9 changed files with 323 additions and 42 deletions
@@ -0,0 +1,55 @@
import { describe, expect, test } from "bun:test";
import type { BotMessageWithAttachments } from "@/src/types";
import dropSentMessage from "./drop-sent-message";
const pendingMessage = (overrides: Partial<BotMessageWithAttachments> = {}): BotMessageWithAttachments => ({
id: -1,
author: "user",
text: "Hello",
...overrides,
});
const savedMessage = (overrides: Partial<BotMessageWithAttachments> = {}): BotMessageWithAttachments => ({
id: 7,
author: "user",
text: "Hello",
...overrides,
});
describe("dropSentMessage", () => {
test("drops the pending copy the saved message replaces", () => {
const other = pendingMessage({ id: -2, text: "Later" });
expect(
dropSentMessage({ pending: [pendingMessage(), other], message: savedMessage() }),
).toEqual([other]);
});
test("leaves the thread alone when nothing matches", () => {
const pending = [pendingMessage({ text: "Something else" })];
expect(dropSentMessage({ pending, message: savedMessage() })).toBe(pending);
});
test("matches on the quote as well as the text", () => {
const pending = [pendingMessage({ reply_to: { id: 3, author: "bot", text: "Hi" } })];
expect(dropSentMessage({ pending, message: savedMessage({ reply_to_message_id: 3 }) })).toEqual([]);
expect(
dropSentMessage({ pending, message: savedMessage({ reply_to_message_id: 4 }) }),
).toBe(pending);
});
test("only drops one copy of the same words", () => {
const pending = [pendingMessage({ id: -1 }), pendingMessage({ id: -2 })];
expect(dropSentMessage({ pending, message: savedMessage() })).toEqual([pendingMessage({ id: -2 })]);
});
test("never drops a bot message", () => {
const pending = [pendingMessage()];
const message = savedMessage({ author: "bot" });
expect(dropSentMessage({ pending, message })).toBe(pending);
});
});
@@ -0,0 +1,34 @@
import type { BotMessageWithAttachments } from "@/src/types";
type Params = {
/**
* Messages shown before the server has them.
*/
pending: BotMessageWithAttachments[];
/**
* The saved message that has arrived, from the send's response or a
* push.
*/
message: BotMessageWithAttachments;
};
/**
* Takes a pending message off the thread once the saved one arrives, so a
* pushed copy never shows beside the bubble still waiting for its
* response. Only user messages can be pending. The first one that matches
* is enough: same text, same quote, same number of files.
*/
export default function dropSentMessage({ pending, message }: Params): BotMessageWithAttachments[] {
if (!pending.length || message.author != "user") return pending;
const index = pending.findIndex(
(item) =>
item.text == message.text &&
(item.reply_to?.id || null) == (message.reply_to_message_id || null) &&
(item.attachments?.length || 0) == (message.attachments?.length || 0),
);
if (index < 0) return pending;
return [...pending.slice(0, index), ...pending.slice(index + 1)];
}