From 016d246ccd00757dddce6426bf5803be7613d65c Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Mon, 15 Jun 2026 22:43:36 -0400 Subject: [PATCH 1/8] refactor(desktop): align channel management panel with profile --- .../features/channels/ui/ChannelCanvas.tsx | 11 +- .../channels/ui/ChannelManagementSheet.tsx | 1074 +++++++++-------- .../ui/ChannelManagementSheetRows.tsx | 304 +++++ 3 files changed, 892 insertions(+), 497 deletions(-) create mode 100644 desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx diff --git a/desktop/src/features/channels/ui/ChannelCanvas.tsx b/desktop/src/features/channels/ui/ChannelCanvas.tsx index 8b379f367..ec304f7e6 100644 --- a/desktop/src/features/channels/ui/ChannelCanvas.tsx +++ b/desktop/src/features/channels/ui/ChannelCanvas.tsx @@ -117,15 +117,8 @@ export function ChannelCanvas({ return (
{canvasContent ? ( -
- +
+
) : (

diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx index e708dfb9b..70c29dda8 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx @@ -1,14 +1,21 @@ import { Archive, ArchiveRestore, + BookOpenText, + ChevronLeft, Copy, DoorClosed, DoorOpen, FileText, - Hash, + Fingerprint, + Eye, Lock, MessageSquare, + Pencil, + Radio, + Type, Users, + X, Zap, } from "lucide-react"; import * as React from "react"; @@ -16,6 +23,7 @@ import { toast } from "sonner"; import { useArchiveChannelMutation, + useCanvasQuery, useChannelDetailsQuery, useChannelMembersQuery, useDeleteChannelMutation, @@ -31,7 +39,6 @@ import { formatTtlDuration, parseTtlDuration, } from "@/features/channels/lib/ephemeralChannel"; -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"; @@ -47,18 +54,36 @@ import { AlertDialogTrigger, } from "@/shared/ui/alert-dialog"; import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; import { Input } from "@/shared/ui/input"; import { Sheet, + SheetClose, SheetContent, SheetDescription, - SheetFooter, SheetHeader, SheetTitle, } from "@/shared/ui/sheet"; -import { Switch } from "@/shared/ui/switch"; import { Textarea } from "@/shared/ui/textarea"; import { ChannelCanvas } from "./ChannelCanvas"; +import { + ChannelHero, + ChannelQuickAction, + CopyFieldRow, + FieldGroup, + getMarkdownPreviewText, + InfoFieldRow, + IngressRow, + NarrativeField, + NarrativeGroup, + ToggleRow, +} from "./ChannelManagementSheetRows"; type ChannelManagementSheetProps = { channel: Channel | null; @@ -70,50 +95,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, @@ -125,8 +106,8 @@ export function ChannelManagementSheet({ const channelId = channel?.id ?? null; const detailsQuery = useChannelDetailsQuery(channelId, open); const membersQuery = useChannelMembersQuery(channelId, open); + const canvasQuery = useCanvasQuery(channelId, channelId !== null && open); const updateChannelDetailsMutation = useUpdateChannelMutation(channelId); - const updateChannelLifecycleMutation = useUpdateChannelMutation(channelId); const setTopicMutation = useSetChannelTopicMutation(channelId); const setPurposeMutation = useSetChannelPurposeMutation(channelId); const archiveChannelMutation = useArchiveChannelMutation(channelId); @@ -148,7 +129,8 @@ export function ChannelManagementSheet({ const isOwner = selfMember?.role === "owner"; const canManageChannel = selfMember?.role === "owner" || selfMember?.role === "admin"; - const canEditNarrative = selfMember !== null && detail?.channelType !== "dm"; + const canEditNarrative = + canManageChannel && selfMember !== null && detail?.channelType !== "dm"; const isArchived = detail?.archivedAt !== null && detail?.archivedAt !== undefined; const canJoin = @@ -173,7 +155,10 @@ export function ChannelManagementSheet({ const [isEphemeralDraft, setIsEphemeralDraft] = React.useState(false); const [ttlDraft, setTtlDraft] = React.useState(""); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); - const [isCreateWorkflowOpen, setIsCreateWorkflowOpen] = React.useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false); + const [activeView, setActiveView] = React.useState<"summary" | "canvas">( + "summary", + ); // Sync drafts from server only when the sheet opens or the channel changes — // not on every background refetch, which would clobber in-flight edits. @@ -183,7 +168,8 @@ export function ChannelManagementSheet({ // Reset on close so the next open re-syncs from server. syncedForRef.current = null; setIsDeleteDialogOpen(false); - setIsCreateWorkflowOpen(false); + setIsEditDialogOpen(false); + setActiveView("summary"); return; } if (!detail) { @@ -205,6 +191,7 @@ export function ChannelManagementSheet({ setTtlDraft( detail.ttlSeconds !== null ? formatTtlDuration(detail.ttlSeconds) : "", ); + setActiveView("summary"); }, [detail, open]); if (!channel) { @@ -253,22 +240,69 @@ export function ChannelManagementSheet({ nextVisibility !== currentVisibility || nextTtlSeconds !== currentTtlSeconds; - function handleSaveLifecycle() { - void updateChannelLifecycleMutation.mutateAsync({ - visibility: - nextVisibility !== currentVisibility ? nextVisibility : undefined, - ttlSeconds: - nextTtlSeconds !== currentTtlSeconds ? nextTtlSeconds : undefined, - }); - } - const resolvedChannel = detail ?? channel; + const nameDirty = nameDraft.trim() !== resolvedChannel.name.trim(); + const descriptionDirty = + descriptionDraft.trim() !== resolvedChannel.description.trim(); + const topicDirty = topicDraft.trim() !== (resolvedChannel.topic ?? "").trim(); + const purposeDirty = + purposeDraft.trim() !== (resolvedChannel.purpose ?? "").trim(); + const isSavingChannelEdits = + updateChannelDetailsMutation.isPending || + setTopicMutation.isPending || + setPurposeMutation.isPending; + const hasChannelEditChanges = + nameDirty || + descriptionDirty || + lifecycleDirty || + topicDirty || + purposeDirty; + const canSaveChannelEdits = + nameDraft.trim().length > 0 && + !ttlInvalid && + hasChannelEditChanges && + !isSavingChannelEdits; + const canvasContent = canvasQuery.data?.content?.trim() ?? ""; + const hasCanvas = canvasContent.length > 0; + const canvasPreview = hasCanvas + ? getMarkdownPreviewText(canvasContent) + : undefined; + const canOpenCanvas = hasCanvas || canEditNarrative; + + async function handleSaveChannelEdits() { + try { + if (nameDirty || descriptionDirty || lifecycleDirty) { + await updateChannelDetailsMutation.mutateAsync({ + description: descriptionDirty ? descriptionDraft.trim() : undefined, + name: nameDirty ? nameDraft.trim() : undefined, + ttlSeconds: + nextTtlSeconds !== currentTtlSeconds ? nextTtlSeconds : undefined, + visibility: + lifecycleDirty && nextVisibility !== currentVisibility + ? nextVisibility + : undefined, + }); + } + + if (topicDirty) { + await setTopicMutation.mutateAsync({ topic: topicDraft.trim() }); + } + + if (purposeDirty) { + await setPurposeMutation.mutateAsync({ purpose: purposeDraft.trim() }); + } + + setIsEditDialogOpen(false); + } catch { + // React Query stores mutation errors; keep the dialog open and render them. + } + } return ( button]:hidden", isDark ? "bg-background/85 backdrop-blur-xl supports-[backdrop-filter]:bg-background/75" : "bg-background", @@ -278,473 +312,537 @@ export function ChannelManagementSheet({ > - {channel.name} - - Channel settings - -
- - - - {isArchived ? ( - - ) : null} -
-
- -
- - {detailsQuery.error instanceof Error ? ( -

- {detailsQuery.error.message} -

- ) : null} - - {membersQuery.error instanceof Error ? ( -

- {membersQuery.error.message} -

- ) : null} - - {canJoin ? ( -
+
+ {activeView === "canvas" ? ( - {joinChannelMutation.error instanceof Error ? ( -

- {joinChannelMutation.error.message} -

- ) : null} -
- ) : null} - -
- + ) : null} + + {activeView === "canvas" ? "Canvas" : "Channel"} +
- -
{ - event.preventDefault(); - void updateChannelDetailsMutation.mutateAsync({ - description: descriptionDraft.trim() || undefined, - name: nameDraft.trim() || undefined, - }); - }} - > -
- - setNameDraft(event.target.value)} - value={nameDraft} - /> -
-
- -