Skip to content

Comments

Early access generate#699

Closed
nizzyabi wants to merge 53 commits intomainfrom
early-access-generate
Closed

Early access generate#699
nizzyabi wants to merge 53 commits intomainfrom
early-access-generate

Conversation

@nizzyabi
Copy link
Collaborator

@nizzyabi nizzyabi commented Apr 17, 2025

NEVER MERGE

Summary by CodeRabbit

  • New Features

    • Introduced customizable and persistent keyboard shortcuts, including shortcut editing, recording, and per-user storage.
    • Added hotkey management UI and global/local scope support for keyboard shortcuts throughout the mail app.
    • Implemented early access user management, including bulk selection, confirmation, and automated welcome emails.
    • Added Farsi (Persian) language support and localization structure.
  • Improvements

    • Enhanced keyboard navigation and hotkey handling using a new provider and external library.
    • Refined navigation and quick action handling in mail lists.
    • Enabled editing and resetting of shortcuts with IndexedDB and server sync.
    • Improved code formatting and UI consistency in several components.
  • Bug Fixes

    • Fixed environment variable handling and service configuration in Docker and compose files.
  • Chores

    • Added new database tables and migrations for user hotkeys.
    • Updated dependencies and localization files.

nizzyabi and others added 30 commits March 25, 2025 14:24
- refactor hotkeys
- add hotkey recorder
@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 19, 2025 5:44pm

@nizzyabi nizzyabi marked this pull request as draft April 17, 2025 16:55
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 17, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This update introduces a comprehensive user hotkeys system and Farsi language support. It adds a new database table and API endpoints for storing user-specific keyboard shortcuts, a frontend IndexedDB-based persistence layer, and a complete React integration for recording, editing, and using hotkeys via the react-hotkeys-hook library. The previous custom hotkey hook is removed, and the UI is refactored to support persistent, editable shortcuts with scope-based activation. Additionally, Farsi (Persian) localization is added, including configuration updates and a new, structured locale file. The deployment setup is enhanced with a Dockerfile and updated Docker Compose configuration.

Changes

Files / Group Change Summary
Dockerfile, docker-compose.yaml Add Dockerfile for Bun/Next.js app; update Compose to add app service, use env vars for tokens.
apps/mail/package.json Add react-hotkeys-hook dependency.
apps/mail/config/shortcuts.ts Redefine shortcut structure: add metadata, types, scope, preventDefault; update key bindings.
apps/mail/lib/hotkeys/hotkeys-db.ts, .../use-hotkey-utils.ts, .../compose-hotkeys.tsx, .../global-hotkeys.tsx, .../mail-list-hotkeys.tsx, .../thread-display-hotkeys.tsx Add IndexedDB hotkey persistence, new hotkey hooks/utilities, and React components for each hotkey scope.
apps/mail/app/(routes)/settings/shortcuts/page.tsx, .../hotkey-recorder.tsx Refactor shortcuts page to use stateful, persistent, and editable hotkeys; add hotkey recording dialog.
apps/mail/components/providers/hotkey-provider-wrapper.tsx, .../layout.tsx, .../mail/layout.tsx Add hotkey provider wrapper and integrate into layouts; replace old hotkey components.
apps/mail/components/mail/keyboard-shortcuts.tsx, apps/mail/hooks/use-hot-key.ts Remove old hotkey component and custom hook.
apps/mail/components/create/create-email.tsx, .../draft/drafts-list.tsx, .../draft/drafts.tsx, .../mail/mail-list.tsx, .../mail/mail.tsx, .../mail/reply-composer.tsx, .../ui/ai-sidebar.tsx Refactor components to use new hotkey context/hooks, remove old logic, improve formatting.
apps/mail/app/api/v1/hotkeys/route.ts, .../api/resend/add-to-audience/route.ts, .../api/resend/send-early-access/route.ts Add API endpoints for hotkey management and Resend audience/email features.
apps/mail/app/(full-width)/early-access/early-access.tsx, .../page.tsx, .../.env.example Add early access management UI and API integration; update env example.
apps/mail/config/navigation.ts Enable shortcuts settings menu item.
apps/mail/i18n/config.ts, .../locales/en.json, .../locales/fa.json, i18n.json Add Farsi language support, update locale config, add new strings.
packages/db/migrations/0022_round_violations.sql, .../meta/0022_snapshot.json, .../meta/_journal.json, .../src/schema.ts Add userHotkeys table and schema migration for user-specific shortcuts.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI (Shortcuts Page)
    participant HotkeysDB (IndexedDB)
    participant API (/api/v1/hotkeys)
    participant DB

    User->>UI (Shortcuts Page): Open settings/shortcuts
    UI->>HotkeysDB: getAllHotkeys()
    HotkeysDB->>API: (if not initialized) GET /api/v1/hotkeys
    API->>DB: Query userHotkeys table
    DB-->>API: Return shortcuts
    API-->>HotkeysDB: Return shortcuts
    HotkeysDB-->>UI: Return merged shortcuts

    User->>UI: Edit/record a shortcut
    UI->>HotkeysDB: saveHotkey(shortcut)
    HotkeysDB->>API: PUT /api/v1/hotkeys (sync)
    API->>DB: Upsert shortcut
    DB-->>API: Ack
    API-->>HotkeysDB: Ack
    HotkeysDB-->>UI: Ack

    User->>UI: Use hotkey in app
    UI->>HotkeysDB: getHotkey(action)
    HotkeysDB-->>UI: Return shortcut
    UI->>UI: Activate handler for shortcut
Loading

Possibly related PRs

  • Mail-0/Zero#591: Introduces the same Dockerfile and Compose changes for deployment, directly related to containerization setup.
  • Mail-0/Zero#636: Adds Farsi language support, updating locale files and config, matching the i18n changes here.
  • Mail-0/Zero#649: Implements the new hotkeys system, including hooks, providers, and UI integration, directly related to the hotkey feature.

Suggested reviewers

  • MrgSub

Poem

In the garden of code, a new breeze blows,
Hotkeys now bloom where the old logic goes.
Farsi joins in, with scripts flowing right,
Shortcuts are captured—recorded in light!
Docker sets sail, the Compose ship departs,
Rabbits tap keys, and joy fills their hearts.

(🐇✨⌨️🌱)


🪧 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: 28

🔭 Outside diff range comments (3)
apps/mail/components/ui/ai-sidebar.tsx (1)

45-91: 💡 Verification agent

🧩 Analysis chain

Removed hotkey handlers for sidebar toggle.

The keyboard shortcuts for toggling the sidebar (Meta+0, Control+0) have been removed as part of the migration to a centralized hotkey management system. Ensure that this functionality is properly implemented in the new global hotkeys component to maintain the same user experience.


🏁 Script executed:

#!/bin/bash
# Check if the sidebar toggle functionality exists in the new hotkey system
echo "Searching for sidebar toggle hotkeys in the new implementation..."
rg -A 3 -B 3 "toggleOpen|setOpen\(.*\!open|sidebar.*toggle" --glob "*.tsx" --glob "*.ts" | grep -v "ai-sidebar.tsx"

Length of output: 389


Let’s locate where global hotkeys are registered and verify if Meta+0 / Control+0 for toggling the AI sidebar exist:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Searching for explicit 'Meta+0' or 'Control+0' registrations…"
rg -n 'Meta\+0|Control\+0' --glob "*.ts*" || true

echo "2) Finding any useHotkeys or hotkey hook usages…"
rg -n 'useHotkeys|hotkey' --glob "*.ts*" --glob "*.tsx" || true

echo "3) Listing files/folders named '*hotkey*'…"
fd -HI hotkey

Length of output: 6947


We need to confirm whether Meta+0 / Control+0 is wired up in global-hotkeys.tsx—it currently doesn’t appear in any hotkey files. Let’s inspect its registrations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Looking for useShortcuts in global-hotkeys.tsx:"
rg -n "useShortcuts" -A 3 -B 1 apps/mail/lib/hotkeys/global-hotkeys.tsx

Length of output: 484


We need to confirm whether the global shortcuts config defines Meta+0 / Control+0. Please run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file exporting keyboardShortcuts
echo "Finding keyboardShortcuts definition…"
FILE=$(rg -l "export const keyboardShortcuts" --glob "*.ts*" --glob "*.js" | head -n1)

if [[ -z "$FILE" ]]; then
  echo "ERROR: keyboardShortcuts export not found."
  exit 1
fi

echo "Found in: $FILE"
echo "Showing export and its entries:"
rg -n "export const keyboardShortcuts" -A 50 "$FILE"

Length of output: 2975


Add missing AI sidebar toggle hotkeys

It looks like the Meta+0 / Control+0 shortcuts for opening and closing the AI sidebar were removed but haven’t been re‑registered in the new global hotkey system. To restore the original UX:

• In apps/mail/config/shortcuts.ts, add a global shortcut entry:

 export const keyboardShortcuts: Shortcut[] = [
   { keys: ["c"], action: "newEmail", … },
   …
+  // Toggle AI sidebar
+  { keys: ["mod", "0"], action: "toggleAISidebar", type: "combination", description: "Toggle AI sidebar", scope: "global" },
 ];

• In apps/mail/lib/hotkeys/global-hotkeys.tsx, wire up the handler:

 import { keyboardShortcuts } from '@/config/shortcuts';
 import { useShortcuts } from './use-hotkey-utils';
+import { useAISidebar } from '@/components/ui/ai-sidebar';

 export function GlobalHotkeys() {
+  const { setOpen } = useAISidebar();
   const scope = 'global';

   const handlers = {
     newEmail: () => { … },
     …,
+    toggleAISidebar: () => setOpen(open => !open),
   };

   useShortcuts(keyboardShortcuts, handlers, { scope });
   return null;
 }

This will restore the original Meta+0 / Control+0 behavior for toggling the AI sidebar.

apps/mail/app/api/auth/early-access/route.ts (1)

23-71: ⚠️ Potential issue

Rate limiting has been removed - potential security concern

The code has been modified to remove rate limiting on the early access registration endpoint. Without rate limiting, this endpoint could be vulnerable to abuse, such as spamming registrations or conducting denial of service attacks.

Consider reimplementing rate limiting or adding alternative protection mechanisms:

import { type NextRequest, NextResponse } from 'next/server';
import { earlyAccess } from '@zero/db/schema';
import { db } from '@zero/db';
import { Resend } from 'resend';
+import { Ratelimit } from '@upstash/ratelimit';
+import { Redis } from '@upstash/redis';

// Add back rate limiting configuration
+const ratelimit = new Ratelimit({
+  redis: Redis.fromEnv(),
+  limiter: Ratelimit.slidingWindow(5, '1 h'),
+  analytics: true,
+});

export async function POST(req: NextRequest) {
  try {
+    // Get IP for rate limiting
+    const ip = req.headers.get('x-forwarded-for') || 'anonymous';
+    const { success, limit, reset, remaining } = await ratelimit.limit(ip);
+    
+    // Return rate limit error if exceeded
+    if (!success) {
+      return NextResponse.json(
+        { error: 'Too many requests, please try again later' },
+        { 
+          status: 429,
+          headers: {
+            'X-RateLimit-Limit': limit.toString(),
+            'X-RateLimit-Remaining': remaining.toString(),
+            'X-RateLimit-Reset': reset.toString(),
+          }
+        }
+      );
+    }
    
    const body = await req.json();
    console.log('Request body:', body);
    // Rest of the function remains unchanged
apps/mail/hooks/use-hot-key.ts (1)

1-33: 🛠️ Refactor suggestion

Consider removing this file entirely

Since the main useHotKey hook has been removed in favor of the react-hotkeys-hook library, the remaining useKeyState hook appears to be redundant. It also has potential issues with its implementation:

  1. It uses a global map for state management which is non-idiomatic for React
  2. The setTimeout with 0 delay to initialize listeners is a workaround
  3. Event listeners are never fully cleaned up, which could cause memory leaks

Consider removing this file completely and migrating any remaining usage of useKeyState to the new hotkey system.

If useKeyState is still needed, reimplement it using React's state management and proper cleanup:

// Alternative implementation
import { useState, useEffect } from 'react';

export function useKeyState(targetKey: string): boolean {
  const [keyPressed, setKeyPressed] = useState(false);

  useEffect(() => {
    const downHandler = ({ key }: KeyboardEvent) => {
      if (key === targetKey) {
        setKeyPressed(true);
      }
    };

    const upHandler = ({ key }: KeyboardEvent) => {
      if (key === targetKey) {
        setKeyPressed(false);
      }
    };
    
    window.addEventListener('keydown', downHandler);
    window.addEventListener('keyup', upHandler);
    
    // Cleanup
    return () => {
      window.removeEventListener('keydown', downHandler);
      window.removeEventListener('keyup', upHandler);
    };
  }, [targetKey]);

  return keyPressed;
}
🧹 Nitpick comments (27)
apps/mail/components/draft/drafts.tsx (1)

36-37: Review unused searchIconRef

A new ref searchIconRef has been added but doesn't appear to be used anywhere in the component.

If this ref isn't being used, consider removing it to avoid confusion. If it's intended for future use or is used by a component not shown in this file, add a comment explaining its purpose.

-  const searchIconRef = useRef<any>(null);
-

Additionally, the AI summary mentions an import for useHotKey was removed, which aligns with the migration to react-hotkeys-hook in this PR.

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

17-17: Consider caching the filtered shortcuts

This filter operation will run on each render. Consider memoizing the result to avoid unnecessary recalculations.

+  const threadDisplayShortcuts = useMemo(
+    () => keyboardShortcuts.filter((shortcut) => shortcut.scope === scope),
+    [scope]
+  );
-  const threadDisplayShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);

Don't forget to import useMemo from React.

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

8-8: Consider cascade deletion strategy

The current foreign key constraint uses "no action" for ON DELETE, which could lead to orphaned hotkey records if a user is deleted.

Consider using CASCADE for deletion if orphaned hotkey records would be useless:

-ALTER TABLE "mail0_user_hotkeys" ADD CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE no action ON UPDATE no action;
+ALTER TABLE "mail0_user_hotkeys" ADD CONSTRAINT "mail0_user_hotkeys_user_id_mail0_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."mail0_user"("id") ON DELETE CASCADE ON UPDATE no action;
apps/mail/lib/hotkeys/global-hotkeys.tsx (1)

21-21: Consider caching the filtered shortcuts

This filter operation will run on each render. Consider memoizing the result to avoid unnecessary recalculations.

+import { useMemo } from 'react';

// ...

+  const globalShortcuts = useMemo(
+    () => keyboardShortcuts.filter((shortcut) => shortcut.scope === scope),
+    [scope]
+  );
-  const globalShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
apps/mail/components/providers/hotkey-provider-wrapper.tsx (1)

17-20: Consider conditional rendering of scope-specific hotkey components

All hotkey components are always rendered regardless of whether their scope is active. This could lead to unnecessary hook executions for inactive scopes.

Consider conditionally rendering the scope-specific components based on current application state:

import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';

// Inside the component:
+  const pathname = usePathname();
+  const [activeComponents, setActiveComponents] = useState(['global']);
+  
+  useEffect(() => {
+    if (pathname.includes('/mail/compose')) {
+      setActiveComponents(['global', 'compose']);
+    } else if (pathname.includes('/mail/thread')) {
+      setActiveComponents(['global', 'thread-display']);
+    } else if (pathname.includes('/mail')) {
+      setActiveComponents(['global', 'mail-list']);
+    } else {
+      setActiveComponents(['global']);
+    }
+  }, [pathname]);

// Then in the JSX:
      <GlobalHotkeys />
-      <MailListHotkeys />
-      <ThreadDisplayHotkeys />
-      <ComposeHotkeys />
+      {activeComponents.includes('mail-list') && <MailListHotkeys />}
+      {activeComponents.includes('thread-display') && <ThreadDisplayHotkeys />}
+      {activeComponents.includes('compose') && <ComposeHotkeys />}
Dockerfile (1)

5-6: Global installation may be unnecessary

Installing packages globally in a Docker container is typically unnecessary since the container is dedicated to running a specific application.

-# Install turbo globally
-RUN bun install -g next turbo
+# These will be available through dependencies in package.json
apps/mail/lib/hotkeys/mail-list-hotkeys.tsx (4)

22-33: Use typed custom events for better type safety

The component uses a custom event without proper TypeScript definitions, which might lead to type safety issues.

const hoveredEmailId = useRef<string | null>(null);

+// Define a custom event type
+interface EmailHoverEvent extends CustomEvent {
+  detail: { id: string | null };
+}

useEffect(() => {
-  const handleEmailHover = (event: CustomEvent<{ id: string | null }>) => {
+  const handleEmailHover = (event: EmailHoverEvent) => {
    hoveredEmailId.current = event.detail.id;
  };

-  window.addEventListener('emailHover', handleEmailHover as EventListener);
+  window.addEventListener('emailHover', handleEmailHover as EventListener);
  return () => {
-    window.removeEventListener('emailHover', handleEmailHover as EventListener);
+    window.removeEventListener('emailHover', handleEmailHover as EventListener);
  };
}, []);

52-63: Extract duplicate toast.promise logic into a helper function

The component has duplicate toast.promise logic for marking emails as unread that could be extracted into a helper function.

+const handleMarkAsUnread = useCallback((ids: string[]) => {
+  return toast.promise(markAsUnreadAction({ ids }), {
+    loading: t('common.actions.loading'),
+    success: async () => {
+      await Promise.all([mutate(), mutateStats()]);
+      return t('common.mail.markedAsUnread');
+    },
+    error: t('common.mail.failedToMarkAsUnread'),
+  });
+}, [mutate, mutateStats, t]);
+
const markAsUnread = useCallback(() => {
  if (hoveredEmailId.current) {
-    toast.promise(markAsUnreadAction({ ids: [hoveredEmailId.current] }), {
-      loading: t('common.actions.loading'),
-      success: async () => {
-        await Promise.all([mutate(), mutateStats()]);
-        return t('common.mail.markedAsUnread');
-      },
-      error: t('common.mail.failedToMarkAsUnread'),
-    });
+    handleMarkAsUnread([hoveredEmailId.current]);
    return;
  }

65-78: Apply the extracted helper function here as well

For consistency, use the same helper function for the bulk selection case.

const idsToMark = mail.bulkSelected;
if (idsToMark.length === 0) {
  toast.info(t('common.mail.noEmailsToSelect'));
  return;
}

-toast.promise(markAsUnreadAction({ ids: idsToMark }), {
-  loading: t('common.actions.loading'),
-  success: async () => {
-    await Promise.all([mutate(), mutateStats()]);
-    return t('common.mail.markedAsUnread');
-  },
-  error: t('common.mail.failedToMarkAsUnread'),
-});
+handleMarkAsUnread(idsToMark);

13-91: Consider adding a debounce mechanism to prevent action spam

There's no protection against rapidly firing shortcut actions which could lead to API spamming or race conditions.

Add debounce logic to prevent rapid firing of shortcut actions:

import { debounce } from 'lodash';

// Inside the component:
const debouncedMarkAsUnread = useCallback(
  debounce(() => {
    // Existing markAsUnread logic
  }, 300),
  [mail.bulkSelected, mutate, mutateStats, t]
);

// Then use debouncedMarkAsUnread in your handlers
const handlers = {
  markAsUnread: debouncedMarkAsUnread,
  selectAll,
};
apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx (2)

23-57: Limit the number of keys that can be recorded

There's no limit to how many keys can be recorded in a shortcut, which could lead to overly complex shortcuts that are difficult to remember or execute.

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

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

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

+  // Limit to 3 keys maximum for usability
+  if (recordedKeys.length >= 3) {
+    return;
+  }

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

81-88: Add a visual indication when no keys are present

When no keys are recorded or provided as current keys, there's no visual indication to the user.

<div className="flex gap-2">
+  {(recordedKeys.length > 0 ? recordedKeys : currentKeys).length === 0 && (
+    <div className="text-muted-foreground italic">
+      {t('pages.settings.shortcuts.noKeysAssigned' as MessageKey)}
+    </div>
+  )}
  {(recordedKeys.length > 0 ? recordedKeys : currentKeys).map((key) => (
    <kbd
      key={key}
docker-compose.yaml (1)

36-36: Remove trailing whitespace

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

-      - 8079:80
-      
+      - 8079:80
🧰 Tools
🪛 YAMLlint (1.35.1)

[error] 36-36: trailing spaces

(trailing-spaces)

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

254-270: Remove console.log statements from production code

While the hotkey scope management logic is sound, there are several console.log statements that should be removed before pushing to production.

  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/resend/send-early-access/route.ts (3)

56-61: Consider extracting the 'from' email to an environment variable.

The sender email address is currently hardcoded. For better flexibility and configuration management, consider extracting this to an environment variable.

+// Get sender email from environment variable with fallback
+const senderEmail = process.env.RESEND_SENDER_EMAIL || 'onboarding@0.email';
+const senderName = process.env.RESEND_SENDER_NAME || '0.email';

 const response = await resend.emails.send({
-  from: '0.email <onboarding@0.email>',
+  from: `${senderName} <${senderEmail}>`,
   to: email,
   subject: subject,
   html: content,
 });

63-71: Simplify conditional checks with optional chaining.

The nested property checks can be simplified using optional chaining for better readability.

-if (response && response.data && response.data.id) {
+if (response?.data?.id) {
   console.log(`Successfully sent email to ${email}`);
   successfulEmails.push(email);
   results.push({ email, success: true, response });
 } else {
   console.error(`Unexpected response format for ${email}:`, response);
   failedEmails.push(email);
   results.push({ email, success: false, error: 'Unexpected response format' });
 }
🧰 Tools
🪛 Biome (1.9.4)

[error] 63-63: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


79-81: Consider making the rate limit delay configurable.

The delay between email sends is hardcoded to 500ms. For better flexibility, especially in different environments, consider making this configurable.

+// Rate limit delay in milliseconds - configurable via environment variable
+const rateLimitDelay = parseInt(process.env.RESEND_RATE_LIMIT_DELAY || '500', 10);

 // Add a small delay between requests to avoid rate limiting
 if (i < emails.length - 1) {
-  await new Promise(resolve => setTimeout(resolve, 500));
+  await new Promise(resolve => setTimeout(resolve, rateLimitDelay));
 }
apps/mail/components/mail/mail-list.tsx (2)

495-496: Remove TODO comment from production code.

There is a TODO comment in the production code that should be addressed or removed.

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

557-562: Remove console log statements from production code.

There are console log statements that appear to be for debugging purposes. These should be removed from production code.

       onMouseEnter={() => {
-        console.log('[MailList] Mouse Enter - Enabling scope: mail-list');
         enableScope('mail-list');
       }}
       onMouseLeave={() => {
-        console.log('[MailList] Mouse Leave - Disabling scope: mail-list');
         disableScope('mail-list');
       }}
apps/mail/app/api/resend/add-to-audience/route.ts (1)

74-74: Use optional chaining for more concise code

Replace the multiple condition checks with optional chaining for better readability.

-if (response && response.data && response.data.id) {
+if (response?.data?.id) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 74-74: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

62-68: serverHotkeysLoaded set before fetch completes => premature sync attempts
Because the flag is flipped to true before awaiting fetchServerHotkeys(), later calls to saveAllHotkeys() will try to POST even if the fetch failed (e.g., offline).
Consider toggling the flag only after a successful fetch to avoid unnecessary network noise and misleading console errors.

-        if (!serverHotkeysLoaded) {
-          serverHotkeysLoaded = true;
-          const serverHotkeys = await fetchServerHotkeys();
+        if (!serverHotkeysLoaded) {
+          const serverHotkeys = await fetchServerHotkeys();
+          serverHotkeysLoaded = !!serverHotkeys;   // true only if we got data
apps/mail/app/(routes)/settings/shortcuts/page.tsx (1)

40-52: Reset‑to‑defaults performs N writes instead of one
Calling saveHotkey inside a loop fires N separate transactions and N POSTs. A bulk helper (e.g., a public saveAllHotkeys) would cut IO dramatically and remove race conditions.

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

205-212: Potential double‑handling of arrow keys (repeat & non‑repeat)
useHotkeys already registers handlers for single key presses, while the manual keydown listener below handles event.repeat logic. When a key is held, both listeners fire:

  1. the useHotkeys handler on the initial press,
  2. the custom listener on every repeat.

This works but can lead to visible jump‑backs if state updates collide. Consider consolidating the repeat logic into the same handler or throttling in one place only.

apps/mail/app/(full-width)/early-access/early-access.tsx (3)

109-123: Shadowing and stale state risk in handleCustomEmails

selectedUsers declared inside the function shadows the state setter’s variable name.
Although currently harmless, this pattern is easy to mis‑read and can lead to bugs when refactoring.

-const selectedUsers = earlyAccessUsers.filter(user => 
-  emails.includes(user.email)
-);
+const matches = earlyAccessUsers.filter((user) =>
+  emails.includes(user.email),
+);

268-391: Hard‑coding a 120‑line HTML template in a string literal hurts bundle size & DX

Embedding the whole email HTML inflates the client bundle (this component ships to the browser).
Consider:
• Moving the template to a server‑side API or a static file.
• Sending only a template key from the client; let the API compose the email body.

This reduces bundle bloat and keeps presentational email markup out of the React tree.


490-497: Table semantics broken by flex on <tr>

Applying Flexbox to table rows can break accessibility and layout in some browsers.
Prefer styling individual <td> elements or wrap rows in <div role="row"> instead of mixing table semantics with flex layouts.

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

92-104: Unconditional DB write on every mount

hotkeysDB.saveHotkey(shortcut) runs even when the user never edited a shortcut, resulting in redundant IndexedDB writes.
Guard the save behind a flag or compare versions before persisting.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c4172c2 and 45dca02.

⛔ Files ignored due to path filters (2)
  • apps/mail/public/email.png is excluded by !**/*.png
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (42)
  • Dockerfile (1 hunks)
  • apps/mail/.env.example (1 hunks)
  • apps/mail/app/(full-width)/early-access/early-access.tsx (1 hunks)
  • apps/mail/app/(full-width)/early-access/page.tsx (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/auth/early-access/route.ts (2 hunks)
  • apps/mail/app/api/resend/add-to-audience/route.ts (1 hunks)
  • apps/mail/app/api/resend/send-early-access/route.ts (1 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/home/hero.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 (9)
apps/mail/app/(routes)/layout.tsx (3)
apps/mail/components/providers/hotkey-provider-wrapper.tsx (1)
  • HotkeyProviderWrapper (14-24)
apps/mail/components/context/command-palette-context.tsx (1)
  • CommandPaletteProvider (210-216)
apps/mail/lib/idb.ts (1)
  • dexieStorageProvider (25-114)
apps/mail/lib/hotkeys/compose-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/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/app/(full-width)/early-access/page.tsx (3)
packages/db/src/index.ts (1)
  • db (17-17)
packages/db/src/schema.ts (2)
  • earlyAccess (68-75)
  • user (7-17)
apps/mail/app/(full-width)/early-access/early-access.tsx (1)
  • EarlyAccessClient (86-552)
apps/mail/components/mail/reply-composer.tsx (1)
apps/mail/types/index.ts (1)
  • Sender (32-35)
apps/mail/components/mail/mail-list.tsx (4)
apps/mail/hooks/use-threads.ts (1)
  • useThread (131-144)
apps/mail/lib/utils.ts (1)
  • cn (51-51)
apps/mail/lib/email-utils.client.tsx (1)
  • highlightText (59-77)
apps/mail/components/ui/tooltip.tsx (1)
  • TooltipContent (59-59)
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 (2)
apps/mail/config/shortcuts.ts (2)
  • Shortcut (3-10)
  • keyboardShortcuts (12-34)
apps/mail/lib/hotkeys/hotkeys-db.ts (1)
  • hotkeysDB (146-146)
🪛 Biome (1.9.4)
apps/mail/app/api/resend/add-to-audience/route.ts

[error] 74-74: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

apps/mail/app/api/resend/send-early-access/route.ts

[error] 63-63: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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)

🪛 YAMLlint (1.35.1)
docker-compose.yaml

[error] 36-36: trailing spaces

(trailing-spaces)

🔇 Additional comments (59)
apps/mail/components/home/hero.tsx (1)

110-110: Verify the intention of the environment logic inversion

The conditional logic has been inverted from process.env.NODE_ENV === 'development' to process.env.NODE_ENV !== 'development'. This changes which UI component is displayed based on the environment - the button group will now appear in production/staging instead of development.

Is this intentional? Given the PR description contains "NEVER MERGE", this might be a temporary change for testing early access features. Please confirm the intended behavior.

apps/mail/.env.example (1)

26-26: Added configuration for Resend audience management

The new RESEND_AUDIENCE_ID environment variable supports the early access email features introduced in this PR.

Since the PR includes new API routes for early access email management with Resend, this addition is appropriate for configuration purposes.

apps/mail/package.json (1)

95-95: Added react-hotkeys-hook for improved keyboard shortcut management

Adding the react-hotkeys-hook library supports the migration from a custom hotkey hook to a more comprehensive hotkey management system.

This addition aligns with the global refactoring of keyboard shortcut handling described in the AI summary. The version constraint (^5.0.1) follows standard practice, allowing minor version updates.

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

158-165: New database migration added for user hotkeys.

This migration entry (0022_round_violations) appears to introduce the mail0_user_hotkeys table for supporting user-specific keyboard shortcut configurations, which aligns with the overall PR focus on implementing a comprehensive hotkeys system.

i18n.json (3)

21-22: Added Farsi (Persian) localization support.

The addition of "fa" to the locale targets list enables Persian language support in the application, expanding accessibility to Farsi-speaking users.


27-27: Reformatted buckets.json.include array.

The formatting change simplifies the include pattern to a single line, maintaining the same functionality while improving readability.


30-31: Added missing newline at end of file.

Adding a newline at the end of the file follows best practices for text files and prevents potential issues with some version control systems and text editors.

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

175-175: Enabled shortcuts settings page in navigation.

Commenting out the disabled: true property makes the keyboard shortcuts settings page accessible to users, which aligns with the PR's focus on implementing a comprehensive user hotkeys system.

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

2-17: LGTM: Added Farsi language support

The addition of Farsi language with code 'fa' looks good and aligns with the PR's purpose of adding Persian language support. Also noting the consistent style change to single quotes.


24-24: LGTM: Style consistency

Properly updated to single quotes to maintain consistency with other string literals.


28-30: LGTM: Arrow function formatting

Properly reformatted arrow function with parentheses around the parameter and trailing commas for better code consistency.

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

2-3: LGTM: Added hotkey components imports

Correctly imported the new hotkey-related components to support the keyboard shortcuts system.


7-11: LGTM: Implemented HotkeyProviderWrapper

Properly wrapped the layout with the new hotkey provider and added the GlobalHotkeys component. This correctly implements the new architecture for keyboard shortcuts using react-hotkeys-hook.

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

50-57: LGTM: Added userHotkeys table for persistent shortcuts

The new userHotkeys table is well-designed with:

  • userId as primary key with reference to the user table
  • shortcuts as a JSONB field for flexible storage
  • Proper timestamp tracking fields

This provides the necessary database structure for the new user-specific keyboard shortcut system.

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

402-405: LGTM: Added localization strings for hotkey functionality

The new localization strings support the hotkey recording and management UI:

  • "recordHotkey" for initiating the recording process
  • "pressKeys" and "releaseKeys" for guiding users during recording
  • "selectAll" for the select all shortcut action

These additions properly support the new keyboard shortcuts system.

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

4-4: LGTM! Import added for new hotkey provider

This import enables the new hotkey management system to be integrated at the application layout level.


10-25: Hotkey management architecture improvement

Wrapping the entire application with HotkeyProviderWrapper ensures consistent keyboard shortcut handling throughout the app. This hierarchical approach aligns with best practices for context-based feature management in React.

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

12-12: LGTM! Migration to react-hotkeys-hook library

Switching from a custom hook to a well-maintained library is a good improvement for stability and maintainability.


95-96: LGTM! Added trailing comma for consistent style

This minor formatting change improves code consistency.


138-156: LGTM! Migrated modifier key detection to react-hotkeys-hook

The implementation correctly handles the migration from the custom hook to the external library for modifier key detection.


158-204: LGTM! Migrated marking functions to react-hotkeys-hook

The implementation successfully migrates the mark as read/unread hotkey functionality to the new library.


220-242: Improved hotkey implementation with event prevention

The migration adds proper event prevention which was likely missing in the previous implementation, preventing default browser behaviors when using these shortcuts.


232-242:

Details

❓ Verification inconclusive

Verify Meta+n and Control+n shortcut behavior

These shortcuts currently perform the same action as Meta+a and Control+a (selecting all items), which seems potentially redundant or confusing. Typically, 'n' shortcuts are used for "new" actions rather than "select all".


🏁 Script executed:

#!/bin/bash
# Check if there are other uses of Meta+n or Control+n shortcuts in the codebase
echo "Checking for Meta+n usage:"
rg -i "meta\+n|cmd\+n" --type tsx --type ts

echo "Checking for Control+n usage:"
rg -i "control\+n|ctrl\+n" --type tsx --type ts

echo "Checking keyboard shortcut config for 'n' key:"
rg "'n'" ./apps/mail/config/shortcuts.ts

Length of output: 441


I wasn’t able to confirm any existing Meta+n or Control+n bindings with the previous patterns. Let’s broaden the search:


🏁 Script executed:

#!/bin/bash
# Search for explicit useHotkeys bindings
echo "Looking for useHotkeys calls with Meta+n or Control+n..."
rg -n "useHotkeys.*['\"]Meta\+n['\"]" || echo "No useHotkeys binding for Meta+n"
rg -n "useHotkeys.*['\"]Control\+n['\"]" || echo "No useHotkeys binding for Control+n"

# Search for any literal references to Meta+n or Ctrl+n
echo "Looking anywhere else for Meta+n or Control+n literals..."
rg -n "Meta\+n" || echo "No Meta+n literals found"
rg -n "Control\+n" || echo "No Control+n literals found"

# Find and inspect shortcut config files
echo "Locating shortcuts config files..."
fd -e ts -e js shortcuts

for file in $(fd -e ts -e js shortcuts); do
  echo "--- Scanning $file for 'n' key definitions ---"
  rg -n "['\"]n['\"]" "$file" || echo "  No 'n' entries in $file"
done

Length of output: 1273


Verify Meta+n and Control+n shortcut assignments

I confirmed these are the only occurrences of Meta+n and Control+n bindings in the codebase, and there’s no global “n” shortcut defined elsewhere. They currently duplicate the select‑all behavior already mapped to Meta+a / Ctrl+a, which can be confusing since “n” is typically used for “new” actions.

Please review the following location and confirm whether this is intentional or if you’d prefer to:

  • Keep “n” for select‑all (and accept the UX deviation),
  • Switch to a more conventional key (e.g. Meta+a / Ctrl+a) for select‑all, or
  • Reserve “n” for a “new draft” shortcut instead.

• apps/mail/components/draft/drafts-list.tsx (232–242)

   useHotkeys('Meta+n', async (event) => {
     event.preventDefault();
     resetSelectMode();
     selectAll();
   });

   useHotkeys('Control+n', async (event) => {
     event.preventDefault();
     resetSelectMode();
     selectAll();
   });
apps/mail/app/api/auth/early-access/route.ts (2)

70-71: LGTM! Status code formatting consistency

The formatting change for the status code in the response is a minor style improvement.


84-85: LGTM! Status code formatting consistency

The formatting change for the status code in the response is a minor style improvement.

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

14-14: Verify the synthetic KeyboardEvent implementation

Creating a synthetic KeyboardEvent doesn't actually dispatch it to any DOM element, so this won't trigger any Escape key handlers elsewhere in the application.

If the intention is to trigger an Escape key handler elsewhere, you need to dispatch the event:

-    closeView: () => closeView(new KeyboardEvent('keydown', { key: 'Escape' })),
+    closeView: () => {
+      const event = new KeyboardEvent('keydown', { key: 'Escape' });
+      closeView(event);
+      document.dispatchEvent(event); // Dispatch to document or a specific element
+    },
packages/db/migrations/0022_round_violations.sql (1)

1-6: LGTM - Table structure is sound

The table structure for storing user hotkeys is well-designed with appropriate columns for user identification, JSON data storage, and timestamp tracking.

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

16-16: LGTM - Good choice of initially active scopes

Setting 'global' as the initially active scope is appropriate since it handles application-wide shortcuts.

apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx (1)

71-72:

Details

❌ Incorrect review comment

Type assertion may indicate a missing translation

Using type assertion (as MessageKey) suggests the translation key might not be properly defined in your type system.


🏁 Script executed:

#!/bin/bash
# Check if the translation key exists in the localization files
echo "Checking for translation key 'pages.settings.shortcuts.actions.recordHotkey'"
rg -n "recordHotkey" --glob="*.json" apps/mail/messages/

Length of output: 285


🏁 Script executed:

#!/bin/bash
# Search for the translation key across all JSON files in the repo
rg -n "pages.settings.shortcuts.actions.recordHotkey" --glob="*.json"
rg -n "recordHotkey" --glob="*.json"

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Show nested context for "recordHotkey" in the English locale to confirm its JSON structure
rg -n '"recordHotkey"' -A5 -B5 --glob="*.json" apps/mail/locales/en.json

Length of output: 537


🏁 Script executed:

#!/bin/bash
# Locate the MessageKey type definition(s)
rg -n "type MessageKey" -R .

# If found, search within for the specific key
# Adjust the path below based on the first command's output
# e.g., rg -n "recordHotkey" path/to/message-keys.d.ts

Length of output: 435


🏁 Script executed:

#!/bin/bash
# Search for MessageKey definition across TypeScript files
rg -n "type MessageKey" -g "*.ts" -g "*.tsx" -g "*.d.ts"
rg -n "MessageKey" -g "*.ts" -g "*.tsx" -g "*.d.ts"

Length of output: 2152


Ignore missing translation warning for recordHotkey
The key pages.settings.shortcuts.actions.recordHotkey is defined in apps/mail/locales/en.json (see line 402). The as MessageKey assertion here is merely satisfying the TypeScript signature of t() for inline string literals and matches existing patterns elsewhere in the codebase (e.g., in nav‑main.tsx). No change is needed.

Likely an incorrect or invalid review comment.

docker-compose.yaml (2)

32-32: Good improvement for security

Changing from a hardcoded token to an environment variable improves security by allowing secure token management outside the codebase.


37-52: LGTM: Docker service configuration looks good

The new app service is well structured with appropriate container name, build configuration, port mapping, dependencies, and environment variables. Using environment variables for sensitive connection strings is a good security practice.

apps/mail/app/(full-width)/early-access/page.tsx (3)

7-16: Looks good: Proper data fetching with normalization

The function correctly queries early access users and normalizes the isEarlyAccess flag to ensure consistent data structure.


18-60: Well-implemented server action with proper error handling

This server action follows best practices:

  • Input validation
  • Comprehensive error handling with logging
  • Properly structured response objects
  • Cache revalidation after database updates

The SQL query is safely constructed using parameterized queries with the SQL tag function to prevent SQL injection.


62-73: Effective server component implementation

The component properly fetches data and passes it to the client component with appropriate props. The structure is clean and follows the server component pattern well.

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

55-55: Good refactoring to standard hotkeys library

The import and usage of useHotkeysContext from 'react-hotkeys-hook' is a good move toward using a standard library instead of custom implementations.

Also applies to: 221-221


272-274: Good use of useCallback for event handler

Properly memoizing the handler function with useCallback prevents unnecessary re-renders and follows React best practices.

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

1-432:

Details

✅ Verification successful

Well-structured localization file for Farsi

The new Farsi localization file follows the correct structure with all the necessary translation keys. The empty strings serve as placeholders for future translations.

Consider:

  1. Adding a plan or timeline for completing the translations
  2. Using a translation management system to help translators fill in these values

To ensure this file covers all the required keys, run this script to compare with an existing complete locale file:


🏁 Script executed:

#!/bin/bash
# Check if the new Farsi locale has all the keys from the English locale

# Find the English locale file
ENGLISH_FILE=$(find apps/mail/locales -name "en*.json" | head -1)

if [ -z "$ENGLISH_FILE" ]; then
  echo "Error: Could not find English locale file"
  exit 1
fi

echo "Comparing Farsi locale with $ENGLISH_FILE..."

# Use jq to extract keys from both files and compare
ENGLISH_KEYS=$(jq -r 'path(..) | select(length > 0) | map(tostring) | join(".")' "$ENGLISH_FILE" | sort)
FARSI_KEYS=$(jq -r 'path(..) | select(length > 0) | map(tostring) | join(".")' "apps/mail/locales/fa.json" | sort)

# Compare the keys
DIFF=$(diff <(echo "$ENGLISH_KEYS") <(echo "$FARSI_KEYS"))

if [ -z "$DIFF" ]; then
  echo "✅ All keys from English locale are present in Farsi locale"
else
  echo "❌ Missing or extra keys in Farsi locale:"
  echo "$DIFF"
fi

Length of output: 50839


Farsi locale keys verified and structurally complete

✅ All keys from apps/mail/locales/en.json are present in apps/mail/locales/fa.json.

Next steps (optional improvements):

  • Define a plan or timeline for translating the placeholder strings.
  • Integrate a translation management system (e.g., Crowdin, Lokalise) to streamline future translations.
apps/mail/config/shortcuts.ts (1)

12-33: Well structured keyboard shortcuts with clear organization by scope.

The keyboard shortcuts are well-defined with a clear type structure and organized by scope. The use of platform-agnostic 'mod' key is a good practice.

One suggestion for future improvement might be to group shortcuts by scope in the code for better readability and maintenance, especially as the list grows:

-export const keyboardShortcuts: Shortcut[] = [
-  { keys: ["c"], action: "newEmail", type: "single", description: "Compose new email", scope: "global" },
+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" },
+  { keys: ["g", "i"], action: "inbox", type: "combination", description: "Go to inbox", scope: "global" },
+  { keys: ["g", "t"], action: "sentMail", type: "combination", description: "Go to sent mail", scope: "global" },
+  { keys: ["/"], action: "search", type: "single", description: "Search", scope: "global" },
+  { keys: ["z"], action: "undoLastAction", type: "single", description: "Undo last action", scope: "global" },
+  { keys: ["?"], action: "helpWithShortcuts", type: "single", description: "Show keyboard shortcuts", scope: "global" },
+  
+  // Compose shortcuts
   { keys: ["mod", "Enter"], action: "sendEmail", type: "combination", description: "Send email", scope: "compose" },
+  
+  // Thread display shortcuts
   { keys: ["r"], action: "reply", type: "single", description: "Reply to email", scope: "thread-display" },
   { keys: ["a"], action: "replyAll", type: "single", description: "Reply all", scope: "thread-display" },
   { keys: ["f"], action: "forward", type: "single", description: "Forward email", scope: "thread-display" },
-  { keys: ["g", "d"], action: "goToDrafts", type: "combination", description: "Go to drafts", scope: "global" },
-  { keys: ["g", "i"], action: "inbox", type: "combination", description: "Go to inbox", scope: "global" },
-  { keys: ["g", "t"], action: "sentMail", type: "combination", description: "Go to sent mail", scope: "global" },
+  { keys: ["mod", "p"], action: "printEmail", type: "combination", description: "Print email", scope: "thread-display" },
+  { keys: ["i"], action: "viewEmailDetails", type: "single", description: "View email details", scope: "thread-display" },
+  
+  // Mail list shortcuts
   { keys: ["#"], action: "delete", type: "single", description: "Delete email", scope: "mail-list" },
-  { keys: ["/"], action: "search", type: "single", description: "Search", scope: "global" },
   { keys: ["u"], action: "markAsUnread", type: "single", description: "Mark as unread", scope: "mail-list" },
   { keys: ["m"], action: "muteThread", type: "single", description: "Mute thread", scope: "mail-list" },
-  { keys: ["mod", "p"], action: "printEmail", type: "combination", description: "Print email", scope: "thread-display" },
   { keys: ["e"], action: "archiveEmail", type: "single", description: "Archive email", scope: "mail-list" },
   { keys: ["!"], action: "markAsSpam", type: "single", description: "Mark as spam", scope: "mail-list" },
   { keys: ["v"], action: "moveToFolder", type: "single", description: "Move to folder", scope: "mail-list" },
-  { keys: ["z"], action: "undoLastAction", type: "single", description: "Undo last action", scope: "global" },
-  { keys: ["i"], action: "viewEmailDetails", type: "single", description: "View email details", scope: "thread-display" },
   { keys: ["o"], action: "expandEmailView", type: "single", description: "Expand email view", scope: "mail-list" },
-  { keys: ["?"], action: "helpWithShortcuts", type: "single", description: "Show keyboard shortcuts", scope: "global" },
   { keys: ["mod", "a"], action: "selectAll", type: "combination", description: "Select all emails", scope: "mail-list", preventDefault: true },
 ];
apps/mail/components/mail/mail-list.tsx (2)

154-154: The custom event emission for emailHover integrates well with the new hotkey system.

The change to dispatch a custom event when mouse leaves an email thread is a good approach for communicating with other components that need to react to hover state changes.


33-34: Good refactoring to use react-hotkeys-hook package.

The refactoring to use the useHotkeysContext hook from react-hotkeys-hook is a good approach for managing keyboard shortcuts in a more structured and maintainable way.

apps/mail/app/api/resend/add-to-audience/route.ts (6)

1-6: Well-structured initialization approach

The file properly imports the necessary dependencies and includes a clear comment about environment variable handling strategy, which is a good practice for API routes.


7-23: Strong environmental configuration validation

The implementation wisely validates the presence of required environment variables before proceeding with API operations. The error handling includes detailed logging with appropriate status codes.


25-37: Thorough request validation

Good practice to validate the request body structure before processing, with clear error responses that include specific reasons for failure.


41-100: Well-implemented sequential processing with rate limiting

The email processing loop intelligently handles each email individually with proper error isolation. The 500ms delay between requests is an excellent approach to prevent rate limiting issues with the Resend API.

🧰 Tools
🪛 Biome (1.9.4)

[error] 74-74: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


102-124: Comprehensive response structure

The response includes detailed information about both successful and failed operations, which is excellent for client-side error handling and debugging.


125-149: Robust error handling

The comprehensive try-catch with detailed error reporting, including stack traces and timestamp, follows best practices for API error handling. The attempt to stringify errors for diagnostics is particularly helpful for troubleshooting.

apps/mail/components/mail/reply-composer.tsx (5)

24-44: Improved import organization

The imports have been reorganized for better readability, with React hooks consolidated into a single import statement and the new useHotkeysContext hook added from react-hotkeys-hook.


161-161: Added hotkey context integration

The component now integrates with the hotkey system by using the useHotkeysContext hook, providing access to enableScope and disableScope functions.


270-282: Improved code formatting in recipient mapping

The formatting of the recipient mapping functions has been improved for better readability while maintaining the same functionality.


504-512: More readable JSON message structure

The JSON structure for checking empty messages has been reformatted for better readability with consistent indentation.


924-937: Added hotkey scope management

The new effect hook properly manages the "compose" hotkey scope, enabling it when the composer is open and disabling it when closed or on cleanup. The console logging statements are useful for debugging the hotkey lifecycle.

This integration with the new hotkey system allows for more granular control of keyboard shortcuts based on the UI context.

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

2-44: Reorganized import statements

The imports have been reorganized with dropdown-related components grouped together and the new useHotkeysContext hook added from react-hotkeys-hook.


103-103: Added hotkey context integration

The component now integrates with the hotkey system by destructuring the necessary functions from useHotkeysContext.


464-472: Added compose scope management

The component now properly manages the "compose" hotkey scope with a dedicated effect hook. This enables contextual keyboard shortcuts when the email composer is active, improving user experience.


521-616: Improved JSX structure and formatting

The JSX structure has been improved with better indentation and consistent class naming. This makes the code more readable without changing functionality.


679-736: Enhanced contact selection UI components

The email recipient input fields have been refactored with more consistent styling and structure, improving maintainability while preserving functionality.

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

585-635: Well-designed user hotkeys table schema

The new mail0_user_hotkeys table is properly structured to support the hotkey customization features:

  • Uses user_id as primary key with a foreign key to mail0_user
  • Employs jsonb type for the shortcuts field, which allows for flexible storage of keyboard shortcut configurations
  • Includes standard timestamp fields for tracking creation and updates

This table design complements the frontend hotkey management changes implemented in the React components.


1-763:

Details

❓ Verification inconclusive

Verify the migration deployment process

This is a substantial database schema snapshot that includes multiple tables. Before deploying to production, ensure you have a rollback strategy in place and have tested the migration in a staging environment.


🏁 Script executed:

#!/bin/bash
# Check if there are existing hotkey-related tables or columns in the database
# that might conflict with this migration

echo "Checking for existing hotkey-related database objects..."
if command -v pg_dump &> /dev/null; then
  echo "This would normally check your database schema, but we're in a sandbox"
  echo "In production, you would run:"
  echo "  pg_dump -s -t '*hotkey*' \$DATABASE_URL"
  echo "  pg_dump -s | grep -i 'hotkey'"
else
  echo "PostgreSQL client not available in sandbox"
fi

# Check for migration-related files
echo "Looking for related migration files:"
fd --type f --glob "*hotkey*.sql" --exec echo "Found migration file: {}"

Length of output: 396


Ensure a Robust Migration Deployment & Rollback Plan

Before rolling out this extensive schema snapshot, please verify the following:

  • You’ve tested the migration (and its down‐path) in a staging environment.
  • A full schema‐and‐data backup is in place for production.
  • You have an automated or well‐documented rollback (down‐migration) procedure.
  • All foreign keys, unique constraints and defaults behave as expected post‐migration.
  • No conflicting “hotkey” objects or prior migrations exist (no *hotkey*.sql files were found in the repo).
apps/mail/hooks/use-mail-navigation.ts (1)

278-285: Missing containerRef.current in hook dependency list may skip re‑binding on ref changes
useEffect depends on containerRef (an object that is stable) instead of containerRef.current. If a new container is assigned, the scroll logic might still target the old element.

Would you confirm whether containerRef ever changes? If yes, include containerRef.current in the dependency array or refactor to use a callback ref.

Comment on lines +9 to +10
import { useTranslations } from 'next-intl';
import { createContext, useContext } 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

Added useTranslations import but not using it.

The useTranslations hook has been imported but isn't being used anywhere in the component. Either remove the unused import or implement localization for the hardcoded strings in the component.

-import { useTranslations } from 'next-intl';

Or implement it for the hardcoded "Ask Zero a question..." string:

+const t = useTranslations();
...
-                Ask Zero a question...
+                {t('ai.askZeroQuestion')}

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

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.

⚠️ Potential issue

Empty handlers object needs implementation

The ComposeHotkeys component filters for compose-scoped shortcuts but doesn't provide any handlers to be invoked when those shortcuts are triggered. The empty handlers object means that none of the filtered shortcuts will actually perform any actions.

Consider implementing the necessary handlers, especially for the "sendEmail" action which is defined in the shortcuts configuration. For example:

export function ComposeHotkeys() {
  const scope = 'compose';

-  const handlers = {};
+  const handlers = {
+    sendEmail: () => {
+      // Implementation for sending email
+      console.log('Send email shortcut triggered');
+    }
+  };

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

  useShortcuts(composeShortcuts, handlers, { scope });

  return null;
}
📝 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
export function ComposeHotkeys() {
const scope = 'compose';
const handlers = {};
const composeShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
useShortcuts(composeShortcuts, handlers, { scope });
return null;
}
export function ComposeHotkeys() {
const scope = 'compose';
const handlers = {
sendEmail: () => {
// Implementation for sending email
console.log('Send email shortcut triggered');
}
};
const composeShortcuts = keyboardShortcuts.filter((shortcut) => shortcut.scope === scope);
useShortcuts(composeShortcuts, handlers, { scope });
return null;
}

Comment on lines +6 to +8
const closeView = (event: KeyboardEvent) => {
event.preventDefault();
};
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

The closeView function is incomplete

The closeView function calls preventDefault() but doesn't actually implement any closing functionality. This appears to be incomplete since the synthetic event created in the handler (line 14) won't do anything useful by itself.

Consider implementing the actual close functionality:

const closeView = (event: KeyboardEvent) => {
  event.preventDefault();
+  // Implement thread view closing logic here, e.g.:
+  // document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
};
📝 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();
};
const closeView = (event: KeyboardEvent) => {
event.preventDefault();
// Implement thread view closing logic here, e.g.:
// document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
};

Comment on lines +15 to +17
search: () => {
console.log('search');
},
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

Implement search functionality

The search handler only logs to the console but doesn't implement actual search functionality.

Consider implementing actual search functionality:

    search: () => {
-      console.log('search');
+      // Implement search functionality
+      const searchInput = document.querySelector('[data-testid="search-input"]');
+      if (searchInput instanceof HTMLElement) {
+        searchInput.focus();
+      } else {
+        // Fallback if no search input is found
+        router.push('/mail/search');
+      }
    },

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

Comment on lines +11 to +19
const handlers = {
goToDrafts: () => router.push('/mail/draft'),
inbox: () => router.push('/mail/inbox'),
sentMail: () => router.push('/mail/sent'),
search: () => {
console.log('search');
},
newEmail: () => router.push('/mail/compose'),
};
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

Add error handling for navigation

The router navigation calls have no error handling, which could lead to unhandled exceptions.

Add try/catch blocks for error handling:

  const handlers = {
-    goToDrafts: () => router.push('/mail/draft'),
-    inbox: () => router.push('/mail/inbox'),
-    sentMail: () => router.push('/mail/sent'),
+    goToDrafts: () => {
+      try {
+        router.push('/mail/draft');
+      } catch (error) {
+        console.error('Failed to navigate to drafts:', error);
+      }
+    },
+    inbox: () => {
+      try {
+        router.push('/mail/inbox');
+      } catch (error) {
+        console.error('Failed to navigate to inbox:', error);
+      }
+    },
+    sentMail: () => {
+      try {
+        router.push('/mail/sent');
+      } catch (error) {
+        console.error('Failed to navigate to sent mail:', error);
+      }
+    },
    search: () => {
      console.log('search');
    },
-    newEmail: () => router.push('/mail/compose'),
+    newEmail: () => {
+      try {
+        router.push('/mail/compose');
+      } catch (error) {
+        console.error('Failed to navigate to compose:', error);
+      }
+    },
  };

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

Comment on lines +75 to +76
function Shortcut({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {
const [isRecording, setIsRecording] = useState(false);
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

Name collision with imported Shortcut type triggers no‑redeclare rule
A value‑level function Shortcut shadows the imported type Shortcut, which confuses static‑analysis tools and future readers. Rename the component to something like ShortcutRow (or re‑export the type under a different alias) to keep the namespaces distinct.

-function Shortcut({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {
+function ShortcutRow({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {

Remember to update the JSX usage:

-            <Shortcut 
+            <ShortcutRow
📝 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);
function ShortcutRow({ children, keys, action }: { children: ReactNode; keys: string[]; action: string }) {
const [isRecording, setIsRecording] = useState(false);
🧰 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 101 to 105
const selectRandomUsers = () => {
const nonEarlyAccessUsers = earlyAccessUsers.filter((user) => user.isEarlyAccess);
const shuffled = [...nonEarlyAccessUsers].sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, Math.min(5000, shuffled.length));
setSelectedUsers(selected);
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

Incorrect filtering selects users that ALREADY have early‑access

nonEarlyAccessUsers is intended to hold non‑granted users, but the filter predicate currently keeps those with isEarlyAccess === true.
As a result, Randomize Early Access may pick only users who are already in the programme, producing an empty selection and confusing counts.

-const nonEarlyAccessUsers = earlyAccessUsers.filter((user) => user.isEarlyAccess);
+const nonEarlyAccessUsers = earlyAccessUsers.filter((user) => !user.isEarlyAccess);
📝 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 selectRandomUsers = () => {
const nonEarlyAccessUsers = earlyAccessUsers.filter((user) => user.isEarlyAccess);
const shuffled = [...nonEarlyAccessUsers].sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, Math.min(5000, shuffled.length));
setSelectedUsers(selected);
const selectRandomUsers = () => {
- const nonEarlyAccessUsers = earlyAccessUsers.filter((user) => user.isEarlyAccess);
+ const nonEarlyAccessUsers = earlyAccessUsers.filter((user) => !user.isEarlyAccess);
const shuffled = [...nonEarlyAccessUsers].sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, Math.min(5000, shuffled.length));
setSelectedUsers(selected);

Comment on lines +427 to +456
return (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Confirm Early Access Selection</DialogTitle>
<DialogDescription>
You are about to grant early access to {selectedUsers.length} users. Please review the
list below.
</DialogDescription>
</DialogHeader>
<div className="max-h-[400px] overflow-auto rounded-md border p-2">
{selectedUsers.map((user) => (
<div key={user.id} className="border-b py-2 last:border-0">
<p className="text-sm font-medium">{user.email}</p>
<p className="text-muted-foreground text-xs">
Signed up: {format(new Date(user.createdAt), 'MMM d, yyyy')}
</p>
</div>
))}
</div>
<DialogFooter className="flex justify-between sm:justify-between">
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleConfirm} disabled={isUpdating}>
Send Early Access Emails
</Button>
</DialogFooter>
</DialogContent>
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

Duplicate dialog implementation – dead code & maintainability

A fully‑featured ConfirmationDialog component is declared at the top of the file but never reused; instead, the very same markup is duplicated here.
Keeping both copies in sync will be error‑prone.

Refactor to reuse ConfirmationDialog:

-<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
-  …duplicated markup…
-</Dialog>
+<ConfirmationDialog
+  isOpen={isDialogOpen}
+  onClose={() => setIsDialogOpen(false)}
+  selectedUsers={selectedUsers}
+  onConfirm={handleConfirm}
/>

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

Comment on lines +138 to +144
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.

⚠️ Potential issue

Hooks are called inside a loop – violates the Rules of Hooks

useShortcut is invoked inside shortcuts.forEach.
If the shortcuts array length or ordering changes between renders, React’s hook call order will differ, leading to runtime errors.

Refactor to compute a stable list once (e.g., in useMemo) and map deterministically, or ask consumers to wrap each call explicitly.

Example fix:

export function useShortcuts(
  shortcuts: Shortcut[],
  handlers: Record<string, () => void>,
  options: Partial<HotkeyOptions> = {},
) {
  shortcuts
    .map((sc) => [sc, handlers[sc.action]] as const)
    .filter(([, h]) => h)
    .forEach(([sc, h]) => useShortcut(sc, h!, options));
}

Even better: enforce shortcuts to be static via useRef or document that it must not change.

Comment on lines +33 to +40
if (keys.length > 1) {
return keys.map(mapKey).join('+');
}

const firstKey = keys[0];
if (!firstKey) return '';
return mapKey(firstKey);
};
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

formatKeys joins multi‑key shortcuts with “+” – may mis‑interpret sequences

react-hotkeys-hook distinguishes between chords (ctrl+a) and sequences (g d).
Blindly converting every multi‑key array to 'key1+key2' disallows sequences such as "g d" used in Gmail‑style shortcuts.
Consider inspecting shortcut.type (already present in Shortcut) and:

return shortcut.type === 'combination'
  ? keys.map(mapKey).join('+')   // chord
  : keys.map(mapKey).join(' ')   // sequence

This preserves the original design in keyboardShortcuts.

@MrgSub MrgSub closed this May 9, 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