diff --git a/apps/mail/app/(auth)/login/login-client.tsx b/apps/mail/app/(auth)/login/login-client.tsx index 4260e199cd..cf4d1458b7 100644 --- a/apps/mail/app/(auth)/login/login-client.tsx +++ b/apps/mail/app/(auth)/login/login-client.tsx @@ -1,9 +1,9 @@ 'use client'; -import { GitHub, Google } from '@/components/icons/icons'; -import { signIn, useSession } from '@/lib/auth-client'; import { useEffect, type ReactNode, useState } from 'react'; +import { GitHub, Google } from '@/components/icons/icons'; import { type EnvVarInfo } from '@/lib/auth-providers'; +import { signIn, useSession } from '@/lib/auth-client'; import { Button } from '@/components/ui/button'; import { useRouter } from 'next/navigation'; import Image from 'next/image'; diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index c8ec159a80..f5640bab80 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -1,15 +1,15 @@ 'use client'; -import { ArrowUpIcon, Paperclip, X } from 'lucide-react'; import { useConnections } from '@/hooks/use-connections'; import { createDraft, getDraft } from '@/actions/drafts'; +import { ArrowUpIcon, Paperclip, X } from 'lucide-react'; import { SidebarToggle } from '../ui/sidebar-toggle'; import { Button } from '@/components/ui/button'; import { useSession } from '@/lib/auth-client'; import { AIAssistant } from './ai-assistant'; import { useTranslations } from 'next-intl'; import { sendEmail } from '@/actions/send'; -import { useQueryState } from 'nuqs'; import { type JSONContent } from 'novel'; +import { useQueryState } from 'nuqs'; import { toast } from 'sonner'; import * as React from 'react'; import Editor from './editor'; diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index 57428c50b7..9ed139b4ac 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -27,12 +27,12 @@ import { useTranslations, useFormatter } from 'next-intl'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useMail } from '@/components/mail/use-mail'; +import { useKeyState } from '@/hooks/use-hot-key'; import { useHotKey } from '@/hooks/use-hot-key'; import { useSession } from '@/lib/auth-client'; import { Badge } from '@/components/ui/badge'; import { useNotes } from '@/hooks/use-notes'; import { useParams } from 'next/navigation'; -import { useTheme } from 'next-themes'; import items from './demo.json'; import { toast } from 'sonner'; @@ -68,6 +68,7 @@ const Thread = memo( const hoverTimeoutRef = useRef | undefined>(undefined); const isHovering = useRef(false); const hasPrefetched = useRef(false); + const isKeyPressed = useKeyState(); const threadHasNotes = useMemo(() => { return !demo && hasNotes(message.threadId ?? message.id); @@ -139,6 +140,11 @@ const Thread = memo( onClick={onClick ? onClick(message) : undefined} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + onMouseDown={(e) => { + if (isKeyPressed('Control') || isKeyPressed('Meta')) { + e.preventDefault(); + } + }} key={message.id} className={cn( 'hover:bg-offsetLight hover:bg-primary/5 group relative flex cursor-pointer flex-col items-start overflow-clip rounded-lg border border-transparent px-4 py-3 text-left text-sm transition-all hover:opacity-100', @@ -276,9 +282,7 @@ export function MailList({ isCompact }: MailListProps) { [isLoading, isValidating, nextPageToken, itemHeight], ); - const [massSelectMode, setMassSelectMode] = useState(false); - const [rangeSelectMode, setRangeSelectMode] = useState(false); - const [selectAllBelowMode, setSelectAllBelowMode] = useState(false); + const isKeyPressed = useKeyState(); const selectAll = useCallback(() => { // If there are already items selected, deselect them all @@ -302,34 +306,7 @@ export function MailList({ isCompact }: MailListProps) { } }, [items, setMail, mail.bulkSelected, t]); - const resetSelectMode = () => { - setMassSelectMode(false); - setRangeSelectMode(false); - setSelectAllBelowMode(false); - }; - - useHotKey('Control', () => { - resetSelectMode(); - setMassSelectMode(true); - }); - - useHotKey('Meta', () => { - resetSelectMode(); - setMassSelectMode(true); - }); - - useHotKey('Shift', () => { - resetSelectMode(); - setRangeSelectMode(true); - }); - - useHotKey('Alt+Shift', () => { - resetSelectMode(); - setSelectAllBelowMode(true); - }); - useHotKey('Meta+Shift+u', () => { - resetSelectMode(); markAsUnread({ ids: mail.bulkSelected }).then((result) => { if (result.success) { toast.success(t('common.mail.markedAsUnread')); @@ -342,20 +319,6 @@ export function MailList({ isCompact }: MailListProps) { }); useHotKey('Control+Shift+u', () => { - resetSelectMode(); - void (async () => { - const res = await markAsUnread({ ids: mail.bulkSelected }); - if (res.success) { - toast.success('Marked as unread'); - setMail((prev) => ({ - ...prev, - bulkSelected: [], - })); - } else toast.error('Failed to mark as unread'); - })(); - }); - useHotKey('Control+Shift+u', () => { - resetSelectMode(); markAsUnread({ ids: mail.bulkSelected }).then((response) => { if (response.success) { toast.success(t('common.mail.markedAsUnread')); @@ -368,20 +331,6 @@ export function MailList({ isCompact }: MailListProps) { }); useHotKey('Meta+Shift+i', () => { - resetSelectMode(); - void (async () => { - const res = await markAsRead({ ids: mail.bulkSelected }); - if (res.success) { - toast.success('Marked as read'); - setMail((prev) => ({ - ...prev, - bulkSelected: [], - })); - } else toast.error('Failed to mark as read'); - })(); - }); - useHotKey('Meta+Shift+i', () => { - resetSelectMode(); markAsRead({ ids: mail.bulkSelected }).then((data) => { if (data.success) { toast.success(t('common.mail.markedAsRead')); @@ -394,7 +343,6 @@ export function MailList({ isCompact }: MailListProps) { }); useHotKey('Control+Shift+i', () => { - resetSelectMode(); markAsRead({ ids: mail.bulkSelected }).then((response) => { if (response.success) { toast.success(t('common.mail.markedAsRead')); @@ -406,85 +354,43 @@ export function MailList({ isCompact }: MailListProps) { }); }); - // useHotKey("Meta+Shift+j", async () => { - // resetSelectMode(); - // const res = await markAsJunk({ ids: mail.bulkSelected }); - // if (res.success) toast.success("Marked as junk"); - // else toast.error("Failed to mark as junk"); - // }); - - // useHotKey("Control+Shift+j", async () => { - // resetSelectMode(); - // const res = await markAsJunk({ ids: mail.bulkSelected }); - // if (res.success) toast.success("Marked as junk"); - // else toast.error("Failed to mark as junk"); - // }); - useHotKey('Meta+a', (event) => { event?.preventDefault(); - resetSelectMode(); selectAll(); }); useHotKey('Control+a', (event) => { event?.preventDefault(); - resetSelectMode(); selectAll(); }); useHotKey('Meta+n', (event) => { event?.preventDefault(); - resetSelectMode(); selectAll(); }); useHotKey('Control+n', (event) => { event?.preventDefault(); - resetSelectMode(); selectAll(); }); - useEffect(() => { - const handleKeyUp = (e: KeyboardEvent) => { - if (e.key === 'Control' || e.key === 'Meta') { - setMassSelectMode(false); - } - if (e.key === 'Shift') { - setRangeSelectMode(false); - } - if (e.key === 'Alt') { - setSelectAllBelowMode(false); - } - }; - - const handleBlur = () => { - setMassSelectMode(false); - setRangeSelectMode(false); - setSelectAllBelowMode(false); - }; - - window.addEventListener('keyup', handleKeyUp); - window.addEventListener('blur', handleBlur); - - return () => { - window.removeEventListener('keyup', handleKeyUp); - window.removeEventListener('blur', handleBlur); - setMassSelectMode(false); - setRangeSelectMode(false); - setSelectAllBelowMode(false); - }; - }, []); - - const selectMode: MailSelectMode = massSelectMode - ? 'mass' - : rangeSelectMode - ? 'range' - : selectAllBelowMode - ? 'selectAllBelow' - : 'single'; + const getSelectMode = useCallback((): MailSelectMode => { + if (isKeyPressed('Control') || isKeyPressed('Meta')) { + return 'mass'; + } + if (isKeyPressed('Shift')) { + return 'range'; + } + if (isKeyPressed('Alt') && isKeyPressed('Shift')) { + return 'selectAllBelow'; + } + return 'single'; + }, [isKeyPressed]); const handleMailClick = useCallback( (message: InitialThread) => () => { + const selectMode = getSelectMode(); + if (selectMode === 'mass') { const updatedBulkSelected = mail.bulkSelected.includes(message.id) ? mail.bulkSelected.filter((id) => id !== message.id) @@ -543,7 +449,7 @@ export function MailList({ isCompact }: MailListProps) { .catch(console.error); } }, - [mail, setMail, selectMode], + [mail, setMail, items, getSelectMode], ); const isEmpty = items.length === 0; @@ -565,7 +471,7 @@ export function MailList({ isCompact }: MailListProps) { >
diff --git a/apps/mail/components/ui/nav-main.tsx b/apps/mail/components/ui/nav-main.tsx index ea02fbdd96..bc857006e7 100644 --- a/apps/mail/components/ui/nav-main.tsx +++ b/apps/mail/components/ui/nav-main.tsx @@ -1,18 +1,18 @@ 'use client'; -import { usePathname, useSearchParams } from 'next/navigation'; -import { useRef, useCallback } from 'react'; -import * as React from 'react'; -import Link from 'next/link'; import { SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarMenuButton } from './sidebar'; import { Collapsible, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { usePathname, useSearchParams } from 'next/navigation'; import { useFeaturebase } from '@/hooks/use-featurebase'; -import { useTranslations } from 'next-intl'; import { type MessageKey } from '@/config/navigation'; import { Badge } from '@/components/ui/badge'; import { useStats } from '@/hooks/use-stats'; +import { useTranslations } from 'next-intl'; +import { useRef, useCallback } from 'react'; import { BASE_URL } from '@/lib/constants'; import { cn } from '@/lib/utils'; +import * as React from 'react'; +import Link from 'next/link'; interface IconProps extends React.SVGProps { ref?: React.Ref; diff --git a/apps/mail/hooks/use-hot-key.ts b/apps/mail/hooks/use-hot-key.ts index 2bfb2e1db3..0f1afdff1f 100644 --- a/apps/mail/hooks/use-hot-key.ts +++ b/apps/mail/hooks/use-hot-key.ts @@ -1,5 +1,37 @@ import { useCallback, useRef, useLayoutEffect, useState, useEffect } from "react"; +const keyStates = new Map(); + +let listenersInit = false; + +function initKeyListeners() { + if (typeof window !== 'undefined' && !listenersInit) { + window.addEventListener('keydown', (e) => { + keyStates.set(e.key, true); + }); + + window.addEventListener('keyup', (e) => { + keyStates.set(e.key, false); + }); + + window.addEventListener('blur', () => { + keyStates.forEach((_, key) => { + keyStates.set(key, false); + }); + }); + + listenersInit = true; + } +} + +if (typeof window !== 'undefined') { + setTimeout(() => initKeyListeners(), 0); +} + +export function useKeyState() { + return useCallback((key: string) => keyStates.get(key) || false, []); +} + export const useHotKey = ( shortcut: string, callback: (event?: KeyboardEvent) => void, @@ -11,7 +43,7 @@ export const useHotKey = ( useLayoutEffect(() => { callbackRef.current = callback; }); - + const handleKeyDown = useCallback( (event: KeyboardEvent) => { const isTextInput = diff --git a/apps/mail/hooks/use-notes.tsx b/apps/mail/hooks/use-notes.tsx index 3315900dec..0461a88979 100644 --- a/apps/mail/hooks/use-notes.tsx +++ b/apps/mail/hooks/use-notes.tsx @@ -7,11 +7,11 @@ import { reorderNotes as reorderNotesAction, } from '@/actions/notes'; import type { Note } from '@/app/api/notes/types'; +import { useSession } from '@/lib/auth-client'; import { useTranslations } from 'next-intl'; import useSWR, { mutate } from 'swr'; import { useCallback } from 'react'; import { toast } from 'sonner'; -import { useSession } from '@/lib/auth-client'; export type { Note }; @@ -144,7 +144,7 @@ export function useNotes() { async (noteId: string): Promise => { try { const noteToUpdate = notes.find((note) => note.id === noteId); - if (!noteToUpdate) return null + if (!noteToUpdate) return null; const result = await updateNoteAction(noteId, { isPinned: !noteToUpdate.isPinned,