diff --git a/desktop/src/features/channels/hooks.ts b/desktop/src/features/channels/hooks.ts index 121e2dbb1c..9003d0f5a5 100644 --- a/desktop/src/features/channels/hooks.ts +++ b/desktop/src/features/channels/hooks.ts @@ -348,13 +348,10 @@ export function useUpdateChannelMutation(channelId: string | null) { return updateChannel({ ...input, channelId }); }, + onMutate: () => ({ channelId }), onSuccess: (updatedChannel) => { - if (!channelId) { - return; - } - queryClient.setQueryData( - channelDetailQueryKey(channelId), + channelDetailQueryKey(updatedChannel.id), updatedChannel, ); queryClient.setQueryData(channelsQueryKey, (current = []) => @@ -365,7 +362,7 @@ export function useUpdateChannelMutation(channelId: string | null) { ), ); }, - onSettled: () => { + onSettled: (_data, _error, _variables, context) => { // refetchType "none": onSuccess already cached the relay-returned detail; // awaiting the full channel-list refetch kept the edit dialog stuck on // "Saving..." (same failure #1360 fixed for create). @@ -373,9 +370,9 @@ export function useUpdateChannelMutation(channelId: string | null) { queryKey: channelsQueryKey, refetchType: "none", }); - if (channelId) { + if (context?.channelId) { void queryClient.invalidateQueries({ - queryKey: channelDetailQueryKey(channelId), + queryKey: channelDetailQueryKey(context.channelId), refetchType: "none", }); } diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx index 57c1f6c140..3c1727e00a 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx @@ -27,8 +27,6 @@ import { useDeleteChannelMutation, useJoinChannelMutation, useLeaveChannelMutation, - useSetChannelPurposeMutation, - useSetChannelTopicMutation, useUnarchiveChannelMutation, useUpdateChannelMutation, } from "@/features/channels/hooks"; @@ -36,7 +34,6 @@ import { compareMembersByRole } from "@/features/channels/lib/memberUtils"; import { DEFAULT_EPHEMERAL_TTL_SECONDS, formatTtlDuration, - parseTtlDuration, } from "@/features/channels/lib/ephemeralChannel"; import type { Channel } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; @@ -45,7 +42,6 @@ import { Button } from "@/shared/ui/button"; import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; @@ -68,6 +64,12 @@ import { PANEL_OVERLAY_CLASS, } from "@/shared/ui/OverlayPanelBackdrop"; import { ChannelCanvas } from "./ChannelCanvas"; +import { + CHANNEL_FORM_FIELD_CONTROL_CLASS, + CHANNEL_FORM_FIELD_SHELL_CLASS, +} from "./channelFormStyles"; +import { ChannelTypeSettings } from "./ChannelTypeSettings"; +import { ChannelPermissionsSettings } from "./ChannelPermissionsSettings"; import { ChannelHero, ChannelQuickAction, @@ -78,7 +80,6 @@ import { IngressRow, NarrativeField, NarrativeGroup, - ToggleRow, } from "./ChannelManagementSheetRows"; import { ChannelManagementModerationActions, @@ -118,13 +119,13 @@ export function ChannelManagementSheet({ const membersQuery = useChannelMembersQuery(channelId, open); const canvasQuery = useCanvasQuery(channelId, channelId !== null && open); const updateChannelDetailsMutation = useUpdateChannelMutation(channelId); - const setTopicMutation = useSetChannelTopicMutation(channelId); - const setPurposeMutation = useSetChannelPurposeMutation(channelId); const archiveChannelMutation = useArchiveChannelMutation(channelId); const unarchiveChannelMutation = useUnarchiveChannelMutation(channelId); const deleteChannelMutation = useDeleteChannelMutation(channelId); const joinChannelMutation = useJoinChannelMutation(channelId); const leaveChannelMutation = useLeaveChannelMutation(channelId); + const channelIdRef = React.useRef(channelId); + channelIdRef.current = channelId; const detail = detailsQuery.data ?? channel; const members = React.useMemo(() => { @@ -159,13 +160,17 @@ export function ChannelManagementSheet({ const [nameDraft, setNameDraft] = React.useState(""); const [descriptionDraft, setDescriptionDraft] = React.useState(""); - const [topicDraft, setTopicDraft] = React.useState(""); - const [purposeDraft, setPurposeDraft] = React.useState(""); const [isPrivateDraft, setIsPrivateDraft] = React.useState(false); const [isEphemeralDraft, setIsEphemeralDraft] = React.useState(false); - const [ttlDraft, setTtlDraft] = React.useState(""); + const [ttlSecondsDraft, setTtlSecondsDraft] = React.useState( + DEFAULT_EPHEMERAL_TTL_SECONDS, + ); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = React.useState(false); + const [isConvertingVisibility, setIsConvertingVisibility] = + React.useState(false); + const [hasUserEditedChannelDraft, setHasUserEditedChannelDraft] = + React.useState(false); const [activeView, setActiveView] = React.useState<"summary" | "canvas">( "summary", ); @@ -194,13 +199,10 @@ export function ChannelManagementSheet({ setNameDraft(detail.name); setDescriptionDraft(detail.description); - setTopicDraft(detail.topic ?? ""); - setPurposeDraft(detail.purpose ?? ""); setIsPrivateDraft(detail.visibility === "private"); setIsEphemeralDraft(detail.ttlSeconds !== null); - setTtlDraft( - detail.ttlSeconds !== null ? formatTtlDuration(detail.ttlSeconds) : "", - ); + setTtlSecondsDraft(detail.ttlSeconds ?? DEFAULT_EPHEMERAL_TTL_SECONDS); + setHasUserEditedChannelDraft(false); setActiveView("summary"); }, [detail, open]); @@ -232,19 +234,13 @@ export function ChannelManagementSheet({ onOpenChange(next); } - // Parsed seconds for the ephemeral TTL field. `null` when the field is empty - // or malformed; the form blocks saving on a non-empty malformed value. - const parsedTtlSeconds = parseTtlDuration(ttlDraft); - const ttlInvalid = - isEphemeralDraft && ttlDraft.trim() !== "" && parsedTtlSeconds === null; - const currentVisibility = detail?.visibility ?? channel.visibility; const currentTtlSeconds = detail?.ttlSeconds ?? null; const nextVisibility: "open" | "private" = isPrivateDraft ? "private" : "open"; const nextTtlSeconds: number | null = isEphemeralDraft - ? (parsedTtlSeconds ?? DEFAULT_EPHEMERAL_TTL_SECONDS) + ? ttlSecondsDraft : null; const lifecycleDirty = nextVisibility !== currentVisibility || @@ -254,22 +250,11 @@ export function ChannelManagementSheet({ 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 isSavingChannelEdits = updateChannelDetailsMutation.isPending; + const hasChannelEditChanges = nameDirty || descriptionDirty || lifecycleDirty; const canSaveChannelEdits = nameDraft.trim().length > 0 && - !ttlInvalid && + hasUserEditedChannelDraft && hasChannelEditChanges && !isSavingChannelEdits; const canvasContent = canvasQuery.data?.content?.trim() ?? ""; @@ -279,6 +264,18 @@ export function ChannelManagementSheet({ : undefined; const canOpenCanvas = hasCanvas || canEditNarrative; + function handleEditDialogOpenChange(next: boolean) { + if (!next) { + setNameDraft(resolvedChannel.name); + setDescriptionDraft(resolvedChannel.description); + setIsEphemeralDraft(currentTtlSeconds !== null); + setTtlSecondsDraft(currentTtlSeconds ?? DEFAULT_EPHEMERAL_TTL_SECONDS); + setHasUserEditedChannelDraft(false); + } + + setIsEditDialogOpen(next); + } + async function handleSaveChannelEdits() { try { if (nameDirty || descriptionDirty || lifecycleDirty) { @@ -294,17 +291,29 @@ export function ChannelManagementSheet({ }); } - if (topicDirty) { - await setTopicMutation.mutateAsync({ topic: topicDraft.trim() }); - } + setHasUserEditedChannelDraft(false); + setIsEditDialogOpen(false); + } catch { + // React Query stores mutation errors; keep the dialog open and render them. + } + } - if (purposeDirty) { - await setPurposeMutation.mutateAsync({ purpose: purposeDraft.trim() }); + async function handleConvertVisibility(visibility: "open" | "private") { + if (visibility === currentVisibility) { + return; + } + setIsConvertingVisibility(true); + try { + const updatedChannel = await updateChannelDetailsMutation.mutateAsync({ + visibility, + }); + if (channelIdRef.current === updatedChannel.id) { + setIsPrivateDraft(visibility === "private"); } - - setIsEditDialogOpen(false); } catch { // React Query stores mutation errors; keep the dialog open and render them. + } finally { + setIsConvertingVisibility(false); } } @@ -421,153 +430,108 @@ export function ChannelManagementSheet({ )} {canManageChannel ? ( - - + +
- Edit channel - - Update settings for{" "} - {resolvedChannel.name}. - + + Edit {currentVisibility === "private" ? "private" : "public"}{" "} + channel +
-
+
- setNameDraft(event.target.value)} - value={nameDraft} - /> +
+ { + setNameDraft(event.target.value); + setHasUserEditedChannelDraft(true); + }} + value={nameDraft} + /> +
-