Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
588ab02
add: add dockerfile
Muhammad-Owais-Warsi Apr 5, 2025
1347a8e
Merge branch 'Mail-0:staging' into add/dockerfile
Muhammad-Owais-Warsi Apr 7, 2025
373423b
add: docker-compose.yaml
Muhammad-Owais-Warsi Apr 7, 2025
cd8833b
Merge branch 'add/dockerfile' of https://github.com/Muhammad-Owais-Wa…
Muhammad-Owais-Warsi Apr 7, 2025
49acc64
cleanup
hiheyhello123 Apr 8, 2025
8dba8a9
cleanup
hiheyhello123 Apr 8, 2025
7dd0fcd
Merge branch 'hotkeys' of github.com:Mail-0/Zero into new-keybinds
ahmetskilinc Apr 9, 2025
0d4be12
update dockerfile.
Muhammad-Owais-Warsi Apr 9, 2025
e35bd3f
Merge branch 'Mail-0:main' into add/dockerfile
Muhammad-Owais-Warsi Apr 9, 2025
d1bcb68
- switched to react-hotkeys-hook
ahmetskilinc Apr 9, 2025
f9f16db
add: docker-compose.yaml
Muhammad-Owais-Warsi Apr 9, 2025
e730552
Merge branch 'staging' into add/dockerfile
Muhammad-Owais-Warsi Apr 9, 2025
85e8fbf
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 10, 2025
dd9f236
feat: add farsi support
essinn Apr 10, 2025
2398a61
Merge branch 'main' into feature/language-support-fa
MrgSub Apr 11, 2025
93eb1d2
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 11, 2025
814fc48
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 11, 2025
749cabc
Merge branch 'staging' of https://github.com/mail-0/zero into new-key…
hiheyhello123 Apr 11, 2025
4d51e20
Merge branch 'main' into feature/language-support-fa
nizzyabi Apr 11, 2025
d3c72b5
hotkeys
hiheyhello123 Apr 12, 2025
972ddd0
Merge branch 'staging' of github.com:Mail-0/Zero into new-keybinds
ahmetskilinc Apr 12, 2025
4ee76c3
feat: save hotkeys to indexedDB
ahmetskilinc Apr 12, 2025
38d85e4
remove console logs from shortcuts
ahmetskilinc Apr 12, 2025
8f687d7
fix: load all shortcuts by default
ahmetskilinc Apr 12, 2025
458b065
fix: add key for selectAll in en.json
ahmetskilinc Apr 12, 2025
8a0a833
hotkeys db and api route
ahmetskilinc Apr 13, 2025
ca1bc22
fix hotkeys db loading too many times
ahmetskilinc Apr 13, 2025
a864044
remove comments
ahmetskilinc Apr 13, 2025
03106ce
feat: update fa.json to empty string and add farsi to i18n.json
essinn Apr 14, 2025
b73d3be
Merge branch 'feature/language-support-fa' of https://github.com/essi…
essinn Apr 14, 2025
a71f0c4
Merge branch 'main' of github.com:Mail-0/Zero into new-keybinds
ahmetskilinc Apr 14, 2025
9e22918
fix: remove unused imports
ahmetskilinc Apr 14, 2025
db58b12
fix: better handling of indexdb and syncing with postgres db
ahmetskilinc Apr 14, 2025
9313985
remove comments from hotkeys files
ahmetskilinc Apr 14, 2025
14dfeed
Merge branch 'main' into new-keybinds
MrgSub Apr 17, 2025
2bdcd8d
Refactor email creation component imports and structure
MrgSub Apr 17, 2025
cffc4f6
Merge pull request #649 from Mail-0/new-keybinds
MrgSub Apr 17, 2025
870dca9
Merge branch 'staging' into feature/language-support-fa
MrgSub Apr 17, 2025
32a698d
Merge pull request #636 from essinn/feature/language-support-fa
MrgSub Apr 17, 2025
576cc73
Merge branch 'staging' into add/dockerfile
MrgSub Apr 17, 2025
c82605a
Merge pull request #591 from Muhammad-Owais-Warsi/add/dockerfile
MrgSub Apr 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
FROM oven/bun:canary

WORKDIR /app

# Install turbo globally
RUN bun install -g next turbo


COPY package.json bun.lock turbo.json ./

RUN mkdir -p apps packages

COPY apps/*/package.json ./apps/
COPY packages/*/package.json ./packages/
COPY packages/tsconfig/ ./packages/tsconfig/

RUN bun install

COPY . .

# Installing with full context. Prevent missing dependencies error.
RUN bun install


RUN bun run build

ENV NODE_ENV=production

# Resolve Nextjs TextEncoder error.
ENV NODE_OPTIONS=--no-experimental-fetch

EXPOSE 3000

CMD ["bun", "run", "start", "--host", "0.0.0.0"]
31 changes: 17 additions & 14 deletions apps/mail/app/(routes)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
'use client';

import { CommandPaletteProvider } from '@/components/context/command-palette-context';
import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper';
import { dexieStorageProvider } from '@/lib/idb';
import { SWRConfig } from 'swr';

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<CommandPaletteProvider>
<div className="flex h-screen w-screen overflow-hidden">
<SWRConfig
value={{
provider: typeof window !== 'undefined' ? dexieStorageProvider : undefined,
revalidateOnFocus: false,
revalidateIfStale: false,
shouldRetryOnError: false,
}}
>
{children}
</SWRConfig>
</div>
</CommandPaletteProvider>
<HotkeyProviderWrapper>
<CommandPaletteProvider>
<div className="flex h-screen w-screen overflow-hidden">
<SWRConfig
value={{
provider: typeof window !== 'undefined' ? dexieStorageProvider : undefined,
revalidateOnFocus: false,
revalidateIfStale: false,
shouldRetryOnError: false,
}}
>
{children}
</SWRConfig>
</div>
</CommandPaletteProvider>
</HotkeyProviderWrapper>
);
}
9 changes: 5 additions & 4 deletions apps/mail/app/(routes)/mail/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { KeyboardShortcuts } from '@/components/mail/keyboard-shortcuts';
import { AppSidebar } from '@/components/ui/app-sidebar';
import { GlobalHotkeys } from '@/lib/hotkeys/global-hotkeys';
import { HotkeyProviderWrapper } from '@/components/providers/hotkey-provider-wrapper';

export default function MailLayout({ children }: { children: React.ReactNode }) {
return (
<>
<HotkeyProviderWrapper>
<AppSidebar />
<KeyboardShortcuts />
<GlobalHotkeys />
<div className="w-full bg-white md:py-3 md:pr-2 dark:bg-black">{children}</div>
</>
</HotkeyProviderWrapper>
Comment on lines +7 to +11
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>
+    </>
+  );
}

);
}
94 changes: 94 additions & 0 deletions apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import type { MessageKey } from '@/config/navigation';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';

interface HotkeyRecorderProps {
isOpen: boolean;
onClose: () => void;
onHotkeyRecorded: (keys: string[]) => void;
currentKeys: string[];
}

export function HotkeyRecorder({
isOpen,
onClose,
onHotkeyRecorded,
currentKeys,
}: HotkeyRecorderProps) {
const t = useTranslations();
const [recordedKeys, setRecordedKeys] = useState<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]);
}
};

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

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

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

useEffect(() => {
if (isOpen) {
setRecordedKeys([]);
setIsRecording(true);
}
}, [isOpen]);

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{t('pages.settings.shortcuts.actions.recordHotkey' as MessageKey)}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center gap-4 py-4">
<div className="text-muted-foreground text-center text-sm">
{isRecording
? t('pages.settings.shortcuts.actions.pressKeys' as MessageKey)
: t('pages.settings.shortcuts.actions.releaseKeys' as MessageKey)}
</div>
<div className="flex gap-2">
{(recordedKeys.length > 0 ? recordedKeys : currentKeys).map((key) => (
<kbd
key={key}
className="border-muted-foreground/10 bg-accent h-6 rounded-[6px] border px-1.5 font-mono text-xs leading-6"
>
{key}
</kbd>
))}
</div>
</div>
</DialogContent>
</Dialog>
);
}
112 changes: 93 additions & 19 deletions apps/mail/app/(routes)/settings/shortcuts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,68 @@
'use client';

import { SettingsCard } from '@/components/settings/settings-card';
import { keyboardShortcuts } from '@/config/shortcuts'; //import the shortcuts
import { keyboardShortcuts, type Shortcut } from '@/config/shortcuts';
import type { MessageKey } from '@/config/navigation';
import { HotkeyRecorder } from './hotkey-recorder';
import { useState, type ReactNode, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { useTranslations } from 'next-intl';
import type { ReactNode } from 'react';
import { formatDisplayKeys } from '@/lib/hotkeys/use-hotkey-utils';
import { hotkeysDB } from '@/lib/hotkeys/hotkeys-db';
import { toast } from 'sonner';

export default function ShortcutsPage() {
const shortcuts = keyboardShortcuts; //now gets shortcuts from the config file
const [shortcuts, setShortcuts] = useState<Shortcut[]>(keyboardShortcuts);
const t = useTranslations();

useEffect(() => {
// Load any custom shortcuts from IndexedDB
hotkeysDB.getAllHotkeys()
.then(savedShortcuts => {
if (savedShortcuts.length > 0) {
const updatedShortcuts = keyboardShortcuts.map(defaultShortcut => {
const savedShortcut = savedShortcuts.find(s => s.action === defaultShortcut.action);
return savedShortcut || defaultShortcut;
});
setShortcuts(updatedShortcuts);
}
})
.catch(console.error);
}, []);

return (
<div className="grid gap-6">
<SettingsCard
title={t('pages.settings.shortcuts.title')}
description={t('pages.settings.shortcuts.description')}
footer={
<div className="flex gap-4">
<Button variant="outline">{t('common.actions.resetToDefaults')}</Button>
<Button>{t('common.actions.saveChanges')}</Button>
<Button
variant="outline"
onClick={async () => {
try {
// Save all default shortcuts to IndexedDB
await Promise.all(keyboardShortcuts.map(shortcut => hotkeysDB.saveHotkey(shortcut)));
setShortcuts(keyboardShortcuts);
toast.success('Shortcuts reset to defaults');
} catch (error) {
console.error('Failed to reset shortcuts:', error);
toast.error('Failed to reset shortcuts');
}
}}
>
{t('common.actions.resetToDefaults')}
</Button>
Comment on lines +40 to +55
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

Inefficient reset – write once instead of N times

Promise.all(keyboardShortcuts.map(hotkeysDB.saveHotkey)) issues one IndexedDB
transaction per shortcut (and each of those opens a read‑write transaction inside
saveAllHotkeys). That’s O(N²) work and noticeably slows the UI on large
shortcut sets.

Expose and call a bulk method once:

-await Promise.all(keyboardShortcuts.map(shortcut => hotkeysDB.saveHotkey(shortcut)));
+await hotkeysDB.saveAllHotkeys(keyboardShortcuts);

(This method already exists internally; just re‑export it or add a dedicated
resetToDefaults() helper.)

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

</div>
}
>
<div className="grid gap-2 md:grid-cols-2">
{shortcuts.map((shortcut, index) => (
<Shortcut key={index} keys={shortcut.keys}>
<Shortcut
key={index}
keys={shortcut.keys}
action={shortcut.action}
>
{t(`pages.settings.shortcuts.actions.${shortcut.action}` as MessageKey)}
</Shortcut>
))}
Expand All @@ -35,20 +72,57 @@ export default function ShortcutsPage() {
);
}

function Shortcut({ children, keys }: { children: ReactNode; keys: string[] }) {
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');
}
};
Comment on lines +75 to +98
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
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');
}
};


return (
<div className="bg-popover text-muted-foreground flex items-center justify-between gap-2 rounded-lg border p-2 text-sm">
<span className="font-medium">{children}</span>
<div className="flex select-none gap-1">
{keys.map((key) => (
<kbd
key={key}
className="border-muted-foreground/10 bg-accent h-6 rounded-[6px] border px-1.5 font-mono text-xs leading-6"
>
{key}
</kbd>
))}
<>
<div
className="bg-popover text-muted-foreground hover:bg-accent/50 flex cursor-pointer items-center justify-between gap-2 rounded-lg border p-2 text-sm"
onClick={() => setIsRecording(true)}
role="button"
tabIndex={0}
>
<span className="font-medium">{children}</span>
<div className="flex select-none gap-1">
{displayKeys.map((key) => (
<kbd
key={key}
className="border-muted-foreground/10 bg-accent h-6 rounded-[6px] border px-1.5 font-mono text-xs leading-6"
>
{key}
</kbd>
))}
</div>
</div>
</div>
<HotkeyRecorder
isOpen={isRecording}
onClose={() => setIsRecording(false)}
onHotkeyRecorded={handleHotkeyRecorded}
currentKeys={keys}
/>
</>
);
}
Loading