Skip to content

Comments

Staging#696

Merged
nizzyabi merged 41 commits intomainfrom
staging
Apr 17, 2025
Merged

Staging#696
nizzyabi merged 41 commits intomainfrom
staging

Conversation

@nizzyabi
Copy link
Collaborator

@nizzyabi nizzyabi commented Apr 17, 2025

READ CAREFULLY THEN REMOVE

Remove bullet points that are not relevant.

PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI.

  • Pull requests that do not follow these guidelines will be closed without review or comment.
  • If you use AI to write your PR description your pr will be close without review or comment.
  • If you are unsure about anything, feel free to ask for clarification.

Description

Please provide a clear description of your changes.


Type of Change

Please delete options that are not relevant.

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature with breaking changes)
  • 📝 Documentation update
  • 🎨 UI/UX improvement
  • 🔒 Security enhancement
  • ⚡ Performance improvement

Areas Affected

Please check all that apply:

  • Email Integration (Gmail, IMAP, etc.)
  • User Interface/Experience
  • Authentication/Authorization
  • Data Storage/Management
  • API Endpoints
  • Documentation
  • Testing Infrastructure
  • Development Workflow
  • Deployment/Infrastructure

Testing Done

Describe the tests you've done:

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • Cross-browser testing (if UI changes)
  • Mobile responsiveness verified (if UI changes)

Security Considerations

For changes involving data or authentication:

  • No sensitive data is exposed
  • Authentication checks are in place
  • Input validation is implemented
  • Rate limiting is considered (if applicable)

Checklist

  • I have read the CONTRIBUTING document
  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in complex areas
  • I have updated the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix/feature works
  • All tests pass locally
  • Any dependent changes are merged and published

Additional Notes

Add any other context about the pull request here.

Screenshots/Recordings

Add screenshots or recordings here if applicable.


By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.

Summary by CodeRabbit

  • New Features

    • Introduced customizable and persistent keyboard shortcuts for mail actions, with support for editing and saving shortcuts per user.
    • Added interactive hotkey recording and management in the settings interface.
    • Enabled hotkey scope management for different mail app sections (global, compose, mail list, thread view).
    • Added Farsi (Persian) language support and localization placeholders.
  • Improvements

    • Switched to a new, extensible keyboard shortcut configuration with descriptions and context-aware scopes.
    • Enhanced keyboard navigation with quick action mode in mail lists.
    • Improved accessibility and user experience for shortcut editing and display.
  • Bug Fixes

    • Fixed navigation sidebar to allow access to shortcut settings.
  • Chores

    • Added Dockerfile and updated docker-compose for containerized deployment.
    • Added database migration and schema for storing user-specific hotkeys.
    • Updated dependencies to include react-hotkeys-hook.

Muhammad-Owais-Warsi and others added 30 commits April 5, 2025 13:05
- refactor hotkeys
- add hotkey recorder
- added route for hotkeys
- added table for hotkeys
- indexdb for hotkeys (synced with db)
@vercel
Copy link

vercel bot commented Apr 17, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
0 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 17, 2025 6:36am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 17, 2025

Walkthrough

This update introduces a comprehensive overhaul of keyboard shortcut management in the mail application, transitioning from a custom hotkey hook to the standardized react-hotkeys-hook library. It adds a persistent, user-specific hotkey configuration system backed by IndexedDB and a new server API, allowing users to customize, save, and sync their shortcuts. Several new React components and hooks are introduced to manage hotkey scopes, display, and editing, including context providers and modal recorders. The update also adds Farsi (Persian) localization support, updates Docker deployment files, and introduces a new database table and migration for storing user hotkeys.

Changes

Files/Groups Change Summary
Dockerfile, docker-compose.yaml Added Dockerfile for Bun-based container; updated compose file to add app service and dynamic env vars.
apps/mail/package.json Added react-hotkeys-hook dependency.
apps/mail/app/(routes)/layout.tsx, apps/mail/app/(routes)/mail/layout.tsx Wrapped layouts with HotkeyProviderWrapper for hotkey context/scoping.
apps/mail/components/providers/hotkey-provider-wrapper.tsx New provider component for hotkey context and scope-based hotkey components.
apps/mail/components/mail/keyboard-shortcuts.tsx, apps/mail/hooks/use-hot-key.ts Removed custom hotkey hook and old keyboard shortcut component.
apps/mail/lib/hotkeys/global-hotkeys.tsx, .../compose-hotkeys.tsx, .../mail-list-hotkeys.tsx, .../thread-display-hotkeys.tsx Added new hotkey scope handler components for global, compose, mail list, and thread display.
apps/mail/lib/hotkeys/use-hotkey-utils.ts New utilities and hooks for formatting, displaying, and registering hotkeys.
apps/mail/lib/hotkeys/hotkeys-db.ts New IndexedDB-backed hotkey persistence and sync logic.
apps/mail/app/(routes)/settings/shortcuts/page.tsx, .../hotkey-recorder.tsx Refactored shortcuts page for persistent, editable shortcuts; added interactive hotkey recorder modal.
apps/mail/app/api/v1/hotkeys/route.ts New API route for getting, setting, and updating user hotkeys.
apps/mail/config/shortcuts.ts Refactored shortcuts config to typed objects with scope, description, and type.
apps/mail/components/draft/drafts-list.tsx, .../mail/mail-list.tsx, .../mail/mail.tsx, .../mail/reply-composer.tsx, .../create/create-email.tsx Migrated to new hotkey context/hooks, added scope management, removed old hotkey logic, improved formatting.
apps/mail/config/navigation.ts Enabled shortcuts menu item by commenting out disabled: true.
apps/mail/i18n/config.ts, i18n.json, apps/mail/locales/fa.json, apps/mail/locales/en.json Added Farsi support, updated language config, and added new localization strings.
packages/db/migrations/0022_round_violations.sql, .../meta/0022_snapshot.json, .../meta/_journal.json, packages/db/src/schema.ts Added new user_hotkeys table and migration for storing user-specific shortcuts.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI (Shortcuts Page)
  participant HotkeyRecorder
  participant hotkeysDB (IndexedDB)
  participant API Server
  participant Database

  User->>UI (Shortcuts Page): Opens Shortcuts Settings
  UI (Shortcuts Page)->>hotkeysDB: getAllHotkeys()
  hotkeysDB->>API Server: (on first load) GET /api/v1/hotkeys
  API Server->>Database: Query user_hotkeys
  Database-->>API Server: Return shortcuts
  API Server-->>hotkeysDB: Return shortcuts
  hotkeysDB-->>UI (Shortcuts Page): Return merged shortcuts

  User->>HotkeyRecorder: Clicks to record new shortcut
  HotkeyRecorder->>User: Shows modal, listens for keys
  User->>HotkeyRecorder: Presses new key combo
  HotkeyRecorder->>UI (Shortcuts Page): onHotkeyRecorded(newKeys)
  UI (Shortcuts Page)->>hotkeysDB: saveHotkey(updatedShortcut)
  hotkeysDB->>API Server: PUT /api/v1/hotkeys (sync)
  API Server->>Database: Upsert shortcut
  Database-->>API Server: Success
  API Server-->>hotkeysDB: Success
Loading

Possibly related PRs

  • Mail-0/Zero#445: Adds Farsi language entry to language configuration, directly related to the new Farsi localization support.
  • Mail-0/Zero#591: Adds Dockerfile and updates docker-compose for app containerization, matching the infrastructure changes here.
  • Mail-0/Zero#649: Introduces HotkeyProviderWrapper and integrates it into layout components, overlapping with the new hotkey context management.

Suggested reviewers

  • MrgSub

Poem

In the warren of code, a rabbit did hop,
Bringing hotkeys to all, with a click and a pop!
Now shortcuts are saved, in the cloud and on disk,
With Farsi to greet you—how clever, how brisk!
Compose, reply, and navigate with flair,
Dockerized and ready, this release is rare.
🐇✨ Press a key, and magic fills the air!

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (18)
packages/db/migrations/0022_round_violations.sql (1)

7-8: Consider cascading on user deletion

If a user row is deleted, keeping an “orphaned” hotkey row is rarely useful.

-ON DELETE no action
+ON DELETE cascade

Please confirm that cascading aligns with your data‑retention policy.

apps/mail/lib/hotkeys/global-hotkeys.tsx (2)

11-19: Memoise handlers & shortcut list to avoid re‑registration every render

Because handlers and globalShortcuts are re‑created on each render, the underlying
react-hotkeys-hook will register / unregister listeners every time ‑ an unnecessary cost
and a potential source of subtle race conditions.

-import { useShortcuts } from './use-hotkey-utils';
+import { useShortcuts } from './use-hotkey-utils';
+import { useMemo } from 'react';
@@
-  const handlers = {
+  const handlers = useMemo(() => ({
     goToDrafts: () => router.push('/mail/draft'),
     inbox: () => router.push('/mail/inbox'),
     sentMail: () => router.push('/mail/sent'),
     search: () => {
       console.log('search');
     },
     newEmail: () => router.push('/mail/compose'),
-  };
+  }), [router]);
@@
-  const globalShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
+  const globalShortcuts = useMemo(
+    () => keyboardShortcuts.filter((s) => s.scope === scope),
+    [],
+  );

15-17: search handler is a stub

console.log('search') is a placeholder. If search navigation is planned, wire it up
(e.g. router.push('/mail/search')) or leave a TODO comment so it is not forgotten.

apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (1)

17-20: Same re‑registration issue as in GlobalHotkeys

Wrap threadDisplayShortcuts & handlers in useMemo to avoid needless
listener churn.

docker-compose.yaml (1)

36-36: Remove trailing whitespace.

There's a trailing whitespace on this line that should be removed.

-      
+
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 36-36: trailing spaces

(trailing-spaces)

apps/mail/lib/hotkeys/mail-list-hotkeys.tsx (2)

52-63: Strengthen the hoveredEmailId check

The conditional check for hoveredEmailId.current would accept empty strings as valid IDs, which could lead to unexpected behavior.

-    if (hoveredEmailId.current) {
+    if (hoveredEmailId.current && hoveredEmailId.current.trim().length > 0) {

35-50: Consider using useMemo for allIds calculation

For large thread lists, recalculating allIds on every render could impact performance.

+  const allIds = useMemo(() => 
+    items.map((item) => item.threadId ?? item.id),
+    [items]
+  );

  const selectAll = useCallback(() => {
    if (mail.bulkSelected.length > 0) {
      setMail((prev) => ({
        ...prev,
        bulkSelected: [],
      }));
    } else if (items.length > 0) {
-      const allIds = items.map((item) => item.threadId ?? item.id);
      setMail((prev) => ({
        ...prev,
        bulkSelected: allIds,
      }));
apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx (3)

26-37: Improve key normalization for special keys

The current key normalization only handles space and single characters, but doesn't normalize other special keys consistently.

-      const key = e.key === ' ' ? 'Space' : e.key;
-
-      const formattedKey = key.length === 1 ? key.toUpperCase() : key;
+      // Normalize key names
+      let formattedKey;
+      switch (e.key) {
+        case ' ':
+          formattedKey = 'Space';
+          break;
+        case 'Control':
+          formattedKey = 'Ctrl';
+          break;
+        case 'Meta':
+          formattedKey = 'Cmd';
+          break;
+        case 'Escape':
+          formattedKey = 'Esc';
+          break;
+        default:
+          formattedKey = e.key.length === 1 ? e.key.toUpperCase() : e.key;
+      }

70-72: Avoid type casting MessageKey

The translation key is being force-cast to MessageKey, which could lead to type safety issues.

-            {t('pages.settings.shortcuts.actions.recordHotkey' as MessageKey)}
+            {t('pages.settings.shortcuts.actions.recordHotkey')}

Similarly for the other instances on line 77-78.


23-57: Consider using a ref for recordedKeys to avoid unnecessary effect runs

The current implementation includes recordedKeys in the dependencies array while also updating it inside the effect, potentially causing unnecessary re-runs.

export function HotkeyRecorder({
  isOpen,
  onClose,
  onHotkeyRecorded,
  currentKeys,
}: HotkeyRecorderProps) {
  const t = useTranslations();
  const [recordedKeys, setRecordedKeys] = useState<string[]>([]);
+  const recordedKeysRef = useRef<string[]>([]);
  const [isRecording, setIsRecording] = useState(false);

  useEffect(() => {
    if (!isOpen) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      e.preventDefault();
      if (!isRecording) return;

      const key = e.key === ' ' ? 'Space' : e.key;

      const formattedKey = key.length === 1 ? key.toUpperCase() : key;

-      if (!recordedKeys.includes(formattedKey)) {
-        setRecordedKeys((prev) => [...prev, formattedKey]);
+      if (!recordedKeysRef.current.includes(formattedKey)) {
+        recordedKeysRef.current = [...recordedKeysRef.current, formattedKey];
+        setRecordedKeys([...recordedKeysRef.current]);
      }
    };

    const handleKeyUp = (e: KeyboardEvent) => {
      e.preventDefault();
      if (isRecording) {
        setIsRecording(false);
-        if (recordedKeys.length > 0) {
-          onHotkeyRecorded(recordedKeys);
+        if (recordedKeysRef.current.length > 0) {
+          onHotkeyRecorded(recordedKeysRef.current);
          onClose();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
-  }, [isOpen, isRecording, recordedKeys, onHotkeyRecorded, onClose]);
+  }, [isOpen, isRecording, onHotkeyRecorded, onClose]);

  useEffect(() => {
    if (isOpen) {
      setRecordedKeys([]);
+      recordedKeysRef.current = [];
      setIsRecording(true);
    }
  }, [isOpen]);
apps/mail/components/mail/mail.tsx (1)

254-270: Remove console.log statements

There are several console.log statements that should be removed before production deployment.

  useEffect(() => {
    if (threadId) {
-      console.log('Enabling thread-display scope, disabling mail-list');
      enableScope('thread-display');
      disableScope('mail-list');
    } else {
-      console.log('Enabling mail-list scope, disabling thread-display');
      enableScope('mail-list');
      disableScope('thread-display');
    }

    return () => {
-      console.log('Cleaning up mail/thread scopes');
      disableScope('thread-display');
      disableScope('mail-list');
    };
  }, [threadId, enableScope, disableScope]);
apps/mail/app/api/v1/hotkeys/route.ts (1)

1-91: Extract common authentication and database operations into utility functions

The three route handlers share common patterns that could be extracted into reusable utility functions to reduce duplication and improve maintainability.

Consider creating utility functions for:

  1. Authenticating the request and returning a standardized error response if needed
  2. The upsert operation with common error handling
  3. A common error response formatter

Example:

// Auth utility
async function authenticateRequest(headers: Headers) {
  const session = await auth.api.getSession({ headers });
  if (!session) {
    return { authenticated: false, error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) };
  }
  return { authenticated: true, session };
}

// DB upsert utility
async function upsertUserHotkeys(userId: string, shortcuts: Shortcut[]) {
  const now = new Date();
  try {
    await db
      .insert(userHotkeys)
      .values({
        userId,
        shortcuts,
        createdAt: now,
        updatedAt: now,
      })
      .onConflictDoUpdate({
        target: [userHotkeys.userId],
        set: {
          shortcuts,
          updatedAt: now,
        },
      });
    return { success: true };
  } catch (error) {
    console.error('Error upserting user hotkeys:', error);
    return { success: false, error };
  }
}
apps/mail/components/mail/reply-composer.tsx (1)

924-937: Remove debug console logs from production code

While adding hotkey scope management is good, the console logging statements should be removed from production code.

useEffect(() => {
  if (composerIsOpen) {
-    console.log('Enabling compose scope (ReplyCompose)');
    enableScope('compose');
  } else {
-    console.log('Disabling compose scope (ReplyCompose)');
    disableScope('compose');
  }

  return () => {
-    console.log('Cleaning up compose scope (ReplyCompose)');
    disableScope('compose');
  };
}, [composerIsOpen, enableScope, disableScope]);
apps/mail/components/create/create-email.tsx (1)

464-472: Remove debug console logs from production code

Similar to the reply-composer component, the console logging statements should be removed from production code.

useEffect(() => {
-  console.log('Enabling compose scope (CreateEmail)');
  enableScope('compose');

  return () => {
-    console.log('Disabling compose scope (CreateEmail)');
    disableScope('compose');
  };
}, [enableScope, disableScope]);
apps/mail/components/mail/mail-list.tsx (1)

495-501: Clear dangling TODO & debugging artefacts

This block still contains a // TODO: debug comment left over from development. It signals unfinished work and will quickly turn into dead code.

-      // TODO: debug
       const allIds = items.map((item) => item.id);

Either finish the debugging task or remove the comment before merging.

apps/mail/lib/hotkeys/hotkeys-db.ts (1)

126-143: Make bulk‑save API public

saveAllHotkeys is private, yet the UI needs an efficient “reset to
defaults” operation (see comment in page.tsx). Consider exposing it or
adding a dedicated resetToDefaults() to avoid O(N²) writes.

apps/mail/hooks/use-mail-navigation.ts (1)

222-285: Consider refactoring repeated key handling

The implementation for handling repeated key presses is complex with nested timeouts and requestAnimationFrame. While it works correctly, it might be worth considering a more declarative approach or extracting this logic to a separate hook for better maintainability.

A throttling or debouncing hook might provide a cleaner implementation:

- useEffect(() => {
-   let isProcessingKey = false;
-   const MOVE_DELAY = 100; // Decreased from 150ms to 100ms for faster movement
-
-   const handleKeyDown = async (event: KeyboardEvent) => {
-     if (isQuickActionMode) return;
-
-     // For non-repeat events (initial press), let the useHotkeys handlers manage it
-     if (!event.repeat) return;
-
-     if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
-       event.preventDefault(); // Prevent default browser behavior
-
-       // If we're already processing a previous key event, don't stack them
-       if (isProcessingKey) return;
-
-       // Check if enough time has passed since the last movement
-       const now = Date.now();
-       if (now - lastMoveTime.current < MOVE_DELAY) return;
-
-       isProcessingKey = true;
-       lastMoveTime.current = now;
-
-       await new Promise<void>((resolve) => {
-         requestAnimationFrame(() => {
-           if (event.key === 'ArrowUp') {
-             setFocusedIndex((prev) => {
-               const newIndex = prev === null ? items.length - 1 : Math.max(0, prev - 1);
-               const threadElement = getThreadElement(newIndex);
-               if (threadElement && containerRef.current) {
-                 threadElement.scrollIntoView({ block: 'nearest', behavior: 'auto' });
-               }
-               return newIndex;
-             });
-           } else if (event.key === 'ArrowDown') {
-             setFocusedIndex((prev) => {
-               const newIndex = prev === null ? 0 : Math.min(items.length - 1, prev + 1);
-               const threadElement = getThreadElement(newIndex);
-               if (threadElement && containerRef.current) {
-                 threadElement.scrollIntoView({ block: 'nearest', behavior: 'auto' });
-               }
-               return newIndex;
-             });
-           }
-           isProcessingKey = false;
-           resolve();
-         });
-       });
-     }
-   };
-
-   window.addEventListener('keydown', handleKeyDown);
-
-   return () => {
-     window.removeEventListener('keydown', handleKeyDown);
-   };
+ // Reuse the same arrow handlers with throttling
+ const throttledArrowUp = useThrottledCallback(handleArrowUp, 100);
+ const throttledArrowDown = useThrottledCallback(handleArrowDown, 100);
+ 
+ // Only handle repeated key presses, initial press is handled by useHotkeys
+ useEffect(() => {
+   const handleKeyDown = (event: KeyboardEvent) => {
+     if (isQuickActionMode || !event.repeat) return;
+     
+     if (event.key === 'ArrowUp') {
+       event.preventDefault();
+       throttledArrowUp(event);
+     } else if (event.key === 'ArrowDown') {
+       event.preventDefault();
+       throttledArrowDown(event);
+     }
+   };
+   
+   window.addEventListener('keydown', handleKeyDown);
+   return () => window.removeEventListener('keydown', handleKeyDown);
}, [
  handleArrowUp,
  handleArrowDown,
  isQuickActionMode,
  items.length,
  getThreadElement,
  containerRef,
]);

You would need to implement or import a useThrottledCallback hook, but this approach would be more maintainable and reuse your existing handlers.

apps/mail/lib/hotkeys/use-hotkey-utils.ts (1)

85-131: useShortcut hook integrates database persistence with hotkey registration

The useShortcut hook effectively bridges user-customized shortcuts with the hotkey registration system. It handles loading/saving to the database and properly configures the hotkey with the right options.

The error handling could be improved beyond just console.error to provide better feedback to users when shortcut operations fail.

Consider adding more robust error handling:

  useEffect(() => {
-   hotkeysDB.saveHotkey(shortcut).catch(console.error);
+   hotkeysDB.saveHotkey(shortcut).catch((error) => {
+     console.error('Failed to save hotkey:', shortcut.action, error);
+     // Consider adding some user feedback or error reporting here
+   });

    hotkeysDB
      .getHotkey(shortcut.action)
      .then((saved) => {
        if (saved && saved.keys !== shortcut.keys) {
          setCurrentShortcut(saved);
        }
      })
-     .catch(console.error);
+     .catch((error) => {
+       console.error('Failed to retrieve hotkey:', shortcut.action, error);
+       // Consider adding some user feedback or fallback behavior
+     });
  }, [shortcut]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5e41c3 and c82605a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • Dockerfile (1 hunks)
  • apps/mail/app/(routes)/layout.tsx (1 hunks)
  • apps/mail/app/(routes)/mail/layout.tsx (1 hunks)
  • apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx (1 hunks)
  • apps/mail/app/(routes)/settings/shortcuts/page.tsx (2 hunks)
  • apps/mail/app/api/v1/hotkeys/route.ts (1 hunks)
  • apps/mail/components/create/create-email.tsx (22 hunks)
  • apps/mail/components/draft/drafts-list.tsx (8 hunks)
  • apps/mail/components/draft/drafts.tsx (1 hunks)
  • apps/mail/components/mail/keyboard-shortcuts.tsx (0 hunks)
  • apps/mail/components/mail/mail-list.tsx (15 hunks)
  • apps/mail/components/mail/mail.tsx (3 hunks)
  • apps/mail/components/mail/reply-composer.tsx (13 hunks)
  • apps/mail/components/providers/hotkey-provider-wrapper.tsx (1 hunks)
  • apps/mail/components/ui/ai-sidebar.tsx (1 hunks)
  • apps/mail/config/navigation.ts (1 hunks)
  • apps/mail/config/shortcuts.ts (1 hunks)
  • apps/mail/hooks/use-hot-key.ts (2 hunks)
  • apps/mail/hooks/use-mail-navigation.ts (6 hunks)
  • apps/mail/i18n/config.ts (1 hunks)
  • apps/mail/lib/hotkeys/compose-hotkeys.tsx (1 hunks)
  • apps/mail/lib/hotkeys/global-hotkeys.tsx (1 hunks)
  • apps/mail/lib/hotkeys/hotkeys-db.ts (1 hunks)
  • apps/mail/lib/hotkeys/mail-list-hotkeys.tsx (1 hunks)
  • apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (1 hunks)
  • apps/mail/lib/hotkeys/use-hotkey-utils.ts (1 hunks)
  • apps/mail/locales/en.json (2 hunks)
  • apps/mail/locales/fa.json (1 hunks)
  • apps/mail/package.json (1 hunks)
  • docker-compose.yaml (1 hunks)
  • i18n.json (1 hunks)
  • packages/db/migrations/0022_round_violations.sql (1 hunks)
  • packages/db/migrations/meta/0022_snapshot.json (1 hunks)
  • packages/db/migrations/meta/_journal.json (1 hunks)
  • packages/db/src/schema.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/mail/components/mail/keyboard-shortcuts.tsx
🧰 Additional context used
🧬 Code Graph Analysis (5)
apps/mail/lib/hotkeys/global-hotkeys.tsx (2)
apps/mail/config/shortcuts.ts (1)
  • keyboardShortcuts (12-34)
apps/mail/lib/hotkeys/use-hotkey-utils.ts (1)
  • useShortcuts (133-144)
apps/mail/components/providers/hotkey-provider-wrapper.tsx (4)
apps/mail/lib/hotkeys/global-hotkeys.tsx (1)
  • GlobalHotkeys (7-26)
apps/mail/lib/hotkeys/mail-list-hotkeys.tsx (1)
  • MailListHotkeys (13-91)
apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (1)
  • ThreadDisplayHotkeys (10-22)
apps/mail/lib/hotkeys/compose-hotkeys.tsx (1)
  • ComposeHotkeys (6-16)
apps/mail/components/mail/reply-composer.tsx (1)
apps/mail/types/index.ts (1)
  • Sender (32-35)
apps/mail/components/create/create-email.tsx (1)
apps/mail/components/mail/data.tsx (1)
  • contacts (219-300)
apps/mail/lib/hotkeys/use-hotkey-utils.ts (3)
apps/mail/config/shortcuts.ts (2)
  • Shortcut (3-10)
  • keyboardShortcuts (12-34)
apps/mail/lib/hotkeys/hotkeys-db.ts (1)
  • hotkeysDB (146-146)
apps/mail/lib/idb.ts (1)
  • keys (63-65)
🪛 YAMLlint (1.35.1)
docker-compose.yaml

[error] 36-36: trailing spaces

(trailing-spaces)

🪛 Biome (1.9.4)
apps/mail/app/(routes)/settings/shortcuts/page.tsx

[error] 75-75: Shouldn't redeclare 'Shortcut'. Consider to delete it or rename it.

'Shortcut' is defined here:

(lint/suspicious/noRedeclare)

🔇 Additional comments (63)
apps/mail/package.json (1)

95-95: Dependency addition looks good

The addition of react-hotkeys-hook aligns with the keyboard shortcut management overhaul mentioned in the PR summary. The version constraint ^5.0.1 allows for compatible minor updates which is appropriate.

i18n.json (2)

21-22: Farsi language support added correctly

Adding "fa" (Persian/Farsi) to the locale targets list follows the established pattern of language code entries. The formatting is consistent with other entries.


27-27: Array formatting change - no functional impact

The formatting change from multi-line to single-line for the include array is cosmetic only and doesn't affect functionality.

packages/db/src/schema.ts (1)

50-57: Well-structured user hotkeys table

The new userHotkeys table is well-designed with appropriate fields:

  • Primary key referencing user.id establishes correct one-to-one relationship
  • JSONB type for shortcuts allows flexible storage of hotkey configurations
  • Standard timestamp fields maintain consistency with other tables

This schema addition properly supports the keyboard shortcut persistence requirements described in the PR summary.

apps/mail/components/ui/ai-sidebar.tsx (1)

9-10: Import changes aligned with keyboard shortcut refactoring.

The changes to import statements, including adding useTranslations and repositioning createContext and useContext, support the broader refactoring effort to replace the custom hotkey handling with the standardized react-hotkeys-hook library.

apps/mail/config/navigation.ts (1)

175-175: Enabling shortcuts settings menu item.

Commenting out the disabled: true property for the shortcuts settings menu now makes this feature accessible to users, which aligns with the keyboard shortcut management enhancements introduced in this PR.

packages/db/migrations/meta/_journal.json (1)

158-165: New migration for user hotkeys table.

The addition of a new migration entry for "0022_round_violations" supports the implementation of persistent, user-customizable keyboard shortcuts. This is a critical component of the overall keyboard shortcut management system.

apps/mail/i18n/config.ts (2)

2-17: Added Farsi language support and improved formatting.

The changes include adding Farsi (Persian) language support to the application and standardizing the formatting with single quotes. This enhances the internationalization capabilities of the application.


24-31: Improved code formatting for locale configuration.

The reformatting of the defaultLocale declaration and availableLocales mapping improves code consistency and readability.

apps/mail/app/(routes)/layout.tsx (2)

4-4: Good integration of the new HotkeyProviderWrapper

The import has been properly added to support the new global keyboard shortcut system.


10-25: Implementation of wrapper component looks correct

The HotkeyProviderWrapper has been properly implemented as the outermost wrapper, providing hotkey context to the entire application. This aligns with the new keyboard shortcut management system that uses the react-hotkeys-hook library.

apps/mail/app/(routes)/mail/layout.tsx (1)

2-3: Correctly imported new hotkey-related components

The GlobalHotkeys and HotkeyProviderWrapper imports have been correctly added to support the new hotkey functionality.

apps/mail/locales/en.json (1)

401-405: Appropriate localization strings added for new hotkey features

The added localization strings for "recordHotkey", "pressKeys", "releaseKeys", and "selectAll" properly support the new keyboard shortcut recording and management functionality.

apps/mail/lib/hotkeys/compose-hotkeys.tsx (1)

1-5: Good client component setup with proper imports

The component is correctly marked with 'use client' and imports the necessary dependencies for hotkey functionality.

packages/db/migrations/0022_round_violations.sql (1)

1-6: Add defaults & timezone‑aware timestamps

created_at / updated_at will always have to be provided by application code.
It is more robust to let the database populate them and to store them as timestamptz rather than timestamp, avoiding silent TZ‑shifts.

-	"created_at" timestamp NOT NULL,
-	"updated_at" timestamp NOT NULL
+	"created_at" timestamptz NOT NULL DEFAULT now(),
+	"updated_at" timestamptz NOT NULL DEFAULT now()

You may also want to add a trigger to keep updated_at in sync on UPDATE.

Would you like a follow‑up snippet for that trigger?

apps/mail/hooks/use-hot-key.ts (1)

7-24: Orphaned global key listeners – verify necessity

With the migration to react-hotkeys-hook, this file now duplicates global
keydown/keyup tracking. If nothing consumes useKeyState anymore, these
listeners add overhead and may interfere with the new library.

Please grep for useKeyState(; if unused, delete the hook entirely.

apps/mail/components/providers/hotkey-provider-wrapper.tsx (3)

1-8: Well-structured imports and client directive.

The client directive and imports are properly organized, bringing in the necessary components from the hotkeys library and the individual hotkey handler components.


10-12: Good type definition for props.

Clean interface definition with proper typing for React children.


14-24: Well-implemented provider structure.

The HotkeyProviderWrapper component correctly initializes the HotkeysProvider with global scope and includes all necessary hotkey handlers. The component follows React best practices by placing the children inside the provider wrapper.

apps/mail/components/draft/drafts-list.tsx (6)

12-12: Good migration to standard library hook.

Successfully replaced the custom useHotKey hook with the standard useHotkeys from the react-hotkeys-hook library.


95-96: Trailing comma added for consistency.

Good code style change to add a trailing comma in the destructured object.


138-156: Proper modifier key handlers.

The implementation for Control, Meta, Shift, and Alt+Shift modifiers correctly sets the selection modes.


158-204: Well-implemented mark as read/unread shortcuts.

The mark as read/unread shortcut handlers correctly implement the functionality for both Meta and Control combinations, with proper error handling and user feedback.


220-242: Successfully uncommented and implemented selection shortcuts.

The Meta+a, Control+a, Meta+n, and Control+n shortcuts are now properly implemented with event.preventDefault() to avoid browser default behaviors.

However, Meta+n and Control+n are calling selectAll() which seems inconsistent with typical "new" shortcut functionality. This might be intentional but could be confusing to users expecting "new item" behavior.

Is using Meta+n/Control+n for "select all" intentional? These key combinations often represent "new" functionality in many applications.


344-345: Trailing comma added for consistency.

Good code style change to add a trailing comma in the object.

docker-compose.yaml (2)

32-32: Security improvement: Using environment variable for Redis token.

Replaced hardcoded token with environment variable ${REDIS_TOKEN}, improving security by avoiding credentials in the codebase.


37-52: Well-configured app service for containerization.

The app service is properly configured with:

  • Container name and build context
  • Port mapping for the application
  • Dependencies on required services
  • Essential environment variables

This enables containerized deployment of the application alongside its dependencies.

Dockerfile (5)

1-7: Good choice of base image and global dependencies.

Using oven/bun:canary is appropriate for a Bun-based application. The global installation of next and turbo ensures the necessary build tools are available.


9-16: Efficient layer caching strategy for dependencies.

The file strategically copies configuration files first, then creates necessary directories and copies package manifests before installing dependencies. This approach optimizes Docker layer caching and speeds up builds.


17-24: Two-phase installation to ensure dependency resolution.

Installing dependencies twice (once with manifests, once with full context) ensures all dependencies are properly resolved, avoiding "missing dependencies" errors that can occur in monorepo setups.


25-30: Appropriate build and environment configuration.

The application is built in production mode, and the NODE_OPTIONS environment variable correctly addresses the Next.js TextEncoder issue by disabling experimental fetch.


32-34: Proper container configuration for running the application.

Exposing port 3000 and using the --host 0.0.0.0 flag ensures the application is accessible from outside the container. Using bun run start follows best practices for running the application.

apps/mail/lib/hotkeys/mail-list-hotkeys.tsx (1)

24-33: Good implementation of the custom event handling

The event listener setup for emailHover is well-implemented with proper cleanup in the effect's return function.

apps/mail/components/mail/mail.tsx (2)

272-274: Good usage of useCallback for the handleClose function

The handleClose function is correctly memoized with useCallback, which prevents unnecessary re-renders and improves performance.


254-270:

Details

✅ Verification successful

Verify scope management implementation across the app

The hotkey scope management looks correct, but let's ensure it's consistent with the broader application architecture.

Let's check how other components handle hotkey scopes:


🏁 Script executed:

#!/bin/bash
# Find other instances of hotkey scope management
echo "Checking hotkey scope management patterns across the app..."
rg -B 3 -A 3 "enableScope|disableScope" --glob "*.{ts,tsx}"

Length of output: 7098


Scope management implementation is consistent across components

  • apps/mail/components/create/create-email.tsx
    • useEffect enables compose, cleanup disables compose
  • apps/mail/components/mail/reply-composer.tsx
    • useEffect toggles compose based on composerIsOpen, cleanup disables compose
  • apps/mail/components/mail/mail-list.tsx
    • onMouseEnter enables mail-list, onMouseLeave disables mail-list
  • apps/mail/components/mail/mail.tsx
    • useEffect switches between thread-display and mail-list based on threadId, cleanup disables both

No mismatches or missing clean‑up calls were found. If exclusive behavior between compose and mail scopes is required, consider adding corresponding disables; otherwise, this implementation aligns with the broader architecture.

apps/mail/config/shortcuts.ts (2)

1-10: LGTM! Type definitions are well-structured and descriptive.

The shortcut type definitions are clear, strongly-typed, and provide good documentation through their naming.


12-34: Shortcuts configuration is comprehensive and well-organized

The keyboard shortcuts are organized with clear scopes, descriptive names, and comprehensive configuration.

Consider grouping shortcuts by scope to improve maintainability as the list grows:

export const keyboardShortcuts: Shortcut[] = [
  // Global shortcuts
  { keys: ["c"], action: "newEmail", type: "single", description: "Compose new email", scope: "global" },
  { keys: ["g", "d"], action: "goToDrafts", type: "combination", description: "Go to drafts", scope: "global" },
  // ...other global shortcuts

  // Mail list shortcuts
  { keys: ["#"], action: "delete", type: "single", description: "Delete email", scope: "mail-list" },
  { keys: ["mod", "a"], action: "selectAll", type: "combination", description: "Select all emails", scope: "mail-list", preventDefault: true },
  // ...other mail-list shortcuts
  
  // Thread display shortcuts
  { keys: ["r"], action: "reply", type: "single", description: "Reply to email", scope: "thread-display" },
  // ...other thread-display shortcuts
  
  // Compose shortcuts
  { keys: ["mod", "Enter"], action: "sendEmail", type: "combination", description: "Send email", scope: "compose" },
  // ...other compose shortcuts
];
apps/mail/components/mail/reply-composer.tsx (4)

24-43: Improved import organization and new hotkey hook

Import organization looks good, with related imports grouped together.


272-281: Fixed indentation in the ccRecipients and bccRecipients blocks

Code formatting improvements with consistent indentation.


500-512: Fixed JSON structure formatting

The indentation of the stringified JSON structure is now more readable.


527-540: Improved Promise.all handling formatting

The Promise.all code block is now properly formatted with consistent indentation.

apps/mail/components/create/create-email.tsx (6)

2-7: Better import organization with DropdownMenu components

Imports are now better organized with related DropdownMenu components grouped together.


14-35: Added hotkey context and organized imports

The import for useHotkeysContext and reorganization of React hooks improves code organization.


564-615: Improved dropdown structure formatting

The code formatting for the contact selection dropdown is now more consistent and readable.


679-736: Better formatting for CC recipient input

The code structure for the CC recipient input and dropdown is improved with consistent formatting.


768-825: Consistent formatting for BCC recipient input

The BCC recipient input and dropdown follow the same improved formatting pattern as the others.


103-103: Consistent implementation of hotkey scope management

The implementation of hotkey scope management is consistent with the approach used in reply-composer, which is good for maintainability.

Consider extracting this pattern to a custom hook to reduce duplication:

// useComposeHotkeyScope.ts
import { useEffect } from 'react';
import { useHotkeysContext } from 'react-hotkeys-hook';

export function useComposeHotkeyScope() {
  const { enableScope, disableScope } = useHotkeysContext();
  
  useEffect(() => {
    enableScope('compose');
    return () => {
      disableScope('compose');
    };
  }, [enableScope, disableScope]);
}

This would simplify your component:

// In your component
import { useComposeHotkeyScope } from '@/hooks/useComposeHotkeyScope';

export function CreateEmail() {
  useComposeHotkeyScope();
  // Rest of component...
}

Also applies to: 464-472

apps/mail/components/mail/mail-list.tsx (1)

33-38:

Details

❌ Incorrect review comment

Remove legacy use-hot-key dependency

useKeyState is imported from @/hooks/use-hot-key, but the PR description says the custom hot‑key hook was removed in favor of react‑hotkeys‑hook.
Leaving this import in place is brittle — the hook might already be deleted or will be in a follow‑up, resulting in a runtime crash when the file is bundled.

-import { useKeyState } from '@/hooks/use-hot-key';
+// import { useKeyState } from '@/hooks/use-hot-key'; // ➜ removed
+import { useKeyState } from '@/lib/hotkeys/use-key-state'; // or migrate logic inline with react‑hotkeys‑hook

Please migrate useKeyState or drop the feature if it’s no longer needed.


🏁 Script executed:

#!/bin/bash
# Search for occurrences of useKeyState and the old/new hook paths
rg "useKeyState" -n .
rg "@\/hooks\/use-hot-key" -n .
fd use-key-state -t f

Length of output: 416


#!/bin/bash
# Check for usages of react-hotkeys-hook
rg -n "react-hotkeys-hook" --glob '*.{ts,tsx,js,jsx}'

# Look for any files or imports related to a new key-state hook
rg -n "use-key-state" --glob '*.{ts,tsx,js,jsx}'
rg -n "@/lib/hotkeys" --glob '*.{ts,tsx,js,jsx}'
fd hotkeys -t d

No action needed for useKeyState import
The custom useKeyState hook (apps/mail/hooks/use-hot-key.ts) is still present and in use, so the import from @/hooks/use-hot-key is valid. There is no replacement hook under @/lib/hotkeys or elsewhere in the codebase—no migration is required at this time.

Likely an incorrect or invalid review comment.

apps/mail/hooks/use-mail-navigation.ts (8)

3-3: Import updated to use react-hotkeys-hook library

The import has been correctly updated to use the standardized useHotkeys hook from the react-hotkeys-hook library, replacing the custom implementation as part of the broader migration to a more standardized approach to hotkey management.


34-61: Well-structured helper functions for thread element access and scrolling

The getThreadElement and scrollIntoView functions are well-implemented with proper dependency arrays and error handling. The scrolling behavior uses "smooth" and "nearest" options for a better user experience.


85-108: Arrow Up handler implementation looks good

The handler correctly prevents default behavior, manages keyboard focus state, and includes special handling for quick action mode. The implementation also includes fallback to the hovered item when no item is focused.


110-133: Arrow Down handler implementation looks good

Similar to the Arrow Up handler, this implementation correctly manages focus state with proper fallbacks and smooth scrolling behavior.


135-153: Enter handler with quick action support

The Enter handler now correctly supports both regular thread navigation and triggering of quick action buttons when in quick action mode. Good use of DOM querying to find and interact with the relevant UI elements.


155-164: Tab and Arrow Left/Right handlers for quick action navigation

These handlers properly manage the quick action mode state and navigation between quick action buttons. The implementation ensures users can toggle in and out of quick action mode and navigate between buttons with clear boundaries.

Also applies to: 166-188


190-203: Escape handler with mode-aware behavior

The Escape handler intelligently handles different states - first exiting quick action mode if active, and then clearing keyboard focus entirely if pressed again. This provides a natural and intuitive escape sequence for users.


205-211: Registration of hotkeys with the new library

All keyboard handlers are now properly registered using the useHotkeys hook from the react-hotkeys-hook library, replacing the previous custom implementation. This standardizes the hotkey handling approach across the application.

packages/db/migrations/meta/0022_snapshot.json (1)

585-635: Well-structured database schema for user hotkeys

The new mail0_user_hotkeys table is properly designed with:

  • user_id as primary key with foreign key constraint to mail0_user
  • shortcuts as a JSONB field for flexible storage of keyboard shortcuts
  • Proper timestamp fields for audit history
  • Consistent naming conventions with other tables

This schema design aligns well with the application's requirements for persistent, user-specific keyboard shortcuts.

apps/mail/lib/hotkeys/use-hotkey-utils.ts (6)

1-5: Clean imports and dependencies

The utility file has minimal and clear imports, properly leveraging the keyboard shortcuts configuration and the hotkeys database for persistence.


6-11: Effective shortcut retrieval with fallback

The findShortcut function properly retrieves user-configured shortcuts from the database with a fallback to default shortcuts if none are found. This ensures a consistent user experience while allowing for customization.


15-40: Effective key formatting for hotkey registration

The formatKeys function properly handles different key formats and special characters, with platform-specific adjustments. This ensures that hotkeys are correctly registered with the underlying library.


42-69: Comprehensive display formatting for key combinations

The formatDisplayKeys function provides user-friendly, platform-specific representations of keyboard shortcuts. It handles a wide range of special keys with appropriate symbols, enhancing the user experience.


71-83: Well-defined options type with sensible defaults

The HotkeyOptions type and default values provide a clear interface for customizing shortcut behavior, with sensible default values that work for most use cases.


13-13:

Details

✅ Verification successful

Platform detection might not work in SSR environments

The platform detection for Mac is done at module level, which might not correctly adapt when code is rendered server-side first.


🏁 Script executed:

#!/bin/bash
# Check if the application uses SSR or is purely client-side

# Check for Next.js SSR-related components
rg -A 1 -B 1 "getServerSideProps|getInitialProps|getStaticProps" --type ts --type tsx

# Check if there are any server components
fd -e tsx -e jsx -E "node_modules" | xargs grep -l "use server"

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Ensure ripgrep recognizes .tsx files and search for Next.js SSR methods
rg --type-add 'tsx:*.tsx' --type ts --type tsx "getServerSideProps|getInitialProps|getStaticProps" -n .

# Check for Next.js in dependencies
grep -R '"next"' -n package.json

# List any .tsx files under apps/mail to confirm React/Next usage
fd -t f -e tsx apps/mail

Length of output: 509


Platform detection is safe – no SSR in this project
We searched for Next.js SSR methods (getServerSideProps, getInitialProps, getStaticProps) and found none, and there’s no Next.js dependency. The typeof window !== 'undefined' guard ensures isMac is only evaluated in the browser. No changes needed.

Comment on lines +36 to +37
const searchIconRef = useRef<any>(null);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify if searchIconRef is being used

The searchIconRef is created but doesn't appear to be used anywhere in the visible component code. This could be a leftover from development or preparation for future functionality.


🏁 Script executed:

#!/bin/bash
# Look for usage of searchIconRef in this file or related components
rg "searchIconRef" apps/mail/components/draft/

Length of output: 127


Remove unused searchIconRef declaration

searchIconRef is defined but never referenced elsewhere in apps/mail/components/draft/drafts.tsx. To clean up dead code, remove its declaration (or add a comment explaining planned future use if still needed).

• Location:

  • apps/mail/components/draft/drafts.tsx, line 36

Comment on lines +7 to +11
<HotkeyProviderWrapper>
<AppSidebar />
<KeyboardShortcuts />
<GlobalHotkeys />
<div className="w-full bg-white md:py-3 md:pr-2 dark:bg-black">{children}</div>
</>
</HotkeyProviderWrapper>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Potential duplicate HotkeyProviderWrapper

The layout is wrapped with HotkeyProviderWrapper, but the root layout (apps/mail/app/(routes)/layout.tsx) already includes this wrapper. Nested providers might cause unexpected behavior.


🏁 Script executed:

#!/bin/bash
# Check if the HotkeyProviderWrapper is implemented with a React Context that supports nesting
rg -A 10 "createContext|HotkeyProviderWrapper" --type tsx --type jsx

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Search for the HotkeyProviderWrapper definition and its implementation
rg -A 20 "HotkeyProviderWrapper" .

# Search for any createContext usage that might indicate nested context handling
rg -A 10 "createContext" .

Length of output: 12957


Remove nested HotkeyProviderWrapper to prevent duplicate context

The HotkeyProviderWrapper is already applied at the root layout (apps/mail/app/(routes)/layout.tsx), and its implementation renders <GlobalHotkeys /> internally. Wrapping the mail‐specific layout again causes a second provider and duplicate <GlobalHotkeys />, which can lead to handlers firing twice or scope conflicts.

Please update apps/mail/app/(routes)/mail/layout.tsx as follows:

• Remove the import and usage of HotkeyProviderWrapper.
• Remove the extra <GlobalHotkeys /> (it’s provided by the wrapper at the root).

Suggested diff:

- import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper';
+ // removed redundant wrapper import

export default function MailLayout({ children }: { children: React.ReactNode }) {
-  return (
-    <HotkeyProviderWrapper>
-      <AppSidebar />
-      <GlobalHotkeys />
-      <div className="w-full bg-white md:py-3 md:pr-2 dark:bg-black">
-        {children}
-      </div>
-    </HotkeyProviderWrapper>
-  );
+  return (
+    <>
+      <AppSidebar />
+      <div className="w-full bg-white md:py-3 md:pr-2 dark:bg-black">
+        {children}
+      </div>
+    </>
+  );
}

Comment on lines +6 to +16
export function ComposeHotkeys() {
const scope = 'compose';

const handlers = {};

const composeShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);

useShortcuts(composeShortcuts, handlers, { scope });

return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Empty handlers object requires verification

The ComposeHotkeys component is filtering shortcuts correctly by scope, but the handlers object is empty. This might be intentional if the useShortcuts hook manages the handlers internally, but should be verified.


🏁 Script executed:

#!/bin/bash
# Check the implementation of useShortcuts to understand how handlers are used
rg -A 15 "export function useShortcuts" --type tsx --type ts

# Also check if there are other similar hotkey components to compare implementation
rg -A 5 "const handlers = {}" --type tsx --type ts

Length of output: 263


To ensure we understand how handlers is used and whether it must be populated, let’s locate the useShortcuts implementation and compare other hotkey components’ usage:


🏁 Script executed:

#!/bin/bash
# Find the useShortcuts hook definition
rg -n "function useShortcuts" -A10

# Find all direct usages of useShortcuts(
rg -n "useShortcuts(" -A5

# List all TSX files that use the hook
rg -l "useShortcuts" -g "*.tsx"

# For each hotkeys component, show how handlers is declared
for file in $(rg -l "useShortcuts" -g "*hotkeys.tsx"); do
  echo "File: $file"
  rg -n "const handlers" -A2 "$file" || echo "  No handlers declaration found"
done

Length of output: 2579


ComposeHotkeys handlers must be implemented

The useShortcuts hook only binds shortcuts when there’s a matching handler function. With an empty handlers object in compose-hotkeys.tsx, none of the compose‑scoped shortcuts will ever fire. Other hotkey components declare their handlers (for example, see thread-display-hotkeys.tsx, mail-list-hotkeys.tsx, global-hotkeys.tsx).

Please populate the handlers object in apps/mail/lib/hotkeys/compose-hotkeys.tsx (around lines 8–12) to map each action from keyboardShortcuts to its callback. For example:

const handlers = {
  sendEmail: () => {/* trigger send */},
  saveDraft: () => {/* trigger save */},
  // …add handlers for all compose actions
};

Ensure each key matches an action in your compose scope shortcuts.

Comment on lines +6 to +15
const closeView = (event: KeyboardEvent) => {
event.preventDefault();
};

export function ThreadDisplayHotkeys() {
const scope = 'thread-display';

const handlers = {
closeView: () => closeView(new KeyboardEvent('keydown', { key: 'Escape' })),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handler prevents default on a synthetic event – does nothing

closeView(new KeyboardEvent(...)) creates a fresh event whose preventDefault
has no effect on the real key press.
Moreover, the keyboardShortcuts config shown in the repo does not define
an action named "closeView", so this hotkey will never fire.

-const closeView = (event: KeyboardEvent) => {
-  event.preventDefault();
-};
+// TODO: replace with actual close‑thread logic invoked by Esc
+function closeThread() {
+  // e.g. navigate back or update state
+}

@@
-  const handlers = {
-    closeView: () => closeView(new KeyboardEvent('keydown', { key: 'Escape' })),
-  };
+  const handlers = { closeThread };

Don’t forget to add the corresponding "closeThread" action to
config/shortcuts.ts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const closeView = (event: KeyboardEvent) => {
event.preventDefault();
};
export function ThreadDisplayHotkeys() {
const scope = 'thread-display';
const handlers = {
closeView: () => closeView(new KeyboardEvent('keydown', { key: 'Escape' })),
};
// TODO: replace with actual close‑thread logic invoked by Esc
function closeThread() {
// e.g. navigate back or update state
}
export function ThreadDisplayHotkeys() {
const scope = 'thread-display';
const handlers = { closeThread };
}

@@ -1,4 +1,4 @@
import { useCallback, useRef, useLayoutEffect, useState, useEffect } from "react";
import { useCallback, useRef, useLayoutEffect, useState, useEffect } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused React imports – will break the build under noUnusedLocals

Only useCallback is referenced; the remainder cause TS/ESLint errors.

-import { useCallback, useRef, useLayoutEffect, useState, useEffect } from 'react';
+import { useCallback } from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useCallback, useRef, useLayoutEffect, useState, useEffect } from 'react';
import { useCallback } from 'react';

Comment on lines +75 to +98
function Shortcut({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {
const [isRecording, setIsRecording] = useState(false);
const displayKeys = formatDisplayKeys(keys);

const handleHotkeyRecorded = async (newKeys: string[]) => {
try {
// Find the original shortcut to preserve its type and description
const originalShortcut = keyboardShortcuts.find(s => s.action === action);
if (!originalShortcut) {
throw new Error('Original shortcut not found');
}

const updatedShortcut: Shortcut = {
...originalShortcut,
keys: newKeys,
};

await hotkeysDB.saveHotkey(updatedShortcut);
toast.success('Shortcut saved successfully');
} catch (error) {
console.error('Failed to save shortcut:', error);
toast.error('Failed to save shortcut');
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Name collision with imported type Shortcut

A component named Shortcut is declared while a type with the same identifier
is imported earlier (import { keyboardShortcuts, type Shortcut } …).
Typescript allows this but tools (see Biome hint) flag it as a redeclaration and
it confuses readers (“is this a type or component?”).

Rename the component, e.g. ShortcutItem:

-function Shortcut({ children, keys, action }: { ... }) {
+function ShortcutItem({ children, keys, action }: { ... }) {

and update the JSX usage above.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function Shortcut({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {
const [isRecording, setIsRecording] = useState(false);
const displayKeys = formatDisplayKeys(keys);
const handleHotkeyRecorded = async (newKeys: string[]) => {
try {
// Find the original shortcut to preserve its type and description
const originalShortcut = keyboardShortcuts.find(s => s.action === action);
if (!originalShortcut) {
throw new Error('Original shortcut not found');
}
const updatedShortcut: Shortcut = {
...originalShortcut,
keys: newKeys,
};
await hotkeysDB.saveHotkey(updatedShortcut);
toast.success('Shortcut saved successfully');
} catch (error) {
console.error('Failed to save shortcut:', error);
toast.error('Failed to save shortcut');
}
};
function ShortcutItem({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {
const [isRecording, setIsRecording] = useState(false);
const displayKeys = formatDisplayKeys(keys);
const handleHotkeyRecorded = async (newKeys: string[]) => {
try {
// Find the original shortcut to preserve its type and description
const originalShortcut = keyboardShortcuts.find(s => s.action === action);
if (!originalShortcut) {
throw new Error('Original shortcut not found');
}
const updatedShortcut: Shortcut = {
...originalShortcut,
keys: newKeys,
};
await hotkeysDB.saveHotkey(updatedShortcut);
toast.success('Shortcut saved successfully');
} catch (error) {
console.error('Failed to save shortcut:', error);
toast.error('Failed to save shortcut');
}
};
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 75-75: Shouldn't redeclare 'Shortcut'. Consider to delete it or rename it.

'Shortcut' is defined here:

(lint/suspicious/noRedeclare)

Comment on lines +79 to +98
const handleHotkeyRecorded = async (newKeys: string[]) => {
try {
// Find the original shortcut to preserve its type and description
const originalShortcut = keyboardShortcuts.find(s => s.action === action);
if (!originalShortcut) {
throw new Error('Original shortcut not found');
}

const updatedShortcut: Shortcut = {
...originalShortcut,
keys: newKeys,
};

await hotkeysDB.saveHotkey(updatedShortcut);
toast.success('Shortcut saved successfully');
} catch (error) {
console.error('Failed to save shortcut:', error);
toast.error('Failed to save shortcut');
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

UI never updates after saving a new hot‑key

handleHotkeyRecorded persists the change but doesn’t update local state, so
the displayed key combo stays stale until the page is reloaded.

Quick fix inside handleHotkeyRecorded:

 await hotkeysDB.saveHotkey(updatedShortcut);
+setShortcuts((prev) =>
+  prev.map((s) => (s.action === action ? updatedShortcut : s)),
+);
 toast.success('Shortcut saved successfully');

This keeps the UI and IndexedDB in sync.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleHotkeyRecorded = async (newKeys: string[]) => {
try {
// Find the original shortcut to preserve its type and description
const originalShortcut = keyboardShortcuts.find(s => s.action === action);
if (!originalShortcut) {
throw new Error('Original shortcut not found');
}
const updatedShortcut: Shortcut = {
...originalShortcut,
keys: newKeys,
};
await hotkeysDB.saveHotkey(updatedShortcut);
toast.success('Shortcut saved successfully');
} catch (error) {
console.error('Failed to save shortcut:', error);
toast.error('Failed to save shortcut');
}
};
const handleHotkeyRecorded = async (newKeys: string[]) => {
try {
// Find the original shortcut to preserve its type and description
const originalShortcut = keyboardShortcuts.find(s => s.action === action);
if (!originalShortcut) {
throw new Error('Original shortcut not found');
}
const updatedShortcut: Shortcut = {
...originalShortcut,
keys: newKeys,
};
await hotkeysDB.saveHotkey(updatedShortcut);
setShortcuts((prev) =>
prev.map((s) => (s.action === action ? updatedShortcut : s)),
);
toast.success('Shortcut saved successfully');
} catch (error) {
console.error('Failed to save shortcut:', error);
toast.error('Failed to save shortcut');
}
};

Comment on lines +54 to +60
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(STORE_NAME)) {
db.deleteObjectStore(STORE_NAME);
}
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unconditional object‑store deletion wipes user data on every version bump

Inside onupgradeneeded you always do db.deleteObjectStore(STORE_NAME) before
createObjectStore. Any saved shortcuts are lost whenever DB_VERSION
increments.

Safer upgrade logic:

-if (db.objectStoreNames.contains(STORE_NAME)) {
-  db.deleteObjectStore(STORE_NAME);
-}
-db.createObjectStore(STORE_NAME, { keyPath: 'id' });
+if (!db.objectStoreNames.contains(STORE_NAME)) {
+  db.createObjectStore(STORE_NAME, { keyPath: 'id' });
+}

Preserve data unless the schema actually changes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(STORE_NAME)) {
db.deleteObjectStore(STORE_NAME);
}
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
};

Comment on lines +49 to +75
request.onsuccess = () => {
this.db = request.result;
resolve();
};

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (db.objectStoreNames.contains(STORE_NAME)) {
db.deleteObjectStore(STORE_NAME);
}
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
};

request.onsuccess = async () => {
this.db = request.result;
if (!serverHotkeysLoaded) {
serverHotkeysLoaded = true;
const serverHotkeys = await fetchServerHotkeys();
if (serverHotkeys?.length) {
const mergedHotkeys = this.mergeWithDefaults(serverHotkeys);
await this.saveAllHotkeys(mergedHotkeys, false);
} else {
await this.initializeDefaultShortcuts();
}
}
resolve();
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate onsuccess handler – first one is overwritten

request.onsuccess is assigned twice (lines 49‑52 and again 62‑75).
The first assignment is silently lost, which can skip this.db = request.result
or double‑resolve the promise in rare cases.

Merge the logic into a single handler:

-request.onsuccess = () => {
-  this.db = request.result;
-  resolve();
-};
-
-request.onsuccess = async () => {
+request.onsuccess = async () => {
   this.db = request.result;
   ...

Alternatively use addEventListener('success', …).

Comment on lines +133 to +144
export function useShortcuts(
shortcuts: Shortcut[],
handlers: { [key: string]: () => void },
options: Partial<HotkeyOptions> = {},
) {
shortcuts.forEach((shortcut) => {
const handler = handlers[shortcut.action];
if (handler) {
useShortcut(shortcut, handler, options);
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring useShortcuts hook to avoid hook rule violations

The useShortcuts function calls hooks inside a forEach loop, which could potentially violate React's rules of hooks depending on how it's implemented. React hooks should be called in the same order on every render.

A safer approach would be:

- export function useShortcuts(
-   shortcuts: Shortcut[],
-   handlers: { [key: string]: () => void },
-   options: Partial<HotkeyOptions> = {},
- ) {
-   shortcuts.forEach((shortcut) => {
-     const handler = handlers[shortcut.action];
-     if (handler) {
-       useShortcut(shortcut, handler, options);
-     }
-   });
- }
+ export function useShortcuts(
+   shortcuts: Shortcut[],
+   handlers: { [key: string]: () => void },
+   options: Partial<HotkeyOptions> = {},
+ ) {
+   // Create a stable array of shortcuts that won't change between renders
+   // if the shortcuts array reference changes but content is the same
+   const stableShortcuts = useMemo(
+     () => shortcuts.filter(shortcut => !!handlers[shortcut.action]),
+     [shortcuts, handlers]
+   );
+   
+   // Register each shortcut in order
+   // This ensures the hooks are always called in the same order
+   for (let i = 0; i < stableShortcuts.length; i++) {
+     const shortcut = stableShortcuts[i];
+     const handler = handlers[shortcut.action];
+     // eslint-disable-next-line react-hooks/rules-of-hooks
+     useShortcut(shortcut, handler, options);
+   }
+ }

Note that even with this approach, you need to disable the ESLint rule because React's linter can't verify that the loop has a stable length. A better approach might be to register all shortcuts in a single hook call if the underlying library supports it.

Committable suggestion skipped: line range outside the PR's diff.

@nizzyabi nizzyabi merged commit c4172c2 into main Apr 17, 2025
4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request May 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants