hotkey-targets.ts3.4 KBView on GitHub /**
* Splitting one keyboard target set into its email half and its chat half.
*
* Every mail-list hotkey resolves the SAME target set — the hovered row, else the bulk
* selection, else the row `j`/`k` left selected — and in the unified inbox that set can hold
* rows from four channels at once. What it may NOT do is hand a channel row to the Gmail
* driver: a LinkedIn row's selection id is `li:<chatId>`, and `optimisticToggleStar` on it
* issues a Gmail modify for a thread that does not exist. That is what `x`, `u`, `i`, `h` and
* `#` all did on a chat row, silently.
*
* The rule is two lines, and the ORDER matters. An id carrying a channel prefix is a chat,
* whether or not the feed can still resolve it: selection and hover state outlive a feed
* replacement, so a lookup miss is an ordinary event, and "it did not resolve, so it must be
* email" is how a `li:` id reaches a Gmail modify for a thread that does not exist. Only then:
* an id the loaded feed knows as a non-email row is a chat. Everything else is a Gmail thread
* id — safely, because a Gmail thread id is hex and can never hold a colon.
*
* An id that is a chat but does not resolve is dropped from BOTH lists. There is no row to
* act on, and no driver that could act on it.
*
* Outside the unibox the feed is empty and no id carries a prefix, so every id falls through
* to email and the mail list behaves exactly as it always has.
*/
import type { InboxItem } from '@/modules/inbox/types';
export interface HotkeyTargets {
/** Every targeted selection id, in list order — what an exit animation plays over. */
ids: string[];
/** The Gmail thread ids among them. */
emailIds: string[];
/** The LinkedIn / WhatsApp / Slack rows among them. */
channelItems: InboxItem[];
}
export const NO_TARGETS: HotkeyTargets = { ids: [], emailIds: [], channelItems: [] };
/**
* The channel prefixes `services/inbox/feed-core.ts` mints (`li:<chatId>`, `wa:<chatId>`,
* `slack:<ws>:<channel>`). An email row's selection id is a BARE Gmail thread id, which is
* hex and therefore cannot collide with any of these.
*/
const CHANNEL_ID_PREFIXES = ['li:', 'wa:', 'slack:'];
export function isChannelSelectionId(id: string): boolean {
return CHANNEL_ID_PREFIXES.some((prefix) => id.startsWith(prefix));
}
/**
* @param ids selection ids (email → bare Gmail threadId, chat → `InboxItem.id`)
* @param lookup the loaded feed's by-selection-id resolver (`store.getInboxItem`)
*/
export function splitHotkeyTargets(
ids: string[],
lookup: (id: string) => InboxItem | undefined,
): HotkeyTargets {
const emailIds: string[] = [];
const channelItems: InboxItem[] = [];
for (const id of ids) {
const item = lookup(id);
if (item) {
if (item.channel === 'email') emailIds.push(id);
else channelItems.push(item);
continue;
}
// Unresolved. Its PREFIX still says what it is, and that is the half that matters:
// a chat id must never fall through to the Gmail driver just because the feed moved on.
if (!isChannelSelectionId(id)) emailIds.push(id);
}
return { ids, emailIds, channelItems };
}
/** Human name for a chat channel, for the one action that has to say it cannot. */
export function channelLabel(item: InboxItem): string {
switch (item.channel) {
case 'linkedin':
return 'LinkedIn';
case 'whatsapp':
return 'WhatsApp';
case 'slack':
return 'Slack';
default:
return item.channel;
}
}