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