use-channel-hotkey-actions.ts5.3 KBView on GitHub /**
* The CHAT half of every mail-list keyboard action.
*
* `MailListHotkeys` resolves one target set and then acts on it twice: Gmail ids go down the
* optimistic email path, and LinkedIn / WhatsApp / Slack rows come here. Before this existed
* the unibox had no chat half at all — the hover ref the key handlers consult only ever holds
* an email id, so `r` on a LinkedIn row reported "No emails to select", and anything reached
* through the bulk selection instead handed a `li:…` id straight to Gmail.
*
* ── What each key means for a chat ──
*
* A chat is not a mail thread, so three of the keys have to be TRANSLATED rather than ported.
* The translations, and why:
*
* - **Spam / trash / archive all collapse into Mark done.** A chat has no spam folder and no
* deletion — the messages live in Slack or LinkedIn, not here. Every "get it out of my
* inbox" key therefore means the one thing the inbox can do about it, and the server
* re-surfaces the row when the counterpart replies.
* - **Mark important stars.** `cedar_inbox_item_state` has exactly one flag for a chat, and
* this is it. `i` sets it (matching email's set-true semantics) where `x` toggles.
* - **Reply, reply-all and Enter are one action: open the chat.** There is a single composer
* at the bottom of a chat, addressed to the one counterpart, and `ChannelThreadView` focuses
* it on open — so "reply" IS "open it", and reply-all has nobody extra to add.
*
* Forward is the one key with no honest translation: it needs a destination the feed cannot
* supply. It says so rather than doing something adjacent.
*/
import { useCallback, useMemo } from 'react';
import { toast } from 'sonner';
import { useInboxItemActions } from '@/modules/inbox/hooks/use-inbox-item-actions';
import { openChannelChat } from '@/modules/inbox/hooks/use-open-channel-item';
import { channelLabel } from '@/modules/inbox/lib/hotkey-targets';
import type { InboxItem } from '@/modules/inbox/types';
export interface ChannelHotkeyActions {
/** e / ! / w / # — out of the inbox. No exit animation: the caller plays one over every target. */
markDone: (items: InboxItem[]) => void;
/** ⇧E — the inverse of Mark done. */
restore: (items: InboxItem[]) => void;
/** ⇧I — read, at the source and as an explicit override. */
markRead: (items: InboxItem[]) => void;
/** u — flip read/unread, on the same "any unread → read them all" rule email uses. */
toggleRead: (items: InboxItem[]) => void;
/** i — the one flag a chat row has. */
star: (items: InboxItem[]) => void;
/** x — toggle it, on the same "any starred → unstar them all" rule email uses. */
toggleStar: (items: InboxItem[]) => void;
/** h — the remind dialog's pick, which for a chat is a snooze. */
snooze: (items: InboxItem[], until: Date) => void;
/** r / ⇧R / Enter — open the chat, which lands the cursor in its composer. */
openChat: (item: InboxItem) => void;
/** f — say what cannot be done, rather than doing nothing. */
refuse: (items: InboxItem[], action: string) => void;
}
export function useChannelHotkeyActions(): ChannelHotkeyActions {
const {
markManyDone,
restore: restoreItem,
markChannelRead,
setUnread,
setStarred,
snooze: snoozeItem,
} = useInboxItemActions();
const markDone = useCallback(
(items: InboxItem[]) => {
if (items.length > 0) markManyDone(items);
},
[markManyDone],
);
const restore = useCallback((items: InboxItem[]) => items.forEach(restoreItem), [restoreItem]);
const markRead = useCallback(
(items: InboxItem[]) => items.forEach(markChannelRead),
[markChannelRead],
);
const toggleRead = useCallback(
(items: InboxItem[]) => {
// One decision for the whole set, taken from the set — the same rule `toggleReadUnread`
// applies to threads, so a mixed selection does not half-flip.
const anyUnread = items.some((item) => item.unread);
for (const item of items) {
if (anyUnread) markChannelRead(item);
else setUnread(item, true);
}
},
[markChannelRead, setUnread],
);
const star = useCallback(
(items: InboxItem[]) => items.forEach((item) => setStarred(item, true)),
[setStarred],
);
const toggleStar = useCallback(
(items: InboxItem[]) => {
const anyStarred = items.some((item) => item.starred);
for (const item of items) setStarred(item, !anyStarred);
},
[setStarred],
);
const snooze = useCallback(
(items: InboxItem[], until: Date) => items.forEach((item) => void snoozeItem(item, until)),
[snoozeItem],
);
const openChat = useCallback((item: InboxItem) => openChannelChat(item), []);
const refuse = useCallback((items: InboxItem[], action: string) => {
const first = items[0];
if (!first) return;
const where = items.every((item) => item.channel === first.channel)
? `${channelLabel(first)} chats`
: 'chats';
toast.info(`${action} isn't available for ${where}.`);
}, []);
// One stable object: `MailListHotkeys` puts it in the dependency list of every handler, and
// those handlers are what `useShortcuts` re-registers when they change.
return useMemo(
() => ({ markDone, restore, markRead, toggleRead, star, toggleStar, snooze, openChat, refuse }),
[markDone, restore, markRead, toggleRead, star, toggleStar, snooze, openChat, refuse],
);
}