Put a user message's copy button beside its timestamp
The copy button sat on its own row above the group's time, costing a line of vertical space for nothing. It now shares that row, left of the timestamp, the way a bot message's actions already do. A file-only message leaves no gap: only messages with text get a button, picked by copyableMessages.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import CopyMessageButton from "./copy-message-button";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One message's copy button, in the row under a user group beside its
|
||||
* timestamp. A message with no text renders nothing at all.
|
||||
*/
|
||||
export default function UserMessageCopy({ text }: Props) {
|
||||
if (!text.trim()) return null;
|
||||
|
||||
return <CopyMessageButton text={text} />;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
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 = {
|
||||
@@ -10,16 +13,24 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Right-aligned ink bubbles (each message's files above it), one
|
||||
* timestamp under the group.
|
||||
* 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.
|
||||
*/
|
||||
export default function UserMessageGroup({ bot, group }: Props) {
|
||||
const copyable = copyableMessages({ messages: group.messages });
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-1! items-end">
|
||||
{group.messages.map((message) => (
|
||||
<UserMessage key={message.id} message={message} bot_name={bot.name || "the bot"} />
|
||||
))}
|
||||
<GroupTimestamp at={group.at} />
|
||||
<Row className="gap-3! items-center">
|
||||
{copyable.map((message) => (
|
||||
<UserMessageCopy key={message.id} text={message.text || ""} />
|
||||
))}
|
||||
<GroupTimestamp at={group.at} />
|
||||
</Row>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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 CopyMessageButton from "./copy-message-button";
|
||||
import MessageAttachments from "./message-attachments";
|
||||
import MessageReactions from "./message-reactions";
|
||||
import MessageReplyQuote from "./message-reply-quote";
|
||||
@@ -17,7 +15,8 @@ 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. Memoized, like `BotMessageLine`.
|
||||
* copy one: the group's copy buttons sit beside its timestamp, in
|
||||
* `UserMessageGroup`. Memoized, like `BotMessageLine`.
|
||||
*/
|
||||
function UserMessage({ message, bot_name }: Props) {
|
||||
return (
|
||||
@@ -37,11 +36,6 @@ function UserMessage({ message, bot_name }: Props) {
|
||||
reactions={message.reactions}
|
||||
/>
|
||||
) : null}
|
||||
{message.text ? (
|
||||
<Row className="gap-3! items-center">
|
||||
<CopyMessageButton text={message.text} />
|
||||
</Row>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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(
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
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()));
|
||||
}
|
||||
Reference in New Issue
Block a user