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