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 <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e <e18f3511303812c437d487ac4bf46ad897dc46946699b18ab2e60bf8ee0ea851@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
npub1ux8n2yfs8qfvgd75s7kyhar2mztac355v6vmrz4juc9l3msw4pgstums9e
2026-07-13 18:45:03 -07:00
parent 120bb7d2a2
commit e1dcc064c7
4 changed files with 32 additions and 11 deletions

View File

@@ -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({
<>
<ContextMenuSeparator />
<ContextMenuItem
disabled={disableManagementActions}
onSelect={() =>
deferMenuAction(() =>
openChannelManagement(channel.id, { edit: true }),
@@ -283,6 +285,7 @@ export function ChannelContextMenuItems({
</ContextMenuItem>
<ContextMenuItem
className="text-destructive focus:text-destructive"
disabled={disableManagementActions}
onSelect={() =>
deferMenuAction(() => {
void archiveChannelMutation.mutateAsync().catch((error) => {

View File

@@ -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<RawChannelMembersResponse> {
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);

View File

@@ -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);

View File

@@ -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[];