From 51fd4d55381ae2061c6aa24b269d081d13465735 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 27 Oct 2026 07:40:22 +0100 Subject: [PATCH] Give each user message its own copy, reply and time Two prompts sent close together (as while a bot is still answering) shared one row under their group: every copy button bunched beside the last message's time. Each user message now has its own row, with its copy button, a new Reply button (once it is saved) and its own time. copyableMessages is gone with the shared row. Co-Authored-By: Claude Opus 5.5 --- .../[bot_id]/(partials)/user-message-copy.tsx | 4 +-- .../(partials)/user-message-group.tsx | 18 ++--------- .../(partials)/user-message-reply.tsx | 30 +++++++++++++++++++ .../bots/[bot_id]/(partials)/user-message.tsx | 15 ++++++++-- .../(utils)/copyable-messages.test.ts | 27 ----------------- .../[bot_id]/(utils)/copyable-messages.ts | 14 --------- 6 files changed, 47 insertions(+), 61 deletions(-) create mode 100644 src/pages/admin/bots/[bot_id]/(partials)/user-message-reply.tsx delete mode 100644 src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.test.ts delete mode 100644 src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.ts diff --git a/src/pages/admin/bots/[bot_id]/(partials)/user-message-copy.tsx b/src/pages/admin/bots/[bot_id]/(partials)/user-message-copy.tsx index d747880..7ddaf70 100644 --- a/src/pages/admin/bots/[bot_id]/(partials)/user-message-copy.tsx +++ b/src/pages/admin/bots/[bot_id]/(partials)/user-message-copy.tsx @@ -5,8 +5,8 @@ type Props = { }; /** - * One message's copy button, in the row under a user group beside its - * timestamp. A message with no text renders nothing at all. + * One message's copy button, in the row under it beside its timestamp. A + * message with no text renders nothing at all. */ export default function UserMessageCopy({ text }: Props) { if (!text.trim()) return null; diff --git a/src/pages/admin/bots/[bot_id]/(partials)/user-message-group.tsx b/src/pages/admin/bots/[bot_id]/(partials)/user-message-group.tsx index 71e4089..87a5cfe 100644 --- a/src/pages/admin/bots/[bot_id]/(partials)/user-message-group.tsx +++ b/src/pages/admin/bots/[bot_id]/(partials)/user-message-group.tsx @@ -1,11 +1,7 @@ -import Row from "@moduletrace/twui/layout/Row"; import Stack from "@moduletrace/twui/layout/Stack"; import type { BotWithLastMessage } from "@/src/types"; -import copyableMessages from "../(utils)/copyable-messages"; import type { MessageGroup } from "../(utils)/group-messages"; import UserMessage from "./user-message"; -import UserMessageCopy from "./user-message-copy"; -import GroupTimestamp from "./group-timestamp"; type Props = { bot: BotWithLastMessage; @@ -13,24 +9,16 @@ type Props = { }; /** - * Right-aligned ink bubbles (each message's files above it), then one - * row under the group: a copy button per message with text, left of the - * group's time. + * Right-aligned ink bubbles (each message's files above it), each with + * its own copy button and time under it, so two messages sent close + * together never share one. */ export default function UserMessageGroup({ bot, group }: Props) { - const copyable = copyableMessages({ messages: group.messages }); - return ( {group.messages.map((message) => ( ))} - - {copyable.map((message) => ( - - ))} - - ); } diff --git a/src/pages/admin/bots/[bot_id]/(partials)/user-message-reply.tsx b/src/pages/admin/bots/[bot_id]/(partials)/user-message-reply.tsx new file mode 100644 index 0000000..033f8a8 --- /dev/null +++ b/src/pages/admin/bots/[bot_id]/(partials)/user-message-reply.tsx @@ -0,0 +1,30 @@ +import { useContext } from "react"; +import { Reply } from "lucide-react"; +import type { BotMessageWithAttachments } from "@/src/types"; +import { BotMessageActionsContext } from "../(data)/message-actions-context"; +import MessageActionButton from "./message-action-button"; + +type Props = { + message: BotMessageWithAttachments; +}; + +/** + * Reply to one of the user's own messages, beside its copy button. A + * message still on its way has only a temporary (negative) id, so it gets + * no button until the saved one replaces it. + */ +export default function UserMessageReply({ message }: Props) { + const { onReply } = useContext(BotMessageActionsContext); + + if (!(Number(message.id) > 0)) return null; + + return ( + onReply({ message })} + > + + + ); +} diff --git a/src/pages/admin/bots/[bot_id]/(partials)/user-message.tsx b/src/pages/admin/bots/[bot_id]/(partials)/user-message.tsx index b7fd91c..ec1d969 100644 --- a/src/pages/admin/bots/[bot_id]/(partials)/user-message.tsx +++ b/src/pages/admin/bots/[bot_id]/(partials)/user-message.tsx @@ -1,10 +1,14 @@ import { memo } from "react"; +import Row from "@moduletrace/twui/layout/Row"; import Stack from "@moduletrace/twui/layout/Stack"; import type { BotMessageWithAttachments } from "@/src/types"; import MessageAttachments from "./message-attachments"; import MessageReactions from "./message-reactions"; import MessageReplyQuote from "./message-reply-quote"; import UserBubble from "./user-bubble"; +import UserMessageCopy from "./user-message-copy"; +import UserMessageReply from "./user-message-reply"; +import GroupTimestamp from "./group-timestamp"; type Props = { message: BotMessageWithAttachments; @@ -14,9 +18,9 @@ type Props = { /** * One user message: the quoted message it answers, its files, then its * text (a file-only message has no bubble) and the bot's reactions to it. - * The user doesn't reply to or react to their own messages, but they can - * copy one: the group's copy buttons sit beside its timestamp, in - * `UserMessageGroup`. Memoized, like `BotMessageLine`. + * The user doesn't react to their own messages, but they can copy one or + * reply to it: those buttons sit beside its own time. Memoized, like + * `BotMessageLine`. */ function UserMessage({ message, bot_name }: Props) { return ( @@ -36,6 +40,11 @@ function UserMessage({ message, bot_name }: Props) { reactions={message.reactions} /> ) : null} + + + + + ); } diff --git a/src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.test.ts b/src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.test.ts deleted file mode 100644 index b9f7f3a..0000000 --- a/src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { BotMessageWithAttachments } from "@/src/types"; -import copyableMessages from "./copyable-messages"; - -function message(text?: string): BotMessageWithAttachments { - return { id: 1, author: "user", text } as BotMessageWithAttachments; -} - -describe("copyableMessages", () => { - test("keeps the messages that have text", () => { - const kept = copyableMessages({ - messages: [message("hello"), message(undefined), message("hi")], - }); - - expect(kept.map((one) => one.text)).toEqual(["hello", "hi"]); - }); - - test("drops text that is only whitespace", () => { - expect(copyableMessages({ messages: [message(" ")] })).toEqual([]); - }); - - test("a group with no text has nothing to copy", () => { - expect(copyableMessages({ messages: [message(), message()] })).toEqual( - [], - ); - }); -}); diff --git a/src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.ts b/src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.ts deleted file mode 100644 index d1f986b..0000000 --- a/src/pages/admin/bots/[bot_id]/(utils)/copyable-messages.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { BotMessageWithAttachments } from "@/src/types"; - -/** - * The messages of a group that can be copied: a file-only message has no - * text, and its copy button renders nothing, so it would only leave a - * gap in the row. - */ -export default function copyableMessages({ - messages, -}: { - messages: BotMessageWithAttachments[]; -}): BotMessageWithAttachments[] { - return messages.filter((message) => Boolean(message.text?.trim())); -}