SmartInboxHotkeys.tsx2.6 KBView on GitHub /**
* Hotkeys specific to the Smart Inbox view.
*
* `e` → markDone (Superhuman-style)
* Optimistically removes thread, pushes to undo stack, and shows an immediate toast.
* cmd+z and the toast Undo button both call undoLastAction → executeUndo → restores.
*/
import { triggerThreadExitAnimation } from '@/modules/threads/threadList/threadItem/components/thread';
import { useOptimisticActions } from '@/modules/threads/rendering/use-optimistic-actions';
import { useCedarStore } from '@/modules/store';
import { useHotkeys } from 'react-hotkeys-hook';
import { useCallback, useEffect, useRef } from 'react';
import { toast } from 'sonner';
export function SmartInboxHotkeys() {
const bulkSelected = useCedarStore((state) => state.bulkSelected);
const selectedThreadId = useCedarStore((state) => state.selectedThreadId);
const hoveredEmailId = useRef<string | null>(null);
useEffect(() => {
const handleEmailHover = (event: CustomEvent<{ id: string | null }>) => {
hoveredEmailId.current = event.detail.id;
};
window.addEventListener('emailHover', handleEmailHover as EventListener);
return () => {
window.removeEventListener('emailHover', handleEmailHover as EventListener);
};
}, []);
const shouldUseHover = bulkSelected.length === 0;
const getTargetIds = useCallback((): string[] => {
if (shouldUseHover && hoveredEmailId.current) return [hoveredEmailId.current];
if (bulkSelected.length > 0) return [...bulkSelected];
if (selectedThreadId) return [selectedThreadId];
return [];
}, [shouldUseHover, bulkSelected, selectedThreadId]);
const { optimisticMarkDone, undoLastAction } = useOptimisticActions();
const handleMarkDone = useCallback(async () => {
const targetIds = getTargetIds();
if (!targetIds.length) {
toast.info('No email selected');
return;
}
let undoReady = false;
const undoToast = toast(
targetIds.length === 1 ? 'Marked as done' : `${targetIds.length} threads marked as done`,
{
duration: 5000,
action: {
label: 'Undo',
onClick: () => {
if (!undoReady) return;
void undoLastAction();
toast.dismiss(undoToast);
},
},
},
);
// Animate out, then optimistically remove + push undo + fire server
await triggerThreadExitAnimation(targetIds, 'archive');
void optimisticMarkDone(targetIds);
undoReady = true;
}, [getTargetIds, optimisticMarkDone, undoLastAction]);
useHotkeys(
'e',
() => { void handleMarkDone(); },
{ scopes: ['mail-list'], preventDefault: true },
[handleMarkDone],
);
return null;
}