Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions apps/mail/app/(auth)/login/login-client.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
4 changes: 2 additions & 2 deletions apps/mail/components/create/create-email.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
144 changes: 25 additions & 119 deletions apps/mail/components/mail/mail-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -68,6 +68,7 @@ const Thread = memo(
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const isHovering = useRef<boolean>(false);
const hasPrefetched = useRef<boolean>(false);
const isKeyPressed = useKeyState();

const threadHasNotes = useMemo(() => {
return !demo && hasNotes(message.threadId ?? message.id);
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand 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'));
Expand All @@ -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'));
Expand All @@ -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'));
Expand All @@ -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'));
Expand All @@ -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)
Expand Down Expand Up @@ -543,7 +449,7 @@ export function MailList({ isCompact }: MailListProps) {
.catch(console.error);
}
},
[mail, setMail, selectMode],
[mail, setMail, items, getSelectMode],
);

const isEmpty = items.length === 0;
Expand All @@ -565,7 +471,7 @@ export function MailList({ isCompact }: MailListProps) {
>
<div
ref={parentRef}
className={cn('w-full', selectMode === 'range' && 'select-none')}
className={cn('w-full', isKeyPressed('Shift') && 'select-none')}
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
Expand All @@ -588,7 +494,7 @@ export function MailList({ isCompact }: MailListProps) {
<Thread
onClick={handleMailClick}
message={item}
selectMode={selectMode}
selectMode={getSelectMode()}
isCompact={isCompact}
sessionData={sessionData}
/>
Expand Down
10 changes: 5 additions & 5 deletions apps/mail/components/ui/nav-main.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGSVGElement> {
ref?: React.Ref<SVGSVGElement>;
Expand Down
34 changes: 33 additions & 1 deletion apps/mail/hooks/use-hot-key.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import { useCallback, useRef, useLayoutEffect, useState, useEffect } from "react";

const keyStates = new Map<string, boolean>();

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,
Expand All @@ -11,7 +43,7 @@ export const useHotKey = (
useLayoutEffect(() => {
callbackRef.current = callback;
});

const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
const isTextInput =
Expand Down
4 changes: 2 additions & 2 deletions apps/mail/hooks/use-notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -144,7 +144,7 @@ export function useNotes() {
async (noteId: string): Promise<Note | null> => {
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,
Expand Down