diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 638eed2ad..4076bc1e1 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -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"; @@ -388,6 +389,8 @@ export function AppSidebar({ React.useState(null); const [deleteSectionTarget, setDeleteSectionTarget] = React.useState(null); + const { requestLeaveChannel, dialog: leaveChannelDialog } = + useLeaveChannelDialog(); const sectionIds = React.useMemo( () => channelSections.map((s) => s.id), @@ -682,6 +685,7 @@ export function AppSidebar({ starredChannelIds={starredChannelIds} onStarChannel={onStarChannel} onUnstarChannel={onUnstarChannel} + onLeaveChannel={requestLeaveChannel} /> ) : null} ))} @@ -981,6 +987,7 @@ export function AppSidebar({ setDeleteSectionTarget(null); }} /> + {leaveChannelDialog} ); diff --git a/desktop/src/features/sidebar/ui/ChannelSectionDialogs.tsx b/desktop/src/features/sidebar/ui/ChannelSectionDialogs.tsx index c25f14ddf..f94867302 100644 --- a/desktop/src/features/sidebar/ui/ChannelSectionDialogs.tsx +++ b/desktop/src/features/sidebar/ui/ChannelSectionDialogs.tsx @@ -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) @@ -208,3 +210,70 @@ export function DeleteSectionAlertDialog({ ); } + +// --------------------------------------------------------------------------- +// LeaveChannelAlertDialog +// --------------------------------------------------------------------------- + +export type LeaveChannelAlertDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + channelName: string; + onConfirm: () => void; +}; + +export function LeaveChannelAlertDialog({ + open, + onOpenChange, + channelName, + onConfirm, +}: LeaveChannelAlertDialogProps) { + return ( + + + + Leave channel + + {`Leave "${channelName}"? You'll stop receiving its messages and can rejoin later.`} + + + + Cancel + + Leave + + + + + ); +} + +// --------------------------------------------------------------------------- +// useLeaveChannelDialog — owns leave-channel state, mutation, and dialog +// --------------------------------------------------------------------------- + +export function useLeaveChannelDialog() { + const [target, setTarget] = React.useState(null); + const leaveChannel = useLeaveChannelMutation(target?.id ?? null); + + const dialog = ( + { + if (!open) setTarget(null); + }} + channelName={target?.name ?? ""} + onConfirm={() => { + if (target) { + leaveChannel.mutate(); + } + setTarget(null); + }} + /> + ); + + return { requestLeaveChannel: setTarget, dialog }; +} diff --git a/desktop/src/features/sidebar/ui/CustomChannelSection.tsx b/desktop/src/features/sidebar/ui/CustomChannelSection.tsx index f4a0add50..7abb40a25 100644 --- a/desktop/src/features/sidebar/ui/CustomChannelSection.tsx +++ b/desktop/src/features/sidebar/ui/CustomChannelSection.tsx @@ -8,7 +8,10 @@ import { CheckCircle2, ChevronDown, CircleDot, + Clipboard, + Copy, GripVertical, + LogOut, Pencil, Plus, Star, @@ -16,6 +19,8 @@ import { Trash2, } from "lucide-react"; +import { toast } from "sonner"; + import { ContextMenu, ContextMenuContent, @@ -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, @@ -133,6 +149,7 @@ export function ChannelContextMenuItems({ onAssignChannel, onUnassignChannel, onCreateSectionForChannel, + onLeaveChannel, }: { channel: Channel; hasUnread: boolean; @@ -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 @@ -219,6 +237,35 @@ export function ChannelContextMenuItems({ /> ) : null} + + + copyToClipboard(channel.name, "Channel name copied to clipboard") + } + > + + Copy channel name + + + copyToClipboard(channel.id, "Channel ID copied to clipboard") + } + > + + Copy channel ID + + {onLeaveChannel ? ( + <> + + onLeaveChannel(channel)} + > + + Leave channel + + + ) : null} ); } @@ -325,6 +372,7 @@ export function ChannelGroupSection({ starredChannelIds, onStarChannel, onUnstarChannel, + onLeaveChannel, }: { browseAriaLabel?: string; createAriaLabel: string; @@ -360,6 +408,7 @@ export function ChannelGroupSection({ starredChannelIds?: ReadonlySet; onStarChannel?: (channelId: string) => void; onUnstarChannel?: (channelId: string) => void; + onLeaveChannel?: (channel: Channel) => void; }) { const contentId = `sidebar-${listTestId}`; @@ -414,6 +463,7 @@ export function ChannelGroupSection({ onAssignChannel={onAssignChannel} onUnassignChannel={onUnassignChannel} onCreateSectionForChannel={onCreateSectionForChannel} + onLeaveChannel={onLeaveChannel} /> @@ -422,7 +472,9 @@ export function ChannelGroupSection({ ) : null; const sectionContent = ( - +