channel-availability.ts2.6 KBView on GitHub import type { InboxChannel, InboxChannelFilter } from '@/modules/inbox/types';
/**
* Which channel badges the ACTIVE TAB can actually answer.
*
* The channel badge and the inbox tab are orthogonal filters over one feed, and they compose
* everywhere except at one intersection, where the product is empty. A tab whose rule is a Gmail
* query — GitHub, Marketing, Agent Drafts, and the Important / Other stubs — resolves through
* `participatingChannels` to email alone: a Slack source cannot evaluate `label:"…"`, so it is
* gated out rather than allowed to contribute its whole unfiltered stream. Correct for `all`.
* But the client then narrowed that same set by an explicit badge, and
* `['email'].filter(c => c === 'slack')` is `[]` — no channel participated, the merge answered
* `ready: true` with no rows, and the list stated "No slack messages." over a mirror holding 38.
*
* An empty intersection is not a result worth rendering, so it is not offered: the badge is
* disabled while the tab cannot answer it. See apps/mail/docs/bug-inbox-channel-filter-empty.md.
*/
/**
* Can this tab answer this badge?
*
* `participating` is the RESOLVED set from `inbox.getFeedScope` — already through the shared
* `participatingChannels` rule, so this asks the server's answer rather than re-deriving it.
* `undefined` means the scope has not resolved yet, which is "not known", not "not allowed":
* disabling on a pending scope would flicker the options out and back on every tab change.
*
* `all` is always answerable — on a mail-only tab it means email, which is what that tab is.
* `email` needs no special case: `participatingChannels` returns it unconditionally.
*/
export function isChannelFilterAvailable(
filter: InboxChannelFilter,
participating: readonly InboxChannel[] | undefined,
): boolean {
if (!participating) return true;
if (filter === 'all') return true;
return participating.includes(filter);
}
/**
* The badge to actually apply, given what this tab can answer.
*
* Disabling the menu item closes the front door; this closes the others. A chat badge reaches a
* mail-only tab without the menu too — `?channel=slack` in a shared link, or the remembered pick
* restored from localStorage, followed by a click onto a Gmail-query tab. Both land in the same
* dead end, so an unanswerable badge resolves to `all`, which every tab can answer.
*/
export function resolveChannelFilter(
filter: InboxChannelFilter,
participating: readonly InboxChannel[] | undefined,
): InboxChannelFilter {
return isChannelFilterAvailable(filter, participating) ? filter : 'all';
}