feat(sidebar): replace Browse Channels button with search icons (#118)

This commit is contained in:
Wes
2026-03-19 15:04:02 -07:00
committed by GitHub
parent 3b18846d30
commit 5066914465
3 changed files with 299 additions and 362 deletions
+14 -4
View File
@@ -67,7 +67,12 @@ export function AppShell() {
const [isChannelManagementOpen, setIsChannelManagementOpen] =
React.useState(false);
const [isSearchOpen, setIsSearchOpen] = React.useState(false);
const [isBrowseChannelsOpen, setIsBrowseChannelsOpen] = React.useState(false);
const [browseDialogType, setBrowseDialogType] = React.useState<
"stream" | "forum" | null
>(null);
const handleBrowseDialogOpenChange = React.useCallback((open: boolean) => {
setBrowseDialogType(open ? "stream" : null);
}, []);
const [searchAnchor, setSearchAnchor] = React.useState<SearchHit | null>(
null,
);
@@ -547,7 +552,11 @@ export function AppShell() {
openChannelView(createdForum.id);
}}
onOpenBrowseChannels={() => {
setIsBrowseChannelsOpen(true);
setBrowseDialogType("stream");
void refetchChannels();
}}
onOpenBrowseForums={() => {
setBrowseDialogType("forum");
void refetchChannels();
}}
onOpenSearch={() => {
@@ -707,10 +716,11 @@ export function AppShell() {
<ChannelBrowserDialog
channels={channels}
channelTypeFilter={browseDialogType ?? "stream"}
onJoinChannel={handleBrowseChannelJoin}
onOpenChange={setIsBrowseChannelsOpen}
onOpenChange={handleBrowseDialogOpenChange}
onSelectChannel={handleOpenChannel}
open={isBrowseChannelsOpen}
open={browseDialogType !== null}
/>
<SearchDialog
@@ -72,6 +72,7 @@ function BrowseState({
type ChannelBrowserDialogProps = {
channels: Channel[];
channelTypeFilter?: "stream" | "forum";
open: boolean;
onOpenChange: (open: boolean) => void;
onJoinChannel: (channelId: string) => Promise<void>;
@@ -80,6 +81,7 @@ type ChannelBrowserDialogProps = {
export function ChannelBrowserDialog({
channels,
channelTypeFilter,
open,
onOpenChange,
onJoinChannel,
@@ -93,12 +95,23 @@ export function ChannelBrowserDialog({
const inputRef = React.useRef<HTMLInputElement>(null);
const deferredQuery = React.useDeferredValue(query.trim().toLowerCase());
const isForumMode = channelTypeFilter === "forum";
const browseTitle = isForumMode ? "Browse Forums" : "Browse Channels";
const browseDescription = isForumMode
? "Discover and join open forums."
: "Discover and join open channels.";
const searchPlaceholder = isForumMode
? "Search forums by name or description"
: "Search channels by name or description";
const entityLabel = isForumMode ? "forum" : "channel";
const browsableChannels = React.useMemo(() => {
const filtered = channels.filter(
(channel) =>
channel.channelType !== "dm" &&
channel.visibility === "open" &&
!channel.archivedAt,
!channel.archivedAt &&
(channelTypeFilter ? channel.channelType === channelTypeFilter : true),
);
if (deferredQuery.length === 0) {
@@ -110,7 +123,7 @@ export function ChannelBrowserDialog({
channel.name.toLowerCase().includes(deferredQuery) ||
channel.description.toLowerCase().includes(deferredQuery),
);
}, [channels, deferredQuery]);
}, [channels, channelTypeFilter, deferredQuery]);
const notJoined = React.useMemo(
() => browsableChannels.filter((channel) => !channel.isMember),
@@ -129,6 +142,10 @@ export function ChannelBrowserDialog({
);
React.useEffect(() => {
if (isForumMode) {
return;
}
function handleKeyDown(event: KeyboardEvent) {
if (
event.key.toLowerCase() !== BROWSE_CHANNELS_SHORTCUT_KEY ||
@@ -147,7 +164,7 @@ export function ChannelBrowserDialog({
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onOpenChange]);
}, [isForumMode, onOpenChange]);
React.useEffect(() => {
if (!open) {
@@ -204,18 +221,18 @@ export function ChannelBrowserDialog({
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent
className="gap-0 overflow-hidden p-0"
data-testid="channel-browser-dialog"
data-testid={
isForumMode ? "forum-browser-dialog" : "channel-browser-dialog"
}
>
<DialogHeader className="border-b border-border/80 px-6 py-5">
<DialogTitle className="flex items-center gap-3">
<span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm">
<Compass className="h-4 w-4" />
</span>
Browse Channels
{browseTitle}
</DialogTitle>
<DialogDescription>
Discover and join open channels.
</DialogDescription>
<DialogDescription>{browseDescription}</DialogDescription>
<div className="mt-4 flex items-center gap-3 rounded-2xl border border-input bg-card px-3 py-3 shadow-sm">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
@@ -249,7 +266,7 @@ export function ChannelBrowserDialog({
handleSelect(selectedItem);
}
}}
placeholder="Search channels by name or description"
placeholder={searchPlaceholder}
ref={inputRef}
value={query}
/>
@@ -265,13 +282,13 @@ export function ChannelBrowserDialog({
<BrowseState
description="Try a different name or keyword."
icon={Search}
title="No channels match your search"
title={`No ${entityLabel}s match your search`}
/>
) : (
<BrowseState
description="All open channels are available in the sidebar. Create a new channel to get started."
description={`All open ${entityLabel}s are available in the sidebar. Create a new ${entityLabel} to get started.`}
icon={Compass}
title="No channels to browse"
title={`No ${entityLabel}s to browse`}
/>
)
) : (
@@ -280,7 +297,7 @@ export function ChannelBrowserDialog({
<>
<div className="mb-3 flex items-center justify-between px-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
<span>
{notJoined.length} channel
{notJoined.length} {entityLabel}
{notJoined.length !== 1 ? "s" : ""} to join
</span>
<span>Enter to join</span>
@@ -333,7 +350,7 @@ export function ChannelBrowserDialog({
</div>
<div className="border-t border-border/80 bg-card/50 px-6 py-3 text-xs text-muted-foreground">
Showing open channels. Private channels require an invite.
Showing open {entityLabel}s. Private {entityLabel}s require an invite.
</div>
</DialogContent>
</Dialog>
+254 -344
View File
@@ -1,5 +1,5 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import { Bot, Compass, Home, PenSquare, Plus, Search } from "lucide-react";
import { Bot, Home, PenSquare, Plus, Search } from "lucide-react";
import * as React from "react";
import { useManagedAgentsQuery } from "@/features/agents/hooks";
@@ -32,6 +32,17 @@ import {
SidebarSeparator,
} from "@/shared/ui/sidebar";
// ---------------------------------------------------------------------------
// Shared styles
// ---------------------------------------------------------------------------
const SECTION_ICON_BUTTON_CLASS =
"flex h-5 w-5 items-center justify-center rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type AppSidebarProps = {
channels: Channel[];
currentPubkey?: string;
@@ -56,6 +67,7 @@ type AppSidebarProps = {
description?: string;
}) => Promise<void>;
onOpenBrowseChannels: () => void;
onOpenBrowseForums: () => void;
onOpenSearch: () => void;
onOpenDm: (input: { pubkeys: string[] }) => Promise<void>;
onSelectAgents: () => void;
@@ -64,99 +76,243 @@ type AppSidebarProps = {
onSelectSettings: () => void;
};
function StreamsSection({
items,
// ---------------------------------------------------------------------------
// useCreateForm — shared state + handler for channel/forum creation
// ---------------------------------------------------------------------------
function useCreateForm(
onCreate: (input: { name: string; description?: string }) => Promise<void>,
entityLabel: string,
) {
const [isOpen, setIsOpen] = React.useState(false);
const [draftName, setDraftName] = React.useState("");
const [draftDescription, setDraftDescription] = React.useState("");
const [errorMessage, setErrorMessage] = React.useState<string | undefined>();
const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
}
}, [isOpen]);
function toggle() {
setErrorMessage(undefined);
setIsOpen((current) => !current);
}
function cancel() {
setErrorMessage(undefined);
setDraftName("");
setDraftDescription("");
setIsOpen(false);
}
function changeName(value: string) {
setErrorMessage(undefined);
setDraftName(value);
}
function changeDescription(value: string) {
setErrorMessage(undefined);
setDraftDescription(value);
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const name = draftName.trim();
const description = draftDescription.trim();
if (!name) {
return;
}
setErrorMessage(undefined);
try {
await onCreate({
name,
description: description || undefined,
});
setDraftName("");
setDraftDescription("");
setIsOpen(false);
} catch (error) {
setErrorMessage(
error instanceof Error
? error.message
: `Failed to create ${entityLabel}.`,
);
}
}
return {
isOpen,
draftName,
draftDescription,
errorMessage,
inputRef,
toggle,
cancel,
changeName,
changeDescription,
handleSubmit,
};
}
// ---------------------------------------------------------------------------
// SectionHeaderActions — search + create icon buttons for section headers
// ---------------------------------------------------------------------------
function SectionHeaderActions({
browseAriaLabel,
browseTestId,
createAriaLabel,
closeAriaLabel,
isCreateOpen,
isCreatingChannel,
draftName,
draftDescription,
createInputRef,
createErrorMessage,
onBrowse,
onToggleCreate,
onChangeName,
onChangeDescription,
onCreateChannel,
onCancelCreate,
onSelectChannel,
isActiveChannel,
selectedChannelId,
unreadChannelIds,
}: {
items: Channel[];
browseAriaLabel: string;
browseTestId?: string;
createAriaLabel: string;
closeAriaLabel: string;
isCreateOpen: boolean;
isCreatingChannel: boolean;
draftName: string;
draftDescription: string;
createInputRef: React.RefObject<HTMLInputElement | null>;
createErrorMessage?: string;
onBrowse: () => void;
onToggleCreate: () => void;
onChangeName: (value: string) => void;
onChangeDescription: (value: string) => void;
onCreateChannel: (event: React.FormEvent<HTMLFormElement>) => void;
onCancelCreate: () => void;
onSelectChannel: (channelId: string) => void;
isActiveChannel: boolean;
selectedChannelId: string | null;
unreadChannelIds: Set<string>;
}) {
return (
<SidebarGroup className="pt-1">
<SidebarGroupLabel>Channels</SidebarGroupLabel>
<SidebarGroupAction
<div className="absolute right-1 top-3 flex items-center gap-0.5">
<button
aria-label={browseAriaLabel}
className={SECTION_ICON_BUTTON_CLASS}
data-testid={browseTestId}
onClick={onBrowse}
type="button"
>
<Search className="h-3.5 w-3.5" />
</button>
<button
aria-expanded={isCreateOpen}
aria-label={isCreateOpen ? "Close new stream form" : "Create a stream"}
className="top-3 text-sidebar-foreground/50 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"
aria-label={isCreateOpen ? closeAriaLabel : createAriaLabel}
className={SECTION_ICON_BUTTON_CLASS}
onClick={onToggleCreate}
type="button"
>
<Plus
className={
isCreateOpen
? "rotate-45 transition-transform"
: "transition-transform"
? "h-4 w-4 rotate-45 transition-transform"
: "h-4 w-4 transition-transform"
}
/>
</SidebarGroupAction>
</button>
</div>
);
}
// ---------------------------------------------------------------------------
// ChannelGroupSection — unified Channels / Forums section
// ---------------------------------------------------------------------------
function ChannelGroupSection({
browseAriaLabel,
browseTestId,
closeAriaLabel,
createAriaLabel,
createFormTestId,
createNameTestId,
createDescriptionTestId,
groupClassName,
isActiveChannel,
isCreating,
items,
listTestId,
namePlaceholder,
descriptionPlaceholder,
onBrowse,
onSelectChannel,
selectedChannelId,
title,
unreadChannelIds,
form,
}: {
browseAriaLabel: string;
browseTestId?: string;
closeAriaLabel: string;
createAriaLabel: string;
createFormTestId: string;
createNameTestId: string;
createDescriptionTestId: string;
groupClassName?: string;
isActiveChannel: boolean;
isCreating: boolean;
items: Channel[];
listTestId: string;
namePlaceholder: string;
descriptionPlaceholder: string;
onBrowse: () => void;
onSelectChannel: (channelId: string) => void;
selectedChannelId: string | null;
title: string;
unreadChannelIds: Set<string>;
form: ReturnType<typeof useCreateForm>;
}) {
return (
<SidebarGroup className={groupClassName}>
<SidebarGroupLabel>{title}</SidebarGroupLabel>
<SectionHeaderActions
browseAriaLabel={browseAriaLabel}
browseTestId={browseTestId}
closeAriaLabel={closeAriaLabel}
createAriaLabel={createAriaLabel}
isCreateOpen={form.isOpen}
onBrowse={onBrowse}
onToggleCreate={form.toggle}
/>
<SidebarGroupContent>
{isCreateOpen ? (
{form.isOpen ? (
<form
className="mb-2 space-y-2 rounded-lg border border-sidebar-border/70 bg-sidebar-accent/60 p-2"
data-testid="create-stream-form"
onSubmit={onCreateChannel}
data-testid={createFormTestId}
onSubmit={(event) => {
void form.handleSubmit(event);
}}
>
<Input
autoComplete="off"
autoCapitalize="none"
autoCorrect="off"
className="h-8 bg-background/80"
data-testid="create-stream-name"
disabled={isCreatingChannel}
onChange={(event) => onChangeName(event.target.value)}
placeholder="release-notes"
ref={createInputRef}
data-testid={createNameTestId}
disabled={isCreating}
onChange={(event) => form.changeName(event.target.value)}
placeholder={namePlaceholder}
ref={form.inputRef}
spellCheck={false}
value={draftName}
value={form.draftName}
/>
<Input
autoComplete="off"
className="h-8 bg-background/80"
data-testid="create-stream-description"
disabled={isCreatingChannel}
onChange={(event) => onChangeDescription(event.target.value)}
placeholder="What this stream is for"
value={draftDescription}
data-testid={createDescriptionTestId}
disabled={isCreating}
onChange={(event) => form.changeDescription(event.target.value)}
placeholder={descriptionPlaceholder}
value={form.draftDescription}
/>
<div className="flex items-center gap-2">
<Button
disabled={isCreatingChannel || draftName.trim().length === 0}
disabled={isCreating || form.draftName.trim().length === 0}
size="sm"
type="submit"
>
{isCreatingChannel ? "Creating..." : "Create"}
{isCreating ? "Creating..." : "Create"}
</Button>
<Button
disabled={isCreatingChannel}
onClick={onCancelCreate}
disabled={isCreating}
onClick={form.cancel}
size="sm"
type="button"
variant="ghost"
@@ -164,14 +320,14 @@ function StreamsSection({
Cancel
</Button>
</div>
{createErrorMessage ? (
<p className="text-sm text-destructive">{createErrorMessage}</p>
{form.errorMessage ? (
<p className="text-sm text-destructive">{form.errorMessage}</p>
) : null}
</form>
) : null}
{items.length > 0 ? (
<SidebarMenu data-testid="stream-list">
<SidebarMenu data-testid={listTestId}>
{items.map((channel) => (
<SidebarMenuItem key={channel.id}>
<ChannelMenuButton
@@ -189,130 +345,9 @@ function StreamsSection({
);
}
function ForumsSection({
items,
isCreateOpen,
isCreatingForum,
draftName,
draftDescription,
createInputRef,
createErrorMessage,
onToggleCreate,
onChangeName,
onChangeDescription,
onCreateForum,
onCancelCreate,
onSelectChannel,
isActiveChannel,
selectedChannelId,
unreadChannelIds,
}: {
items: Channel[];
isCreateOpen: boolean;
isCreatingForum: boolean;
draftName: string;
draftDescription: string;
createInputRef: React.RefObject<HTMLInputElement | null>;
createErrorMessage?: string;
onToggleCreate: () => void;
onChangeName: (value: string) => void;
onChangeDescription: (value: string) => void;
onCreateForum: (event: React.FormEvent<HTMLFormElement>) => void;
onCancelCreate: () => void;
onSelectChannel: (channelId: string) => void;
isActiveChannel: boolean;
selectedChannelId: string | null;
unreadChannelIds: Set<string>;
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Forums</SidebarGroupLabel>
<SidebarGroupAction
aria-expanded={isCreateOpen}
aria-label={isCreateOpen ? "Close new forum form" : "New forum"}
className="top-3 text-sidebar-foreground/50 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"
onClick={onToggleCreate}
type="button"
>
<Plus
className={
isCreateOpen
? "rotate-45 transition-transform"
: "transition-transform"
}
/>
</SidebarGroupAction>
<SidebarGroupContent>
{isCreateOpen ? (
<form
className="mb-2 space-y-2 rounded-lg border border-sidebar-border/70 bg-sidebar-accent/60 p-2"
data-testid="create-forum-form"
onSubmit={onCreateForum}
>
<Input
autoComplete="off"
autoCapitalize="none"
autoCorrect="off"
className="h-8 bg-background/80"
data-testid="create-forum-name"
disabled={isCreatingForum}
onChange={(event) => onChangeName(event.target.value)}
placeholder="design-discussions"
ref={createInputRef}
spellCheck={false}
value={draftName}
/>
<Input
autoComplete="off"
className="h-8 bg-background/80"
data-testid="create-forum-description"
disabled={isCreatingForum}
onChange={(event) => onChangeDescription(event.target.value)}
placeholder="What this forum is for"
value={draftDescription}
/>
<div className="flex items-center gap-2">
<Button
disabled={isCreatingForum || draftName.trim().length === 0}
size="sm"
type="submit"
>
{isCreatingForum ? "Creating..." : "Create"}
</Button>
<Button
disabled={isCreatingForum}
onClick={onCancelCreate}
size="sm"
type="button"
variant="ghost"
>
Cancel
</Button>
</div>
{createErrorMessage ? (
<p className="text-sm text-destructive">{createErrorMessage}</p>
) : null}
</form>
) : null}
{items.length > 0 ? (
<SidebarMenu data-testid="forum-list">
{items.map((channel) => (
<SidebarMenuItem key={channel.id}>
<ChannelMenuButton
channel={channel}
hasUnread={unreadChannelIds.has(channel.id)}
isActive={isActiveChannel && selectedChannelId === channel.id}
onSelectChannel={onSelectChannel}
/>
</SidebarMenuItem>
))}
</SidebarMenu>
) : null}
</SidebarGroupContent>
</SidebarGroup>
);
}
// ---------------------------------------------------------------------------
// AppSidebar
// ---------------------------------------------------------------------------
export function AppSidebar({
channels,
@@ -332,6 +367,7 @@ export function AppSidebar({
onCreateChannel,
onCreateForum,
onOpenBrowseChannels,
onOpenBrowseForums,
onOpenSearch,
onOpenDm,
onSelectAgents,
@@ -340,21 +376,11 @@ export function AppSidebar({
onSelectSettings,
}: AppSidebarProps) {
const skeletonRows = ["first", "second", "third", "fourth", "fifth", "sixth"];
const [isCreateOpen, setIsCreateOpen] = React.useState(false);
const [isForumCreateOpen, setIsForumCreateOpen] = React.useState(false);
const [isNewDmOpen, setIsNewDmOpen] = React.useState(false);
const [draftName, setDraftName] = React.useState("");
const [draftDescription, setDraftDescription] = React.useState("");
const [forumDraftName, setForumDraftName] = React.useState("");
const [forumDraftDescription, setForumDraftDescription] = React.useState("");
const [createErrorMessage, setCreateErrorMessage] = React.useState<
string | undefined
>();
const [forumCreateErrorMessage, setForumCreateErrorMessage] = React.useState<
string | undefined
>();
const createInputRef = React.useRef<HTMLInputElement>(null);
const forumCreateInputRef = React.useRef<HTMLInputElement>(null);
const streamForm = useCreateForm(onCreateChannel, "stream");
const forumForm = useCreateForm(onCreateForum, "forum");
const streamChannels = channels.filter(
(channel) => channel.channelType === "stream",
);
@@ -380,76 +406,6 @@ export function AppSidebar({
fallbackDisplayName?.trim() ||
"Current identity";
React.useEffect(() => {
if (!isCreateOpen) {
return;
}
createInputRef.current?.focus();
}, [isCreateOpen]);
async function handleCreateChannel(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const name = draftName.trim();
const description = draftDescription.trim();
if (!name) {
return;
}
setCreateErrorMessage(undefined);
try {
await onCreateChannel({
name,
description: description || undefined,
});
setDraftName("");
setDraftDescription("");
setIsCreateOpen(false);
} catch (error) {
setCreateErrorMessage(
error instanceof Error ? error.message : "Failed to create stream.",
);
}
}
async function handleCreateForum(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const name = forumDraftName.trim();
const description = forumDraftDescription.trim();
if (!name) {
return;
}
setForumCreateErrorMessage(undefined);
try {
await onCreateForum({
name,
description: description || undefined,
});
setForumDraftName("");
setForumDraftDescription("");
setIsForumCreateOpen(false);
} catch (error) {
setForumCreateErrorMessage(
error instanceof Error ? error.message : "Failed to create forum.",
);
}
}
React.useEffect(() => {
if (!isForumCreateOpen) {
return;
}
forumCreateInputRef.current?.focus();
}, [isForumCreateOpen]);
function handleDragPointerDown(e: React.PointerEvent) {
if (e.button !== 0) return;
const target = e.target as HTMLElement;
@@ -549,86 +505,47 @@ export function AppSidebar({
{!isLoading ? (
<>
<StreamsSection
createErrorMessage={createErrorMessage}
createInputRef={createInputRef}
draftDescription={draftDescription}
draftName={draftName}
isCreateOpen={isCreateOpen}
isCreatingChannel={isCreatingChannel}
<ChannelGroupSection
browseAriaLabel="Browse channels"
browseTestId="browse-channels"
closeAriaLabel="Close new stream form"
createAriaLabel="Create a stream"
createFormTestId="create-stream-form"
createNameTestId="create-stream-name"
createDescriptionTestId="create-stream-description"
descriptionPlaceholder="What this stream is for"
form={streamForm}
groupClassName="pt-1"
isActiveChannel={selectedView === "channel"}
isCreating={isCreatingChannel}
items={streamChannels}
onCancelCreate={() => {
setCreateErrorMessage(undefined);
setDraftName("");
setDraftDescription("");
setIsCreateOpen(false);
}}
onChangeDescription={(value) => {
setCreateErrorMessage(undefined);
setDraftDescription(value);
}}
onChangeName={(value) => {
setCreateErrorMessage(undefined);
setDraftName(value);
}}
onCreateChannel={(event) => {
void handleCreateChannel(event);
}}
listTestId="stream-list"
namePlaceholder="release-notes"
onBrowse={onOpenBrowseChannels}
onSelectChannel={onSelectChannel}
onToggleCreate={() => {
setCreateErrorMessage(undefined);
setIsCreateOpen((current) => !current);
}}
selectedChannelId={selectedChannelId}
title="Channels"
unreadChannelIds={unreadChannelIds}
/>
<SidebarMenu className="px-2">
<SidebarMenuItem>
<SidebarMenuButton
className="text-sidebar-foreground/60 hover:text-sidebar-foreground"
data-testid="browse-channels"
onClick={onOpenBrowseChannels}
tooltip="Browse channels"
type="button"
>
<Compass className="h-4 w-4" />
<span>Browse channels</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<ForumsSection
createErrorMessage={forumCreateErrorMessage}
createInputRef={forumCreateInputRef}
draftDescription={forumDraftDescription}
draftName={forumDraftName}
<ChannelGroupSection
browseAriaLabel="Browse forums"
browseTestId="browse-forums"
closeAriaLabel="Close new forum form"
createAriaLabel="New forum"
createFormTestId="create-forum-form"
createNameTestId="create-forum-name"
createDescriptionTestId="create-forum-description"
descriptionPlaceholder="What this forum is for"
form={forumForm}
isActiveChannel={selectedView === "channel"}
isCreateOpen={isForumCreateOpen}
isCreatingForum={isCreatingForum}
isCreating={isCreatingForum}
items={forumChannels}
onCancelCreate={() => {
setForumCreateErrorMessage(undefined);
setForumDraftName("");
setForumDraftDescription("");
setIsForumCreateOpen(false);
}}
onChangeDescription={(value) => {
setForumCreateErrorMessage(undefined);
setForumDraftDescription(value);
}}
onChangeName={(value) => {
setForumCreateErrorMessage(undefined);
setForumDraftName(value);
}}
onCreateForum={(event) => {
void handleCreateForum(event);
}}
listTestId="forum-list"
namePlaceholder="design-discussions"
onBrowse={onOpenBrowseForums}
onSelectChannel={onSelectChannel}
onToggleCreate={() => {
setForumCreateErrorMessage(undefined);
setIsForumCreateOpen((current) => !current);
}}
selectedChannelId={selectedChannelId}
title="Forums"
unreadChannelIds={unreadChannelIds}
/>
<SidebarSection
@@ -647,7 +564,6 @@ export function AppSidebar({
</SidebarGroupAction>
}
dmParticipantsByChannelId={dmParticipantsByChannelId}
emptyState="No direct messages yet."
isActiveChannel={selectedView === "channel"}
items={directMessages}
channelLabels={dmChannelLabels}
@@ -661,12 +577,6 @@ export function AppSidebar({
</>
) : null}
{!isLoading && channels.length === 0 ? (
<div className="px-3 py-2 text-sm text-sidebar-foreground/70">
No channels available yet.
</div>
) : null}
{errorMessage ? (
<div className="px-3 py-2 text-sm text-destructive">
{errorMessage}