Add replies and emoji reactions to chat messages

Every message has a row under it to reply or react. Replying quotes the
message above the composer, and the sent message stores
reply_to_message_id (a plain id, not a foreign key, so deleting the
quoted message only drops the quote). send-message accepts a quote from
any session of the same conversation, so a rollover or a thread that
moved on still sends; the conversation drops a picked quote when its
session changes. The bot reads the quote as "[Replying to ...]" before
the user's turn, file-only replies included, and relayed messages carry
it too.

Reactions are a fixed set (BotMessageReactions) in
bot_message_reactions, one per message, emoji and user or bot. Users
toggle their own through api/admin/bots/react-message, and every bot
gets react_to_message for the session it answers (the user's latest
message by default). Both push the fresh list as server:bot-reaction,
with mine worked out for the session's user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-19 21:56:31 +01:00
co-authored by Claude Opus 5.5
parent bc499a7466
commit d1b5b6d68a
44 changed files with 1289 additions and 58 deletions
+18 -2
View File
@@ -1,5 +1,6 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BUN_SQLITE_COBBLE_BOT_MESSAGES } from "@/db/types/db";
import grabMessagesReplies from "@/src/functions/backend/db/bots/grab-messages-replies";
import { sendToUser } from "@/src/websocket/(functions)/send-to-user";
type Params = {
@@ -9,7 +10,8 @@ type Params = {
/**
* Pushes `server:bot-message` for every message newer than `after_id` to
* the sockets on this process of the user whose conversation it's in. Returns the newest id read.
* the sockets on this process of the user whose conversation it's in,
* with the message a reply quotes. Returns the newest id read.
*/
export default async function relayNewBotMessages({ after_id, users }: Params) {
const rows =
@@ -21,9 +23,23 @@ export default async function relayNewBotMessages({ after_id, users }: Params) {
})
).payload || [];
const replies = await grabMessagesReplies({
message_ids: rows
.filter((row) => row.reply_to_message_id && users.has(String(row.user_id)))
.map((row) => Number(row.id)),
});
for (const { user_id, ...message } of rows) {
if (users.has(String(user_id))) {
sendToUser({ user_id, data: { event: "server:bot-message", data: message } });
const reply_to = replies.get(Number(message.id));
sendToUser({
user_id,
data: {
event: "server:bot-message",
data: reply_to ? { ...message, reply_to } : message,
},
});
}
}