ChannelSelector.tsx3.1 KBView on GitHub import { Check, ChevronDown } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
CHANNEL_FILTERS,
type InboxChannel,
type InboxChannelFilter,
} from '@/modules/inbox/types';
import { ChannelSelectorIcon } from '@/modules/inbox/components/channel-icons';
import { isChannelFilterAvailable } from '@/modules/inbox/lib/channel-availability';
import { cn } from '@/lib/utils';
interface ChannelSelectorProps {
value: InboxChannelFilter;
onChange: (value: InboxChannelFilter) => void;
/**
* The channels the ACTIVE TAB participates in, from `inbox.getFeedScope`. A badge outside this
* set is disabled: on a Gmail-query tab it intersects the tab's rule to nothing, and the feed
* rendered that empty set as "No slack messages." Undefined while the scope resolves, which
* offers everything rather than flickering the options out and back.
*/
participating?: readonly InboxChannel[];
className?: string;
}
/**
* The channel filter that leads the inbox tab row — styled exactly like a folder
* tab (`InboxTab`): a borderless `text-base font-medium` label + chevron. Reads
* "All Channels ⌄" on the unibox, or the pinned channel (e.g. "Email ⌄").
* See apps/mail/docs/omni-channel-inbox.md Phase 9.
*/
export function ChannelSelector({
value,
onChange,
participating,
className,
}: ChannelSelectorProps) {
const current = CHANNEL_FILTERS.find((c) => c.value === value) ?? CHANNEL_FILTERS[0];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
'relative flex cursor-pointer select-none items-center gap-1.5 self-stretch whitespace-nowrap px-2 text-base font-medium leading-none text-foreground outline-none transition-colors hover:text-foreground/80',
className,
)}
>
<ChannelSelectorIcon channel={value} className="h-4 w-4" />
<span>{current.label}</span>
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-44">
{CHANNEL_FILTERS.map((c) => {
const available = isChannelFilterAvailable(c.value, participating);
return (
<DropdownMenuItem
key=[redacted]
disabled={!available}
// Guarded as well as disabled: `disabled` is the primitive's contract, and this
// is the one the empty feed depended on being wrong.
onClick={() => available && onChange(c.value)}
className={cn(
'flex items-center gap-2',
available ? 'cursor-pointer' : 'cursor-not-allowed',
)}
>
<ChannelSelectorIcon channel={c.value} className="h-4 w-4" />
<span className="flex-1">{c.label}</span>
{c.value === value && <Check className="h-3.5 w-3.5 opacity-70" />}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}