mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
fix(messages): route edits to the owning composer
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -220,11 +220,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
isActiveWelcomeChannel,
|
||||
currentPubkey ?? null,
|
||||
);
|
||||
const isEditInThread =
|
||||
editTarget != null &&
|
||||
threadHeadMessage != null &&
|
||||
(editTarget.id === threadHeadMessage.id ||
|
||||
threadMessages.some((entry) => entry.message.id === editTarget.id));
|
||||
const isEditInThread = editTarget?.isThreadReply ?? false;
|
||||
const mainEditTarget = editTarget && !isEditInThread ? editTarget : null;
|
||||
const threadEditTarget = editTarget && isEditInThread ? editTarget : null;
|
||||
const findLastOwnEditable = React.useCallback(
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type * as React from "react";
|
||||
import type { BotActivityAgent } from "@/features/channels/ui/BotActivityBar";
|
||||
import type { ChannelAgentSessionAgent } from "@/features/channels/ui/useChannelAgentSessions";
|
||||
import type { ImetaMedia } from "@/features/messages/lib/imetaMediaMarkdown";
|
||||
import type { MessageComposerEditTarget } from "@/features/messages/ui/MessageComposer.types";
|
||||
import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel";
|
||||
import type { ChannelWindowThreadSummary } from "@/features/messages/lib/channelWindowStore";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { TypingIndicatorEntry } from "@/features/messages/useChannelTyping";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import type { DraftMentionRef } from "@/features/messages/lib/useDrafts";
|
||||
import type {
|
||||
ProfilePanelTab,
|
||||
ProfilePanelView,
|
||||
@@ -37,13 +36,7 @@ export type ChannelPaneProps = {
|
||||
botTypingEntries: TypingIndicatorEntry[];
|
||||
channelManagementOpen?: boolean;
|
||||
currentPubkey?: string;
|
||||
editTarget?: {
|
||||
author: string;
|
||||
body: string;
|
||||
id: string;
|
||||
imetaMedia?: ImetaMedia[];
|
||||
mentionRefs?: DraftMentionRef[];
|
||||
} | null;
|
||||
editTarget?: MessageComposerEditTarget | null;
|
||||
fetchOlder?: () => Promise<void>;
|
||||
header?: React.ReactNode;
|
||||
hasOlderMessages?: boolean;
|
||||
|
||||
@@ -69,6 +69,26 @@ test("edit target preserves tagged identities while profiles are unavailable", (
|
||||
assert.deepEqual(target.unresolvedMentionPubkeys, [ALICE, BOB]);
|
||||
});
|
||||
|
||||
test("edit target records semantic thread ownership", () => {
|
||||
const root = buildMessageComposerEditTarget(
|
||||
message("Root", [["h", "channel-id"]]),
|
||||
undefined,
|
||||
() => false,
|
||||
);
|
||||
const reply = buildMessageComposerEditTarget(
|
||||
message("Reply", [
|
||||
["h", "channel-id"],
|
||||
["e", "root-id", "", "root"],
|
||||
["e", "root-id", "", "reply"],
|
||||
]),
|
||||
undefined,
|
||||
() => false,
|
||||
);
|
||||
|
||||
assert.equal(root.isThreadReply, false);
|
||||
assert.equal(reply.isThreadReply, true);
|
||||
});
|
||||
|
||||
test("edit target separates resolved refs from identities missing profiles", () => {
|
||||
const target = buildMessageComposerEditTarget(
|
||||
message("Please review this, @Alice and @Bob.", [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { hasMention } from "@/features/messages/lib/hasMention";
|
||||
import { imetaMediaFromTags } from "@/features/messages/lib/imetaMediaMarkdown";
|
||||
import { isThreadReply } from "@/features/messages/lib/threading";
|
||||
import type { DraftMentionRef } from "@/features/messages/lib/useDrafts";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { MessageComposerEditTarget } from "@/features/messages/ui/MessageComposer.types";
|
||||
@@ -95,6 +96,7 @@ export function buildMessageComposerEditTarget(
|
||||
author: message.author,
|
||||
body: message.body,
|
||||
id: message.id,
|
||||
isThreadReply: isThreadReply(message.tags ?? []),
|
||||
imetaMedia: imetaMediaFromTags(message.tags),
|
||||
...mentionState,
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ function MoreActionsMenu({
|
||||
{onEdit ? (
|
||||
<DropdownMenuItem
|
||||
data-testid={`edit-message-${message.id}`}
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
editJustSelectedRef.current = true;
|
||||
onEdit(message);
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type MessageComposerEditTarget = {
|
||||
author: string;
|
||||
body: string;
|
||||
id: string;
|
||||
isThreadReply?: boolean;
|
||||
/**
|
||||
* NIP-92 imeta attachments on the original event, in tag order. Loaded
|
||||
* into the composer's pending-imeta state on edit-open so the user sees
|
||||
|
||||
@@ -2970,6 +2970,82 @@ test("thread composer keeps focus after sending a thread reply", async ({
|
||||
await expect(threadInput).toBeFocused();
|
||||
});
|
||||
|
||||
test("editing the thread root uses and focuses the main composer", async ({
|
||||
page,
|
||||
}) => {
|
||||
const root = `Root edit routing ${Date.now()}`;
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const mainInput = page
|
||||
.getByTestId("channel-composer-overlay")
|
||||
.getByTestId("message-input");
|
||||
await mainInput.fill(root);
|
||||
await mainInput.press("Enter");
|
||||
|
||||
const timeline = page.getByTestId("message-timeline");
|
||||
const timelineRoot = timeline.getByTestId("message-row").last();
|
||||
await expect(timelineRoot).toContainText(root);
|
||||
await timelineRoot.hover();
|
||||
await timelineRoot.getByRole("button", { name: "Reply" }).click();
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
const threadRoot = threadPanel.getByTestId("message-row").first();
|
||||
await threadRoot.hover();
|
||||
await threadRoot.getByRole("button", { name: "More actions" }).click();
|
||||
await page.getByRole("menuitem", { name: "Edit message" }).click();
|
||||
|
||||
await expect(page.getByTestId("edit-target")).toHaveCount(1);
|
||||
await expect(threadPanel.getByTestId("edit-target")).toHaveCount(0);
|
||||
await expect(mainInput).toHaveText(root);
|
||||
await expect(mainInput).toBeFocused();
|
||||
});
|
||||
|
||||
test("editing a thread reply uses and focuses the thread composer", async ({
|
||||
page,
|
||||
}) => {
|
||||
const root = `Reply edit routing root ${Date.now()}`;
|
||||
const reply = `Reply edit routing ${Date.now()}`;
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const mainInput = page
|
||||
.getByTestId("channel-composer-overlay")
|
||||
.getByTestId("message-input");
|
||||
await mainInput.fill(root);
|
||||
await mainInput.press("Enter");
|
||||
|
||||
const timelineRoot = page
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.last();
|
||||
await timelineRoot.scrollIntoViewIfNeeded();
|
||||
await timelineRoot.hover();
|
||||
await timelineRoot.getByRole("button", { name: "Reply" }).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
const threadInput = threadPanel.getByTestId("message-input");
|
||||
await threadInput.fill(reply);
|
||||
await threadInput.press("Enter");
|
||||
await expect(threadPanel).toContainText(reply);
|
||||
|
||||
const threadReply = threadPanel.getByTestId("message-row").last();
|
||||
await threadReply.hover();
|
||||
await threadReply.getByRole("button", { name: "More actions" }).click();
|
||||
await page.getByRole("menuitem", { name: "Edit message" }).click();
|
||||
|
||||
await expect(threadPanel.getByTestId("edit-target")).toBeVisible();
|
||||
await expect(threadInput).toHaveText(reply);
|
||||
await expect(threadInput).toBeFocused();
|
||||
});
|
||||
|
||||
test("ArrowUp in an empty composer edits your last message right after sending", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user