Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
0e2d67a
cleanup
hiheyhello123 Apr 17, 2025
61a4304
golden ticket fit
nizzyabi 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"]
122 changes: 121 additions & 1 deletion apps/mail/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,127 @@ export async function handleGoldenTicket(email: string) {
from: '0.email <onboarding@0.email>',
to: email,
subject: 'You <> Zero',
text: `Congrats on joining Zero (beta)! Your friend gave you direct access to the beta while skipping the waitlist! You are able to log in now with your email. If you have any questions or need help, please don't hesitate to reach out to us on Discord https://discord.gg/0email.`,
html: `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html dir="ltr" lang="en">
<head>
<link
rel="preload"
as="image"
href="https://i.imgur.com/xBnWSpN.png" />
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta name="x-apple-disable-message-reformatting" />
</head>
<body
style='background-color:rgb(246,249,252);font-family:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-top:60px;padding-bottom:60px'>
<div
style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">
You&#x27;ve been granted early access to Zero!
<div>
‌​‍‎‏
</div>
</div>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="background-color:rgb(255,255,255);border-radius:8px;margin-left:auto;margin-right:auto;padding:32px;max-width:600px">
<tbody>
<tr style="width:100%">
<td>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="margin-bottom:32px">
<tbody>
<tr>
<td>
<img
alt="Zero Early Access"
src="https://i.imgur.com/xBnWSpN.png"
style="width:100%;height:auto;object-fit:cover;border-radius:4px;display:block;outline:none;border:none;text-decoration:none" />
</td>
</tr>
</tbody>
</table>
<h1
style="font-size:24px;font-weight:700;color:rgb(51,51,51);margin-top:0px;margin-bottom:24px">
Welcome to Zero Early Access!
</h1>
<p
style="font-size:16px;line-height:24px;color:rgb(85,85,85);margin-bottom:16px;margin-top:16px">
Hi there,
</p>
<p
style="font-size:16px;line-height:24px;color:rgb(85,85,85);margin-bottom:32px;margin-top:16px">
Your friend has invited you to join Zero! We're excited to have you on board. Click the button below to get started.
</p>
<table
align="center"
width="100%"
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="text-align:center;margin-bottom:32px">
<tbody>
<tr>
<td>
<a
href="https://0.email/login"
style="background-color:rgb(0,0,0);color:rgb(255,255,255);font-weight:700;padding:16px 32px;border-radius:4px;text-decoration-line:none;text-align:center;box-sizing:border-box;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px"
target="_blank"
><span
><!--[if mso]><i style="mso-font-width:400%;mso-text-raise:18" hidden>&#8202;&#8202;&#8202;</i><![endif]--></span
><span
style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px"
>Access Zero Now</span
><span
><!--[if mso]><i style="mso-font-width:400%" hidden>&#8202;&#8202;&#8202;&#8203;</i><![endif]--></span
></a
>
</td>
</tr>
</tbody>
</table>
<p
style="font-size:16px;line-height:24px;color:rgb(85,85,85);margin-bottom:24px;margin-top:16px">
Join our
<a
href="https://discord.gg/0email"
style="color:rgb(0,0,0);text-decoration-line:underline"
target="_blank"
>Discord community</a
>
to connect with other early users and the Zero team for support,
feedback, and exclusive updates.
</p>
<p
style="font-size:16px;line-height:24px;color:rgb(85,85,85);margin-bottom:24px;margin-top:16px">
Your feedback during this early access phase is invaluable to us
as we continue to refine and improve Zero. We can't wait to
hear what you think!
</p>
<hr
style="border-color:rgb(230,235,241);margin-top:32px;margin-bottom:32px;width:100%;border:none;border-top:1px solid #eaeaea" />
<p
style="font-size:12px;line-height:16px;color:rgb(136,152,170);margin:0px;margin-bottom:16px;margin-top:16px">
©
<!-- -->2025<!-- -->
Zero Email Inc. All rights reserved.
</p>
</td>
</tr>
</tbody>
</table>
</body>
</html>`,
});
};

Expand Down
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>
);
}
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]);
}
};
Comment on lines +26 to +37
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 key combination validation and conflict detection

The recorder accepts any key combination without validation. Consider adding validation to ensure the combination is valid and doesn't conflict with browser defaults.

     const handleKeyDown = (e: KeyboardEvent) => {
       e.preventDefault();
       if (!isRecording) return;
 
       const key = e.key === ' ' ? 'Space' : e.key;
 
       const formattedKey = key.length === 1 ? key.toUpperCase() : key;
 
+      // Skip standalone modifier keys as they should be part of combinations
+      if (['Control', 'Alt', 'Meta', 'Shift'].includes(formattedKey) && recordedKeys.length === 0) {
+        return;
+      }
+      
+      // Check for potential browser conflicts
+      const hasModifier = recordedKeys.some(k => ['Control', 'Alt', 'Meta', 'Shift'].includes(k));
+      const isBrowserShortcut = !hasModifier && ['F5', 'F11'].includes(formattedKey);
+      if (isBrowserShortcut) {
+        console.warn('This shortcut may conflict with browser defaults');
+      }
+
       if (!recordedKeys.includes(formattedKey)) {
         setRecordedKeys((prev) => [...prev, formattedKey]);
       }
     };
📝 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 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 handleKeyDown = (e: KeyboardEvent) => {
e.preventDefault();
if (!isRecording) return;
const key = e.key === ' ' ? 'Space' : e.key;
const formattedKey = key.length === 1 ? key.toUpperCase() : key;
// Skip standalone modifier keys as they should be part of combinations
if (['Control', 'Alt', 'Meta', 'Shift'].includes(formattedKey) && recordedKeys.length === 0) {
return;
}
// Check for potential browser conflicts
const hasModifier = recordedKeys.some(k => ['Control', 'Alt', 'Meta', 'Shift'].includes(k));
const isBrowserShortcut = !hasModifier && ['F5', 'F11'].includes(formattedKey);
if (isBrowserShortcut) {
console.warn('This shortcut may conflict with browser defaults');
}
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]);
Comment on lines +50 to +57
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

Ensure proper event order management and resource cleanup

The current implementation handles events without checking their source element. Consider adding checks to only handle events when the modal is focused.


🏁 Script executed:

#!/bin/bash

# Check if the event listeners could capture events from outside the modal
# This could potentially cause issues if multiple HotkeyRecorder instances are open

# Look for similar event handlers in other components
rg -A 3 "window.addEventListener\('keydown'" --type ts --type tsx

Length of output: 101


I’ll re-run the searches without the incorrect --type filter and also inspect the handleKeyDown implementation:


🏁 Script executed:

#!/bin/bash
# 1. Look for other window keydown listeners across .ts/.tsx files
rg -A 3 "window.addEventListener('keydown'" -g '*.ts' -g '*.tsx'

# 2. Show the definition of handleKeyDown to verify if it already guards by focus/modal state
rg -C 5 "handleKeyDown" -g '*.ts' -g '*.tsx'

Length of output: 14806


Limit key handling to active recording and modal focus

We should avoid intercepting unrelated keystrokes and re‑attaching listeners on every key press. In apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx:

• Move e.preventDefault() inside the isRecording guard (so non‑recording key presses aren’t blocked)
• (Optional) Verify the event’s target is within the recorder modal before handling
• Remove recordedKeys from the effect’s dependency list to prevent tearing down and reattaching listeners on each record

Suggested diff:

--- a/apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx
+++ b/apps/mail/app/(routes)/settings/shortcuts/hotkey-recorder.tsx
@@ const handleKeyDown = (e: KeyboardEvent) => {
-  e.preventDefault();
-  if (!isRecording) return;
+  if (!isRecording) return;
+  // only block and record keys when the modal is open, focused, and actively recording
+  if (!modalRef.current?.contains(document.activeElement)) return;
+  e.preventDefault();
@@
-}, [isOpen, isRecording, recordedKeys, onHotkeyRecorded, onClose]);
+}, [isOpen, isRecording, onHotkeyRecorded, onClose]);
📝 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
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, [isOpen, isRecording, recordedKeys, onHotkeyRecorded, onClose]);
// --- In handleKeyDown: ---
const handleKeyDown = (e: KeyboardEvent) => {
if (!isRecording) return;
// only block and record keys when the modal is open, focused, and actively recording
if (!modalRef.current?.contains(document.activeElement)) return;
e.preventDefault();
// ... existing key‑recording logic ...
};
// --- In the effect that wires up the listeners: ---
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, [isOpen, isRecording, 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>
);
}
Loading