From 881dcca4f862069efa281e33cd68c95710044e63 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Tue, 16 Jun 2026 20:58:42 -0400 Subject: [PATCH] feat(desktop): add channel settings modal Co-authored-by: Cursor --- .../channels/ui/ChannelManagementSheet.tsx | 822 ++++++++++-------- .../ui/ChannelManagementSheetRows.tsx | 211 +++++ .../e2e/channel-controls-screenshots.spec.ts | 52 +- desktop/tests/e2e/channels.spec.ts | 56 +- desktop/tests/e2e/integration.spec.ts | 25 +- .../relay-connectivity-screenshots.spec.ts | 14 +- 6 files changed, 757 insertions(+), 423 deletions(-) create mode 100644 desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx 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 ( - - ); -} - 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 ? ( -
- - {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 + +
-
{ - event.preventDefault(); - void updateChannelDetailsMutation.mutateAsync({ - description: descriptionDraft.trim() || undefined, - name: nameDraft.trim() || undefined, - }); - }} - > -
- - setNameDraft(event.target.value)} - value={nameDraft} + + + + + + + {isArchived ? ( + -
-
- -