-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Staging #700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Staging #700
Changes from all commits
588ab02
1347a8e
373423b
cd8833b
49acc64
8dba8a9
7dd0fcd
0d4be12
e35bd3f
d1bcb68
f9f16db
e730552
85e8fbf
dd9f236
2398a61
93eb1d2
814fc48
749cabc
4d51e20
d3c72b5
972ddd0
4ee76c3
38d85e4
8f687d7
458b065
8a0a833
ca1bc22
a864044
03106ce
b73d3be
a71f0c4
9e22918
db58b12
9313985
14dfeed
2bdcd8d
cffc4f6
870dca9
32a698d
576cc73
c82605a
0e2d67a
61a4304
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"] |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainEnsure 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 tsxLength of output: 101 I’ll re-run the searches without the incorrect 🏁 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 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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