Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions desktop/src/features/sidebar/ui/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
CreateSectionDialog,
DeleteSectionAlertDialog,
RenameSectionDialog,
useLeaveChannelDialog,
} from "@/features/sidebar/ui/ChannelSectionDialogs";
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
import { SidebarSection } from "@/features/sidebar/ui/SidebarSection";
Expand Down Expand Up @@ -388,6 +389,8 @@ export function AppSidebar({
React.useState<ChannelSection | null>(null);
const [deleteSectionTarget, setDeleteSectionTarget] =
React.useState<ChannelSection | null>(null);
const { requestLeaveChannel, dialog: leaveChannelDialog } =
useLeaveChannelDialog();

const sectionIds = React.useMemo(
() => channelSections.map((s) => s.id),
Expand Down Expand Up @@ -682,6 +685,7 @@ export function AppSidebar({
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
) : null}
<SidebarDndContext
Expand Down Expand Up @@ -735,6 +739,7 @@ export function AppSidebar({
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
))}
<ChannelGroupSection
Expand Down Expand Up @@ -768,6 +773,7 @@ export function AppSidebar({
starredChannelIds={starredChannelIds}
onStarChannel={onStarChannel}
onUnstarChannel={onUnstarChannel}
onLeaveChannel={requestLeaveChannel}
/>
</SidebarDndContext>
<FeatureGate feature="forum">
Expand Down Expand Up @@ -981,6 +987,7 @@ export function AppSidebar({
setDeleteSectionTarget(null);
}}
/>
{leaveChannelDialog}
<SidebarRail />
</Sidebar>
);
Expand Down
69 changes: 69 additions & 0 deletions desktop/src/features/sidebar/ui/ChannelSectionDialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
DialogTitle,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import type { Channel } from "@/shared/api/types";
import { useLeaveChannelMutation } from "@/features/channels/hooks";

// ---------------------------------------------------------------------------
// SectionNameDialog (internal)
Expand Down Expand Up @@ -208,3 +210,70 @@ export function DeleteSectionAlertDialog({
</AlertDialog>
);
}

// ---------------------------------------------------------------------------
// LeaveChannelAlertDialog
// ---------------------------------------------------------------------------

export type LeaveChannelAlertDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
channelName: string;
onConfirm: () => void;
};

export function LeaveChannelAlertDialog({
open,
onOpenChange,
channelName,
onConfirm,
}: LeaveChannelAlertDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Leave channel</AlertDialogTitle>
<AlertDialogDescription>
{`Leave "${channelName}"? You'll stop receiving its messages and can rejoin later.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={onConfirm}
>
Leave
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

// ---------------------------------------------------------------------------
// useLeaveChannelDialog — owns leave-channel state, mutation, and dialog
// ---------------------------------------------------------------------------

export function useLeaveChannelDialog() {
const [target, setTarget] = React.useState<Channel | null>(null);
const leaveChannel = useLeaveChannelMutation(target?.id ?? null);

const dialog = (
<LeaveChannelAlertDialog
open={target !== null}
onOpenChange={(open) => {
if (!open) setTarget(null);
}}
channelName={target?.name ?? ""}
onConfirm={() => {
if (target) {
leaveChannel.mutate();
}
setTarget(null);
}}
/>
);

return { requestLeaveChannel: setTarget, dialog };
}
62 changes: 60 additions & 2 deletions desktop/src/features/sidebar/ui/CustomChannelSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import {
CheckCircle2,
ChevronDown,
CircleDot,
Clipboard,
Copy,
GripVertical,
LogOut,
Pencil,
Plus,
Star,
StarOff,
Trash2,
} from "lucide-react";

import { toast } from "sonner";

import {
ContextMenu,
ContextMenuContent,
Expand Down Expand Up @@ -117,6 +122,17 @@ function MoveToSectionSubmenu({
// ChannelContextMenuItems — shared context menu items for channel rows
// ---------------------------------------------------------------------------

function copyToClipboard(text: string, successMessage: string) {
void navigator.clipboard
.writeText(text)
.then(() => {
toast.success(successMessage);
})
.catch(() => {
toast.error("Failed to copy to clipboard");
});
}

export function ChannelContextMenuItems({
channel,
hasUnread,
Expand All @@ -133,6 +149,7 @@ export function ChannelContextMenuItems({
onAssignChannel,
onUnassignChannel,
onCreateSectionForChannel,
onLeaveChannel,
}: {
channel: Channel;
hasUnread: boolean;
Expand All @@ -152,6 +169,7 @@ export function ChannelContextMenuItems({
onAssignChannel?: (channelId: string, sectionId: string) => void;
onUnassignChannel?: (channelId: string) => void;
onCreateSectionForChannel?: (channelId: string) => void;
onLeaveChannel?: (channel: Channel) => void;
}) {
const showStar = Boolean(onStarChannel && onUnstarChannel);
const showReadToggle = hasUnread
Expand Down Expand Up @@ -219,6 +237,35 @@ export function ChannelContextMenuItems({
/>
</>
) : null}
<ContextMenuSeparator />
<ContextMenuItem
onClick={() =>
copyToClipboard(channel.name, "Channel name copied to clipboard")
}
>
<Copy className="h-4 w-4" />
Copy channel name
</ContextMenuItem>
<ContextMenuItem
onClick={() =>
copyToClipboard(channel.id, "Channel ID copied to clipboard")
}
>
<Clipboard className="h-4 w-4" />
Copy channel ID
</ContextMenuItem>
{onLeaveChannel ? (
<>
<ContextMenuSeparator />
<ContextMenuItem
className="text-destructive focus:text-destructive"
onClick={() => onLeaveChannel(channel)}
>
<LogOut className="h-4 w-4" />
Leave channel
</ContextMenuItem>
</>
) : null}
</>
);
}
Expand Down Expand Up @@ -325,6 +372,7 @@ export function ChannelGroupSection({
starredChannelIds,
onStarChannel,
onUnstarChannel,
onLeaveChannel,
}: {
browseAriaLabel?: string;
createAriaLabel: string;
Expand Down Expand Up @@ -360,6 +408,7 @@ export function ChannelGroupSection({
starredChannelIds?: ReadonlySet<string>;
onStarChannel?: (channelId: string) => void;
onUnstarChannel?: (channelId: string) => void;
onLeaveChannel?: (channel: Channel) => void;
}) {
const contentId = `sidebar-${listTestId}`;

Expand Down Expand Up @@ -414,6 +463,7 @@ export function ChannelGroupSection({
onAssignChannel={onAssignChannel}
onUnassignChannel={onUnassignChannel}
onCreateSectionForChannel={onCreateSectionForChannel}
onLeaveChannel={onLeaveChannel}
/>
</ContextMenuContent>
</ContextMenu>
Expand All @@ -422,7 +472,9 @@ export function ChannelGroupSection({
) : null;

const sectionContent = (
<SidebarGroup className={cn("group/sidebar-section", groupClassName)}>
<SidebarGroup
className={cn("group/sidebar-section select-none", groupClassName)}
>
<div className="relative">
<SidebarGroupLabel asChild>
<button
Expand Down Expand Up @@ -500,6 +552,7 @@ export function CustomChannelSection({
starredChannelIds,
onStarChannel,
onUnstarChannel,
onLeaveChannel,
}: {
section: ChannelSection;
channels: Channel[];
Expand Down Expand Up @@ -534,6 +587,7 @@ export function CustomChannelSection({
starredChannelIds?: ReadonlySet<string>;
onStarChannel?: (channelId: string) => void;
onUnstarChannel?: (channelId: string) => void;
onLeaveChannel?: (channel: Channel) => void;
}) {
const contentId = `sidebar-section-${section.id}`;

Expand All @@ -542,7 +596,10 @@ export function CustomChannelSection({
{({ dragHandleProps, isDragging }) => (
<DroppableSectionBody sectionId={section.id}>
<SidebarGroup
className={cn("group/sidebar-section", isDragging && "opacity-30")}
className={cn(
"group/sidebar-section select-none",
isDragging && "opacity-30",
)}
>
<ContextMenu>
<ContextMenuTrigger asChild>
Expand Down Expand Up @@ -684,6 +741,7 @@ export function CustomChannelSection({
onCreateSectionForChannel={
onCreateSectionForChannel
}
onLeaveChannel={onLeaveChannel}
/>
</ContextMenuContent>
</ContextMenu>
Expand Down
13 changes: 4 additions & 9 deletions desktop/src/features/sidebar/ui/SidebarSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export function SidebarSection({
const canToggle = Boolean(onToggleCollapsed);

return (
<SidebarGroup className="group/sidebar-section">
<SidebarGroup className="group/sidebar-section select-none">
<div className="relative">
<SidebarGroupLabel asChild={canToggle}>
{canToggle ? (
Expand Down Expand Up @@ -405,12 +405,9 @@ export function SidebarSection({
</SidebarMenuItem>
);

const hasContextAction =
(unreadChannelIds.has(channel.id) && onMarkChannelRead) ||
(!unreadChannelIds.has(channel.id) && onMarkChannelUnread) ||
(onMuteChannel && onUnmuteChannel);

return hasContextAction ? (
// The shared menu always renders copy actions, so every row
// gets a context menu regardless of read/mute availability.
return (
<ContextMenu key={channel.id}>
<ContextMenuTrigger asChild>{menuItem}</ContextMenuTrigger>
<ContextMenuContent>
Expand All @@ -425,8 +422,6 @@ export function SidebarSection({
/>
</ContextMenuContent>
</ContextMenu>
) : (
menuItem
);
})}
</SidebarMenu>
Expand Down
Loading