diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx
index e708dfb9b6..5b622a4565 100644
--- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx
+++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx
@@ -4,10 +4,14 @@ import {
Copy,
DoorClosed,
DoorOpen,
+ Eye,
FileText,
- Hash,
+ Fingerprint,
Lock,
MessageSquare,
+ Pencil,
+ Radio,
+ Type,
Users,
Zap,
} from "lucide-react";
@@ -31,10 +35,13 @@ import {
formatTtlDuration,
parseTtlDuration,
} from "@/features/channels/lib/ephemeralChannel";
+import {
+ SettingsOptionGroup,
+ SettingsOptionRow,
+} from "@/features/settings/ui/SettingsOptionGroup";
import { CreateWorkflowDialog } from "@/features/workflows/ui/CreateWorkflowDialog";
import type { Channel } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
-import { useTheme } from "@/shared/theme/ThemeProvider";
import {
AlertDialog,
AlertDialogAction,
@@ -47,18 +54,26 @@ import {
AlertDialogTrigger,
} from "@/shared/ui/alert-dialog";
import { Button } from "@/shared/ui/button";
-import { Input } from "@/shared/ui/input";
import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/shared/ui/sheet";
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/shared/ui/dialog";
+import { Input } from "@/shared/ui/input";
import { Switch } from "@/shared/ui/switch";
import { Textarea } from "@/shared/ui/textarea";
import { ChannelCanvas } from "./ChannelCanvas";
+import {
+ CanvasSummaryRow,
+ ChannelHero,
+ ChannelQuickAction,
+ CopyFieldRow,
+ FieldGroup,
+ InfoFieldRow,
+ NarrativeField,
+} from "./ChannelManagementSheetRows";
type ChannelManagementSheetProps = {
channel: Channel | null;
@@ -70,50 +85,6 @@ type ChannelManagementSheetProps = {
const DEFAULT_EPHEMERAL_TTL_SECONDS = 24 * 60 * 60;
-function MetadataPill({
- icon: Icon,
- label,
-}: {
- icon: React.ComponentType<{ className?: string }>;
- label: string;
-}) {
- return (
-
-
- {label}
-
- );
-}
-
-function ChannelIdRow({ channelId }: { channelId: string }) {
- async function handleCopyChannelId() {
- await navigator.clipboard.writeText(channelId);
- toast.success("Copied channel ID to clipboard");
- }
-
- return (
- {
- void handleCopyChannelId();
- }}
- title="Copy channel ID"
- type="button"
- >
-
-
- Channel ID
-
-
- {channelId}
-
-
-
-
- );
-}
-
export function ChannelManagementSheet({
channel,
currentPubkey,
@@ -121,7 +92,6 @@ export function ChannelManagementSheet({
onOpenChange,
open,
}: ChannelManagementSheetProps) {
- const { isDark } = useTheme();
const channelId = channel?.id ?? null;
const detailsQuery = useChannelDetailsQuery(channelId, open);
const membersQuery = useChannelMembersQuery(channelId, open);
@@ -164,7 +134,6 @@ export function ChannelManagementSheet({
selfMember !== null;
const memberCount =
members.length || detail?.memberCount || channel?.memberCount || 0;
-
const [nameDraft, setNameDraft] = React.useState("");
const [descriptionDraft, setDescriptionDraft] = React.useState("");
const [topicDraft, setTopicDraft] = React.useState("");
@@ -174,6 +143,7 @@ export function ChannelManagementSheet({
const [ttlDraft, setTtlDraft] = React.useState("");
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
const [isCreateWorkflowOpen, setIsCreateWorkflowOpen] = React.useState(false);
+ const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false);
// Sync drafts from server only when the sheet opens or the channel changes —
// not on every background refetch, which would clobber in-flight edits.
@@ -184,6 +154,7 @@ export function ChannelManagementSheet({
syncedForRef.current = null;
setIsDeleteDialogOpen(false);
setIsCreateWorkflowOpen(false);
+ setIsEditDialogOpen(false);
return;
}
if (!detail) {
@@ -263,55 +234,70 @@ export function ChannelManagementSheet({
}
const resolvedChannel = detail ?? channel;
+ const canEditChannel = canManageChannel || canEditNarrative;
return (
-
-
+
-
- {channel.name}
-
+
+ Channel
+
Channel settings
-
-
-
+
+
+
+
+
+
+ {
+ void navigator.clipboard
+ .writeText(resolvedChannel.id)
+ .then(() => toast.success("Copied channel ID"));
+ }}
+ testId="channel-management-copy-id-action"
/>
-
-
- {isArchived ? (
-
+ {canJoin ? (
+ {
+ void joinChannelMutation.mutateAsync();
+ }}
+ testId="channel-management-join"
+ />
+ ) : null}
+ {canLeave ? (
+ {
+ void leaveChannelMutation.mutateAsync().then(() => {
+ onOpenChange(false);
+ });
+ }}
+ testId="channel-management-leave"
+ />
+ ) : null}
+ {canEditChannel ? (
+ setIsEditDialogOpen(true)}
+ testId="channel-management-edit"
+ />
) : null}
-
-
-
{detailsQuery.error instanceof Error ? (
{detailsQuery.error.message}
@@ -324,263 +310,116 @@ export function ChannelManagementSheet({
) : null}
- {canJoin ? (
-
-
{
- void joinChannelMutation.mutateAsync();
- }}
- size="sm"
- type="button"
- >
-
- {joinChannelMutation.isPending ? "Joining..." : "Join channel"}
-
- {joinChannelMutation.error instanceof Error ? (
-
- {joinChannelMutation.error.message}
-
+ {joinChannelMutation.error instanceof Error ? (
+
+ {joinChannelMutation.error.message}
+
+ ) : null}
+
+ {leaveChannelMutation.error instanceof Error ? (
+
+ {leaveChannelMutation.error.message}
+
+ ) : null}
+
+ {resolvedChannel.description.trim() ||
+ resolvedChannel.topic?.trim() ||
+ resolvedChannel.purpose?.trim() ? (
+
+ {resolvedChannel.description.trim() ? (
+
) : null}
-
+ {resolvedChannel.topic?.trim() ? (
+
+ ) : null}
+ {resolvedChannel.purpose?.trim() ? (
+
+ ) : null}
+
) : null}
-
+ {
+ setIsEditDialogOpen(true);
+ }}
+ >
+
+ Canvas
+
+
+ Open editor
+
+
-
-
- {resolvedChannel.channelType !== "dm" ? (
-
-
-
-
Private
-
- Only members can find and join this channel.
-
-
-
-
-
-
-
-
Ephemeral
-
- Automatically delete this channel after a set time.
-
-
-
-
-
- {isEphemeralDraft ? (
-
-
- Timeout
-
-
setTtlDraft(event.target.value)}
- placeholder="e.g. 1d, 12h, 30m"
- value={ttlDraft}
- />
-
- {ttlInvalid
- ? "Enter a duration like 1d, 12h, or 30m."
- : "Defaults to 1d when left empty. Resets the deletion countdown from now whenever changed."}
-
-
- ) : null}
-
-
- {updateChannelLifecycleMutation.isPending
- ? "Saving..."
- : "Save visibility"}
-
-
- ) : null}
-
-
{
- event.preventDefault();
- void setTopicMutation.mutateAsync({
- topic: topicDraft.trim(),
- });
- }}
- >
-
-
- Topic
-
- setTopicDraft(event.target.value)}
- value={topicDraft}
+ {resolvedChannel.ttlSeconds !== null ? (
+
-
-
- {setTopicMutation.isPending ? "Saving..." : "Save topic"}
-
- {setTopicMutation.error instanceof Error ? (
-
- {setTopicMutation.error.message}
-
) : null}
-
-
-
{
- event.preventDefault();
- void setPurposeMutation.mutateAsync({
- purpose: purposeDraft.trim(),
- });
- }}
- >
-
-
- Purpose
-
- setPurposeDraft(event.target.value)}
- value={purposeDraft}
- />
-
-
- {setPurposeMutation.isPending ? "Saving..." : "Save purpose"}
-
- {setPurposeMutation.error instanceof Error ? (
-
- {setPurposeMutation.error.message}
-
- ) : null}
-
+
{canEditNarrative ? (
setIsCreateWorkflowOpen(true)}
size="sm"
@@ -594,34 +433,12 @@ export function ChannelManagementSheet({
{resolvedChannel.channelType !== "dm" ? (
-
- {canLeave ? (
- {
- void leaveChannelMutation.mutateAsync().then(() => {
- onOpenChange(false);
- });
- }}
- size="sm"
- type="button"
- variant="outline"
- >
-
- {leaveChannelMutation.isPending ? "Leaving..." : "Leave"}
-
- ) : null}
{isArchived ? (
) : null}
- {leaveChannelMutation.error instanceof Error ? (
-
- {leaveChannelMutation.error.message}
-
- ) : null}
{archiveChannelMutation.error instanceof Error ? (
{archiveChannelMutation.error.message}
@@ -736,15 +548,279 @@ export function ChannelManagementSheet({
) : null}
-
+
) : null}
-
+
+
+
+
+
+ Edit channel
+
+ Edit channel details
+
+
+
+
+
{
+ event.preventDefault();
+ void updateChannelDetailsMutation.mutateAsync({
+ description: descriptionDraft.trim() || undefined,
+ name: nameDraft.trim() || undefined,
+ });
+ }}
+ >
+
+
+ Name
+
+ setNameDraft(event.target.value)}
+ value={nameDraft}
+ />
+
+
+
+ Description
+
+ setDescriptionDraft(event.target.value)}
+ value={descriptionDraft}
+ />
+
+
+ {updateChannelDetailsMutation.isPending
+ ? "Saving..."
+ : "Save details"}
+
+ {updateChannelDetailsMutation.error instanceof Error ? (
+
+ {updateChannelDetailsMutation.error.message}
+
+ ) : null}
+
+
+
+
+
+
+ {resolvedChannel.channelType !== "dm" ? (
+
+
+
+
+
Private
+
+ Only members can find and join this channel.
+
+
+
+
+
+
+
+
Ephemeral
+
+ Automatically delete this channel after a set time.
+
+
+
+
+
+
+ {isEphemeralDraft ? (
+
+
+ Timeout
+
+
setTtlDraft(event.target.value)}
+ placeholder="e.g. 1d, 12h, 30m"
+ value={ttlDraft}
+ />
+
+ {ttlInvalid
+ ? "Enter a duration like 1d, 12h, or 30m."
+ : "Defaults to 1d when left empty. Resets the deletion countdown from now whenever changed."}
+
+
+ ) : null}
+
+
+ {updateChannelLifecycleMutation.isPending
+ ? "Saving..."
+ : "Save visibility"}
+
+
+ ) : null}
+
+
+
{
+ event.preventDefault();
+ void setTopicMutation.mutateAsync({
+ topic: topicDraft.trim(),
+ });
+ }}
+ >
+
+
+ Topic
+
+ setTopicDraft(event.target.value)}
+ value={topicDraft}
+ />
+
+
+ {setTopicMutation.isPending ? "Saving..." : "Save topic"}
+
+ {setTopicMutation.error instanceof Error ? (
+
+ {setTopicMutation.error.message}
+
+ ) : null}
+
+
+
{
+ event.preventDefault();
+ void setPurposeMutation.mutateAsync({
+ purpose: purposeDraft.trim(),
+ });
+ }}
+ >
+
+
+ Purpose
+
+ setPurposeDraft(event.target.value)}
+ value={purposeDraft}
+ />
+
+
+ {setPurposeMutation.isPending ? "Saving..." : "Save purpose"}
+
+ {setPurposeMutation.error instanceof Error ? (
+
+ {setPurposeMutation.error.message}
+
+ ) : null}
+
+
+
+
+
-
+
);
}
diff --git a/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx b/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx
new file mode 100644
index 0000000000..7b57d23e36
--- /dev/null
+++ b/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx
@@ -0,0 +1,211 @@
+import {
+ BookOpenText,
+ ChevronRight,
+ Copy,
+ FileText,
+ Hash,
+ MessageSquare,
+ type LucideIcon,
+} from "lucide-react";
+import type * as React from "react";
+import { toast } from "sonner";
+
+import type { Channel } from "@/shared/api/types";
+import { cn } from "@/shared/lib/cn";
+
+function getChannelIcon(channelType: Channel["channelType"]): LucideIcon {
+ if (channelType === "forum") {
+ return FileText;
+ }
+ if (channelType === "dm") {
+ return MessageSquare;
+ }
+ return Hash;
+}
+
+export function ChannelHero({ channel }: { channel: Channel }) {
+ const Icon = getChannelIcon(channel.channelType);
+ return (
+
+
+
+
+
+ {channel.name}
+
+
+ );
+}
+
+export function ChannelQuickAction({
+ active,
+ disabled,
+ icon: Icon,
+ label,
+ onClick,
+ testId,
+}: {
+ active?: boolean;
+ disabled?: boolean;
+ icon: LucideIcon;
+ label: string;
+ onClick: () => void;
+ testId?: string;
+}) {
+ return (
+
+
+
+
+
+ {label}
+
+
+ );
+}
+
+export function FieldGroup({ children }: { children: React.ReactNode }) {
+ return (
+ {children}
+ );
+}
+
+function FieldIcon({ icon: Icon }: { icon: LucideIcon }) {
+ return (
+
+
+
+ );
+}
+
+export function InfoFieldRow({
+ icon,
+ label,
+ value,
+ testId,
+}: {
+ icon: LucideIcon;
+ label: string;
+ value: string;
+ testId?: string;
+}) {
+ return (
+
+
+
+
+ {label}
+
+
+ {value}
+
+
+
+ );
+}
+
+export function CopyFieldRow({
+ icon,
+ label,
+ value,
+ testId,
+}: {
+ icon: LucideIcon;
+ label: string;
+ value: string;
+ testId?: string;
+}) {
+ async function handleCopy() {
+ await navigator.clipboard.writeText(value);
+ toast.success(`Copied ${label.toLowerCase()}`);
+ }
+
+ return (
+ {
+ void handleCopy();
+ }}
+ title={`Copy ${label}`}
+ type="button"
+ >
+
+
+
+ {label}
+
+
+ {value}
+
+
+
+
+ );
+}
+
+export function NarrativeField({
+ icon,
+ label,
+ value,
+ testId,
+}: {
+ icon: LucideIcon;
+ label: string;
+ value: string;
+ testId: string;
+}) {
+ return (
+
+
+
+
+ {label}
+
+
+ {value}
+
+
+
+ );
+}
+
+export function CanvasSummaryRow({
+ children,
+ onClick,
+}: {
+ children: React.ReactNode;
+ onClick: () => void;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/desktop/tests/e2e/channel-controls-screenshots.spec.ts b/desktop/tests/e2e/channel-controls-screenshots.spec.ts
index ed6c8126cc..c17ebbc3d9 100644
--- a/desktop/tests/e2e/channel-controls-screenshots.spec.ts
+++ b/desktop/tests/e2e/channel-controls-screenshots.spec.ts
@@ -6,12 +6,19 @@ const SHOTS = "test-results/channel-controls";
// `general` seeds the mock identity as owner, so the owner/admin-gated
// visibility + ephemeral controls are live and interactive.
-async function openManagementSheet(page: import("@playwright/test").Page) {
+async function openManagementModal(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
+}
+
+async function openManagementEdit(page: import("@playwright/test").Page) {
+ await page.getByTestId("channel-management-edit").click();
+ await expect(
+ page.getByTestId("channel-management-edit-dialog"),
+ ).toBeVisible();
}
async function settle(page: import("@playwright/test").Page) {
@@ -25,7 +32,8 @@ test.describe("channel controls screenshots", () => {
page,
}) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
+ await openManagementEdit(page);
const lifecycle = page.getByTestId("channel-management-lifecycle");
await lifecycle.scrollIntoViewIfNeeded();
@@ -42,7 +50,8 @@ test.describe("channel controls screenshots", () => {
test("02 — Private toggled on", async ({ page }) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
+ await openManagementEdit(page);
const lifecycle = page.getByTestId("channel-management-lifecycle");
await lifecycle.scrollIntoViewIfNeeded();
@@ -61,7 +70,8 @@ test.describe("channel controls screenshots", () => {
test("03 — Ephemeral on with friendly timeout field", async ({ page }) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
+ await openManagementEdit(page);
const lifecycle = page.getByTestId("channel-management-lifecycle");
await lifecycle.scrollIntoViewIfNeeded();
@@ -82,7 +92,8 @@ test.describe("channel controls screenshots", () => {
page,
}) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
+ await openManagementEdit(page);
const lifecycle = page.getByTestId("channel-management-lifecycle");
await lifecycle.scrollIntoViewIfNeeded();
@@ -101,7 +112,7 @@ test.describe("channel controls screenshots", () => {
test("05 — sticky footer pins lifecycle buttons", async ({ page }) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
const footer = page.getByTestId("channel-management-footer");
await expect(footer).toBeVisible();
@@ -111,22 +122,23 @@ test.describe("channel controls screenshots", () => {
await footer.screenshot({ path: `${SHOTS}/05-sticky-footer.png` });
});
- test("06 — full sheet with new controls", async ({ page }) => {
+ test("06 — full modal with new controls", async ({ page }) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
- const sheet = page.getByTestId("channel-management-sheet");
- await expect(sheet).toBeVisible();
+ const modal = page.getByTestId("channel-management-modal");
+ await expect(modal).toBeVisible();
await settle(page);
- await sheet.screenshot({ path: `${SHOTS}/06-management-sheet.png` });
+ await modal.screenshot({ path: `${SHOTS}/06-management-modal.png` });
});
test("07 — saving lifecycle leaves details save idle", async ({ page }) => {
await installMockBridge(page, { updateChannelDelayMs: 500 });
- await openManagementSheet(page);
+ await openManagementModal(page);
+ await openManagementEdit(page);
- const sheet = page.getByTestId("channel-management-sheet");
+ const modal = page.getByTestId("channel-management-edit-dialog");
await page.getByTestId("channel-management-ephemeral-toggle").click();
await expect(
page.getByTestId("channel-management-save-lifecycle"),
@@ -139,7 +151,7 @@ test.describe("channel controls screenshots", () => {
await expect(
page.getByTestId("channel-management-save-details"),
).toHaveText("Save details");
- await sheet.screenshot({
+ await modal.screenshot({
path: `${SHOTS}/07-lifecycle-saving-details-idle.png`,
});
@@ -152,7 +164,8 @@ test.describe("channel controls screenshots", () => {
page,
}) => {
await installMockBridge(page);
- await openManagementSheet(page);
+ await openManagementModal(page);
+ await openManagementEdit(page);
await page.getByTestId("channel-management-private-toggle").click();
await page.getByTestId("channel-management-ephemeral-toggle").click();
@@ -163,9 +176,14 @@ test.describe("channel controls screenshots", () => {
await page.keyboard.press("Escape");
await expect(
- page.getByTestId("channel-management-sheet"),
+ page.getByTestId("channel-management-edit-dialog"),
+ ).not.toBeVisible();
+ await page.keyboard.press("Escape");
+ await expect(
+ page.getByTestId("channel-management-modal"),
).not.toBeVisible();
await page.getByTestId("channel-management-trigger").click();
+ await openManagementEdit(page);
const lifecycle = page.getByTestId("channel-management-lifecycle");
await lifecycle.scrollIntoViewIfNeeded();
diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts
index 3437b0f181..44fa1eed86 100644
--- a/desktop/tests/e2e/channels.spec.ts
+++ b/desktop/tests/e2e/channels.spec.ts
@@ -12,12 +12,27 @@ async function openChannelManagement(
await page.getByTestId(`channel-${channelName}`).click();
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
}
async function closeChannelManagement(page: import("@playwright/test").Page) {
+ if (await page.getByTestId("channel-management-edit-dialog").isVisible()) {
+ await page.keyboard.press("Escape");
+ await expect(
+ page.getByTestId("channel-management-edit-dialog"),
+ ).not.toBeVisible();
+ }
await page.keyboard.press("Escape");
- await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).not.toBeVisible();
+}
+
+async function openChannelManagementEdit(
+ page: import("@playwright/test").Page,
+) {
+ await page.getByTestId("channel-management-edit").click();
+ await expect(
+ page.getByTestId("channel-management-edit-dialog"),
+ ).toBeVisible();
}
async function openMembersSidebar(
@@ -1055,6 +1070,7 @@ test("manage channel updates details and context", async ({ page }) => {
await page.goto("/");
await openChannelManagement(page, "general");
+ await openChannelManagementEdit(page);
await page.getByTestId("channel-management-name").fill(newName);
await page.getByTestId("channel-management-description").fill(newDescription);
@@ -1088,7 +1104,8 @@ test("manage channel updates details and context", async ({ page }) => {
await page.getByTestId("stream-list").getByText(newName).click();
await expect(page.getByTestId("chat-title")).toHaveText(newName);
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
+ await openChannelManagementEdit(page);
await expect(page.getByTestId("channel-management-name")).toHaveValue(
newName,
@@ -1109,6 +1126,7 @@ test("manage channel updates visibility and ephemeral lifecycle independently",
}) => {
await page.goto("/");
await openChannelManagement(page, "general");
+ await openChannelManagementEdit(page);
const saveDetailsButton = page.getByTestId("channel-management-save-details");
const saveLifecycleButton = page.getByTestId(
@@ -1152,6 +1170,7 @@ test("manage channel updates visibility and ephemeral lifecycle independently",
await closeChannelManagement(page);
await openChannelManagement(page, "general");
+ await openChannelManagementEdit(page);
await expect(
page.getByTestId("channel-management-private-toggle"),
@@ -1196,6 +1215,7 @@ test("manage channel updates visibility and ephemeral lifecycle independently",
await closeChannelManagement(page);
await openChannelManagement(page, "general");
+ await openChannelManagementEdit(page);
await expect(
page.getByTestId("channel-management-private-toggle"),
@@ -1206,25 +1226,17 @@ test("manage channel updates visibility and ephemeral lifecycle independently",
await expect(page.getByTestId("channel-management-ttl")).toHaveCount(0);
});
-test("manage channel keeps canvas near the top of the sheet", async ({
+test("manage channel includes the canvas section in the modal", async ({
page,
}) => {
await page.goto("/");
await openChannelManagement(page, "general");
- const sheet = page.getByTestId("channel-management-sheet");
-
- // Canvas section should appear before the name input in the DOM.
- const canvasBox = await sheet
- .getByTestId("channel-canvas-section")
- .boundingBox();
- const nameBox = await sheet
- .getByTestId("channel-management-name")
- .boundingBox();
-
- expect(canvasBox).not.toBeNull();
- expect(nameBox).not.toBeNull();
- expect(canvasBox?.y).toBeLessThan(nameBox?.y);
+ await expect(
+ page
+ .getByTestId("channel-management-modal")
+ .getByTestId("channel-canvas-section"),
+ ).toBeVisible();
});
test("members sidebar can invite and remove members", async ({ page }) => {
@@ -1583,13 +1595,13 @@ test("open channel management supports join and leave", async ({ page }) => {
// Open channel management — should show Leave since we just joined
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
await expect(page.getByTestId("channel-management-join")).toHaveCount(0);
await expect(page.getByTestId("channel-management-leave")).toBeVisible();
// Leave the channel
await page.getByTestId("channel-management-leave").click();
- await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).not.toBeVisible();
// After leaving, the app navigates away — re-open browser and find design
await page.getByTestId("browse-channels").click();
@@ -1628,7 +1640,7 @@ test("manage channel can archive and unarchive a stream", async ({ page }) => {
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
await page.getByTestId("channel-management-unarchive").click();
await expect(page.getByTestId("channel-management-archive")).toBeVisible();
@@ -1650,7 +1662,7 @@ test("manage channel can delete an owned stream", async ({ page }) => {
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
await page.getByTestId("channel-management-delete").click();
await expect(
page.getByTestId("channel-delete-confirmation-dialog"),
@@ -1671,7 +1683,7 @@ test("canceling channel deletion keeps the owned stream", async ({ page }) => {
await expect(page.getByTestId("chat-title")).toHaveText(channelName);
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
await page.getByTestId("channel-management-delete").click();
await expect(
page.getByTestId("channel-delete-confirmation-dialog"),
diff --git a/desktop/tests/e2e/integration.spec.ts b/desktop/tests/e2e/integration.spec.ts
index 63d019e326..815f6de52a 100644
--- a/desktop/tests/e2e/integration.spec.ts
+++ b/desktop/tests/e2e/integration.spec.ts
@@ -25,12 +25,27 @@ async function createStream(
async function openChannelManagement(page: import("@playwright/test").Page) {
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
}
async function closeChannelManagement(page: import("@playwright/test").Page) {
+ if (await page.getByTestId("channel-management-edit-dialog").isVisible()) {
+ await page.keyboard.press("Escape");
+ await expect(
+ page.getByTestId("channel-management-edit-dialog"),
+ ).not.toBeVisible();
+ }
await page.keyboard.press("Escape");
- await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).not.toBeVisible();
+}
+
+async function openChannelManagementEdit(
+ page: import("@playwright/test").Page,
+) {
+ await page.getByTestId("channel-management-edit").click();
+ await expect(
+ page.getByTestId("channel-management-edit-dialog"),
+ ).toBeVisible();
}
async function assertDesktopNotificationsEnabled(
@@ -439,7 +454,7 @@ test("multiple channels independent", async ({ page }) => {
);
});
-test("manage sheet updates channel details and context through the relay", async ({
+test("manage modal updates channel details and context through the relay", async ({
page,
}) => {
const stamp = Date.now();
@@ -455,6 +470,7 @@ test("manage sheet updates channel details and context through the relay", async
await createStream(page, initialName, initialDescription);
await openChannelManagement(page);
+ await openChannelManagementEdit(page);
await page.getByTestId("channel-management-name").fill(renamedChannel);
await page
.getByTestId("channel-management-description")
@@ -493,6 +509,7 @@ test("manage sheet updates channel details and context through the relay", async
);
await openChannelManagement(page);
+ await openChannelManagementEdit(page);
await expect(page.getByTestId("channel-management-name")).toHaveValue(
renamedChannel,
);
@@ -507,7 +524,7 @@ test("manage sheet updates channel details and context through the relay", async
);
});
-test("manage sheet archive and unarchive survives a reload through the relay", async ({
+test("manage modal archive and unarchive survives a reload through the relay", async ({
page,
}) => {
const channelName = `archive-integration-${Date.now()}`;
diff --git a/desktop/tests/e2e/relay-connectivity-screenshots.spec.ts b/desktop/tests/e2e/relay-connectivity-screenshots.spec.ts
index ee42430d76..053a20e09a 100644
--- a/desktop/tests/e2e/relay-connectivity-screenshots.spec.ts
+++ b/desktop/tests/e2e/relay-connectivity-screenshots.spec.ts
@@ -105,14 +105,14 @@ test.describe("relay connectivity screenshots", () => {
});
});
- test("04 — canvas unreachable in management sheet", async ({ page }) => {
+ test("04 — canvas unreachable in management modal", async ({ page }) => {
await installMockBridge(page, { canvasReadError: RELAY_UNREACHABLE });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("channel-management-trigger").click();
- await expect(page.getByTestId("channel-management-sheet")).toBeVisible();
+ await expect(page.getByTestId("channel-management-modal")).toBeVisible();
// ChannelCanvas shows the destructive error paragraph when the query fails.
const canvasSection = page.getByTestId("channel-canvas-section");
@@ -121,9 +121,9 @@ test.describe("relay connectivity screenshots", () => {
canvasSection.getByText("Can't reach the relay."),
).toBeVisible();
- // Await Radix sheet animations before screenshotting.
- const sheet = page.getByTestId("channel-management-sheet");
- await sheet.evaluate((el) =>
+ // Await Radix dialog animations before screenshotting.
+ const modal = page.getByTestId("channel-management-modal");
+ await modal.evaluate((el) =>
Promise.all(
el
.closest("[data-state]")
@@ -133,8 +133,8 @@ test.describe("relay connectivity screenshots", () => {
);
await settle(page);
- // Capture the whole sheet so the error renders in its Canvas-section context.
- await sheet.screenshot({
+ // Capture the whole modal so the error renders in its Canvas-section context.
+ await modal.screenshot({
path: `${SHOTS}/04-canvas-unreachable.png`,
});
});