From e1dcc064c7df7e06140e89c149742c735886077b Mon Sep 17 00:00:00 2001 From: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e Date: Mon, 13 Jul 2026 18:45:03 -0700 Subject: [PATCH] Address review feedback round 5 Keep cold-cache channel management actions visible but disabled until permissions resolve, and cover the loading state plus negative menu assertions. Co-authored-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e Signed-off-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e --- .../sidebar/ui/ChannelContextMenu.tsx | 25 +++++++++++-------- desktop/src/testing/e2eBridge.ts | 6 +++++ desktop/tests/e2e/channels.spec.ts | 11 ++++++++ desktop/tests/helpers/bridge.ts | 1 + 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/desktop/src/features/sidebar/ui/ChannelContextMenu.tsx b/desktop/src/features/sidebar/ui/ChannelContextMenu.tsx index 12eecccd93..8467f09c97 100644 --- a/desktop/src/features/sidebar/ui/ChannelContextMenu.tsx +++ b/desktop/src/features/sidebar/ui/ChannelContextMenu.tsx @@ -1,5 +1,3 @@ -import * as React from "react"; - import { Archive, Bell, @@ -231,15 +229,18 @@ export function ChannelContextMenuItems({ selfMember?.role === "owner" || selfMember?.role === "admin" || canManageOwnedAgentChannel; - // Context-menu content mounts once per open session, so this capability is a - // stable snapshot. Late query/refetch results cannot insert an immediate - // Archive action or move another item beneath the user's pointer. - const [showManagementActions] = React.useState( - () => - channel.channelType !== "dm" && - channel.archivedAt === null && - canManageChannel, - ); + const hasResolvedMembers = membersQuery.data !== undefined; + const hasResolvedOwnerProfiles = + ownerPubkeys.length === 0 || ownerProfilesQuery.data !== undefined; + const isManagementCandidate = + channel.channelType !== "dm" && channel.archivedAt === null; + // Reserve disabled rows while capability queries resolve. This keeps the + // menu stable under the pointer without making owners close and reopen a + // cold-cache menu before the actions appear. + const showManagementActions = + isManagementCandidate && + (!hasResolvedMembers || !hasResolvedOwnerProfiles || canManageChannel); + const disableManagementActions = !canManageChannel; const showStar = Boolean(onStarChannel && onUnstarChannel); const showReadToggle = hasUnread ? Boolean(onMarkChannelRead) @@ -270,6 +271,7 @@ export function ChannelContextMenuItems({ <> deferMenuAction(() => openChannelManagement(channel.id, { edit: true }), @@ -283,6 +285,7 @@ export function ChannelContextMenuItems({ deferMenuAction(() => { void archiveChannelMutation.mutateAsync().catch((error) => { diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 41054deab2..c36db54563 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -143,6 +143,7 @@ type E2eConfig = { * tests can observe the in-flight prepend window. 0/undefined = instant. */ channelWindowDelayMs?: number; profileReadDelayMs?: number; + channelMembersDelayMs?: number; profileReadError?: string; profileUpdateError?: string; profileUpdateErrors?: string[]; @@ -5503,6 +5504,11 @@ async function handleGetChannelMembers( args: { channelId: string }, config: E2eConfig | undefined, ): Promise { + const delayMs = config?.mock?.channelMembersDelayMs ?? 0; + if (delayMs > 0) { + await new Promise((resolve) => window.setTimeout(resolve, delayMs)); + } + const identity = getIdentity(config); if (!identity) { const channel = getMockChannel(args.channelId); diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts index 968956eae3..a07378ce1c 100644 --- a/desktop/tests/e2e/channels.spec.ts +++ b/desktop/tests/e2e/channels.spec.ts @@ -2335,8 +2335,17 @@ test("open channel management supports join and leave", async ({ page }) => { }); test("channel context menu edits a stream", async ({ page }) => { + await installMockBridge(page, { channelMembersDelayMs: 500 }); await page.goto("/"); + await page.getByTestId("channel-agents").click({ button: "right" }); + const pendingEditItem = page.getByRole("menuitem", { name: "Edit channel" }); + await expect(pendingEditItem).toBeVisible(); + await expect(pendingEditItem).toBeDisabled(); + await expect(pendingEditItem).toBeEnabled(); + await page.keyboard.press("Escape"); + await expect(pendingEditItem).toHaveCount(0); + await page.getByTestId("channel-general").click({ button: "right" }); const editItem = page.getByRole("menuitem", { name: "Edit channel" }); await expect(editItem).toBeVisible(); @@ -2384,6 +2393,7 @@ test("channel context menu hides management actions from members and DMs", async await page.getByTestId("auxiliary-panel-close").click(); await page.getByTestId("channel-random").click({ button: "right" }); + await expect(page.getByRole("menuitem", { name: "Copy" })).toBeVisible(); await expect( page.getByRole("menuitem", { name: "Edit channel" }), ).toHaveCount(0); @@ -2397,6 +2407,7 @@ test("channel context menu hides management actions from members and DMs", async .locator('[data-testid^="channel-"]') .first(); await firstDm.click({ button: "right" }); + await expect(page.getByRole("menuitem", { name: "Copy" })).toBeVisible(); await expect( page.getByRole("menuitem", { name: "Edit channel" }), ).toHaveCount(0); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 27e3fef136..486c6dbb7d 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -168,6 +168,7 @@ type MockBridgeOptions = { /** Delay (ms) for older-history fetches; see e2eBridge mock config. */ channelWindowDelayMs?: number; profileReadDelayMs?: number; + channelMembersDelayMs?: number; profileReadError?: string; profileUpdateError?: string; profileUpdateErrors?: string[];