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>
  );
}