From 12cfc64ec27f93f7d9dbfbb744d79b771a31f420 Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 19 Mar 2026 17:29:48 -0700 Subject: [PATCH] feat(desktop): add private channel support to UI (#121) --- desktop/package.json | 1 + desktop/pnpm-lock.yaml | 47 +++++++++++ desktop/src/app/AppShell.tsx | 8 +- .../src/features/sidebar/ui/AppSidebar.tsx | 79 ++++++++++++++++++- .../features/sidebar/ui/SidebarSection.tsx | 6 +- desktop/src/shared/ui/checkbox.tsx | 28 +++++++ 6 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 desktop/src/shared/ui/checkbox.tsx diff --git a/desktop/package.json b/desktop/package.json index b520a014a..28c8dd6b7 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 20f3f5407..a895a321c 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@radix-ui/react-alert-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -502,6 +505,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -805,6 +821,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.1': resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} peerDependencies: @@ -2280,6 +2305,22 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) @@ -2583,6 +2624,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': dependencies: '@radix-ui/rect': 1.1.1 diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 192e3e424..20d75f44e 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -531,22 +531,22 @@ export function AppShell() { isLoading={channelsQuery.isLoading} isOpeningDm={openDmMutation.isPending} selfPresenceStatus={presenceSession.currentStatus} - onCreateChannel={async ({ description, name }) => { + onCreateChannel={async ({ description, name, visibility }) => { const createdChannel = await createChannelMutation.mutateAsync({ name, description, channelType: "stream", - visibility: "open", + visibility, }); openChannelView(createdChannel.id); }} - onCreateForum={async ({ description, name }) => { + onCreateForum={async ({ description, name, visibility }) => { const createdForum = await createForumMutation.mutateAsync({ name, description, channelType: "forum", - visibility: "open", + visibility, }); openChannelView(createdForum.id); diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index b417e59bd..b49cbed4f 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -1,5 +1,5 @@ import { getCurrentWindow } from "@tauri-apps/api/window"; -import { Bot, Home, PenSquare, Plus, Search } from "lucide-react"; +import { Bot, Home, Lock, PenSquare, Plus, Search } from "lucide-react"; import * as React from "react"; import { useManagedAgentsQuery } from "@/features/agents/hooks"; @@ -12,8 +12,14 @@ import { SidebarSection, } from "@/features/sidebar/ui/SidebarSection"; import { NewDirectMessageDialog } from "@/features/sidebar/ui/NewDirectMessageDialog"; -import type { Channel, PresenceStatus, Profile } from "@/shared/api/types"; +import type { + Channel, + ChannelVisibility, + PresenceStatus, + Profile, +} from "@/shared/api/types"; import { Button } from "@/shared/ui/button"; +import { Checkbox } from "@/shared/ui/checkbox"; import { Input } from "@/shared/ui/input"; import { Sidebar, @@ -61,10 +67,12 @@ type AppSidebarProps = { onCreateChannel: (input: { name: string; description?: string; + visibility: ChannelVisibility; }) => Promise; onCreateForum: (input: { name: string; description?: string; + visibility: ChannelVisibility; }) => Promise; onOpenBrowseChannels: () => void; onOpenBrowseForums: () => void; @@ -81,12 +89,18 @@ type AppSidebarProps = { // --------------------------------------------------------------------------- function useCreateForm( - onCreate: (input: { name: string; description?: string }) => Promise, + onCreate: (input: { + name: string; + description?: string; + visibility: ChannelVisibility; + }) => Promise, entityLabel: string, ) { const [isOpen, setIsOpen] = React.useState(false); const [draftName, setDraftName] = React.useState(""); const [draftDescription, setDraftDescription] = React.useState(""); + const [draftVisibility, setDraftVisibility] = + React.useState("open"); const [errorMessage, setErrorMessage] = React.useState(); const inputRef = React.useRef(null); @@ -105,6 +119,7 @@ function useCreateForm( setErrorMessage(undefined); setDraftName(""); setDraftDescription(""); + setDraftVisibility("open"); setIsOpen(false); } @@ -118,6 +133,11 @@ function useCreateForm( setDraftDescription(value); } + function changeVisibility(value: ChannelVisibility) { + setErrorMessage(undefined); + setDraftVisibility(value); + } + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); @@ -133,10 +153,12 @@ function useCreateForm( await onCreate({ name, description: description || undefined, + visibility: draftVisibility, }); setDraftName(""); setDraftDescription(""); + setDraftVisibility("open"); setIsOpen(false); } catch (error) { setErrorMessage( @@ -151,12 +173,14 @@ function useCreateForm( isOpen, draftName, draftDescription, + draftVisibility, errorMessage, inputRef, toggle, cancel, changeName, changeDescription, + changeVisibility, handleSubmit, }; } @@ -212,6 +236,43 @@ function SectionHeaderActions({ ); } +// --------------------------------------------------------------------------- +// PrivateCheckbox — checkbox toggle for channel visibility +// --------------------------------------------------------------------------- + +function PrivateCheckbox({ + disabled, + isPrivate, + onChange, + testId, +}: { + disabled: boolean; + isPrivate: boolean; + onChange: (isPrivate: boolean) => void; + testId: string; +}) { + const id = React.useId(); + + return ( +
+ onChange(checked === true)} + /> + +
+ ); +} + // --------------------------------------------------------------------------- // ChannelGroupSection — unified Channels / Forums section // --------------------------------------------------------------------------- @@ -224,6 +285,7 @@ function ChannelGroupSection({ createFormTestId, createNameTestId, createDescriptionTestId, + createVisibilityTestId, groupClassName, isActiveChannel, isCreating, @@ -245,6 +307,7 @@ function ChannelGroupSection({ createFormTestId: string; createNameTestId: string; createDescriptionTestId: string; + createVisibilityTestId: string; groupClassName?: string; isActiveChannel: boolean; isCreating: boolean; @@ -302,6 +365,14 @@ function ChannelGroupSection({ placeholder={descriptionPlaceholder} value={form.draftDescription} /> + + form.changeVisibility(isPrivate ? "private" : "open") + } + testId={createVisibilityTestId} + />