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
427 changes: 0 additions & 427 deletions apps/mail/app/(auth)/login/early-access/page.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion apps/mail/app/(routes)/mail/[folder]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface MailPageProps {

const ALLOWED_FOLDERS = ['inbox', 'draft', 'sent', 'spam', 'bin', 'archive'];

export default async function MailPage({ params, searchParams }: MailPageProps) {
export default async function MailPage({ params }: MailPageProps) {
const headersList = await headers();
const session = await auth.api.getSession({ headers: headersList });

Expand Down
10 changes: 2 additions & 8 deletions apps/mail/app/api/driver/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const driver = async (config: IConfig): Promise<MailManager> => {

return {
id: id || 'ERROR',
bcc: [],
threadId: threadId || '',
title: snippet ? he.decode(snippet).trim() : 'ERROR',
tls: wasSentWithTLS(receivedHeaders) || !!hasTLSReport,
Expand Down Expand Up @@ -479,14 +480,7 @@ export const driver = async (config: IConfig): Promise<MailManager> => {
const { folder: normalizedFolder, q: normalizedQ } = normalizeSearch(folder, q ?? '');
const labelIds = [..._labelIds];
if (normalizedFolder) labelIds.push(normalizedFolder.toUpperCase());
console.log({
folder,
userId: 'me',
q: normalizedQ ? normalizedQ : undefined,
labelIds: folder === 'inbox' ? labelIds : [],
maxResults,
pageToken: pageToken ? pageToken : undefined,
})

const res = await gmail.users.threads.list({
userId: 'me',
q: normalizedQ ? normalizedQ : undefined,
Expand Down
23 changes: 10 additions & 13 deletions apps/mail/components/mail/mail-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { type ComponentProps, memo, useCallback, useEffect, useMemo, useRef } fr
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { EmptyState, type FolderType } from '@/components/mail/empty-state';
import { ThreadContextMenu } from '@/components/context/thread-context';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { cn, FOLDERS, formatDate, getEmailLogo } from '@/lib/utils';
import { Avatar, AvatarImage, AvatarFallback } from '../ui/avatar';
import { useMailNavigation } from '@/hooks/use-mail-navigation';
Expand Down Expand Up @@ -84,17 +84,17 @@ const Thread = memo(
const [mail] = useMail();
const [searchValue] = useSearchValue();
const t = useTranslations();
const searchParams = useSearchParams();
const { folder } = useParams<{ folder: string }>();
const { mutate } = useThreads();
const threadIdParam = searchParams.get('threadId');
const [threadId, setThreadId] = useQueryState('threadId');
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const isHovering = useRef<boolean>(false);
const hasPrefetched = useRef<boolean>(false);
const isMailSelected = useMemo(() => {
const threadId = message.threadId ?? message.id;
return threadId === threadIdParam || threadId === mail.selected;
}, [message.id, message.threadId, threadIdParam, mail.selected]);
if (!threadId) return false;
const _threadId = message.threadId ?? message.id;
return _threadId === threadId || threadId === mail.selected;
}, [threadId, message.id, message.threadId, mail.selected]);

const isMailBulkSelected = mail.bulkSelected.includes(message.threadId ?? message.id);

Expand Down Expand Up @@ -194,7 +194,7 @@ const Thread = memo(
'text-md flex items-baseline gap-1 group-hover:opacity-100',
)}
>
<span className={cn(threadIdParam ? 'max-w-[3ch] truncate' : '')}>
<span className={cn(threadId ? 'max-w-[3ch] truncate' : '')}>
{highlightText(message.sender.name, searchValue.highlight)}
</span>{' '}
{message.unread && !isMailSelected ? (
Expand Down Expand Up @@ -281,7 +281,7 @@ const Thread = memo(
)}
>
<span
className={cn('truncate', threadIdParam ? 'max-w-[20ch] truncate' : '')}
className={cn('truncate', threadId ? 'max-w-[20ch] truncate' : '')}
>
{highlightText(message.sender.name, searchValue.highlight)}
</span>{' '}
Expand Down Expand Up @@ -377,7 +377,6 @@ export const MailList = memo(({ isCompact }: MailListProps) => {
const [mail, setMail] = useMail();
const { data: session } = useSession();
const t = useTranslations();
const searchParams = useSearchParams();
const router = useRouter();
const [threadId, setThreadId] = useQueryState('threadId');
const [category, setCategory] = useQueryState('category');
Expand Down Expand Up @@ -435,11 +434,9 @@ export const MailList = memo(({ isCompact }: MailListProps) => {

const handleNavigateToThread = useCallback(
(threadId: string) => {
const currentParams = new URLSearchParams(searchParams.toString());
currentParams.set('threadId', threadId);
router.push(`/mail/${folder}?${currentParams.toString()}`);
setThreadId(threadId);
},
[folder, router, searchParams],
[folder, router],
);

const {
Expand Down
18 changes: 8 additions & 10 deletions apps/mail/components/mail/mail-quick-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
'use client';

import { moveThreadsTo, ThreadDestination } from '@/lib/thread-actions';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { Archive, Mail, Reply, Trash, Inbox } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
import { Archive, Mail, Inbox } from 'lucide-react';
import { markAsRead, markAsUnread } from '@/actions/mail';
import { useCallback, memo, useState } from 'react';
import { cn, FOLDERS, LABELS } from '@/lib/utils';
import { cn, FOLDERS } from '@/lib/utils';
import { useThreads } from '@/hooks/use-threads';
import { Button } from '@/components/ui/button';
import { useStats } from '@/hooks/use-stats';
import type { InitialThread } from '@/types';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import { useQueryState } from 'nuqs';

interface MailQuickActionsProps {
message: InitialThread;
Expand All @@ -36,27 +37,24 @@ export const MailQuickActions = memo(
const { mutate: mutateStats } = useStats();
const t = useTranslations();
const router = useRouter();
const searchParams = useSearchParams();
const [isProcessing, setIsProcessing] = useState(false);
const [threadId, setThreadId] = useQueryState('threadId');

const currentFolder = folder ?? '';
const isInbox = currentFolder === FOLDERS.INBOX;
const isArchiveFolder = currentFolder === FOLDERS.ARCHIVE;

const closeThreadIfOpen = useCallback(() => {
const threadIdParam = searchParams.get('threadId');
const messageId = message.threadId ?? message.id;

if (threadIdParam === messageId) {
const currentParams = new URLSearchParams(searchParams.toString());
currentParams.delete('threadId');
router.push(`/mail/${currentFolder}?${currentParams.toString()}`);
if (threadId === messageId) {
setThreadId(null)
}

if (resetNavigation) {
resetNavigation();
}
}, [searchParams, message, router, currentFolder, resetNavigation]);
}, [threadId, message, router, currentFolder, resetNavigation]);

const handleArchive = useCallback(
async (e?: React.MouseEvent) => {
Expand Down
9 changes: 4 additions & 5 deletions apps/mail/components/mail/mail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { useState, useCallback, useMemo, useEffect, useRef, memo } from 'react';
import { ThreadDisplay, ThreadDemo } from '@/components/mail/thread-display';
import { MailList, MailListDemo } from '@/components/mail/mail-list';
import { handleUnsubscribe } from '@/lib/email-utils.client';
import { useParams, useSearchParams } from 'next/navigation';
import { useParams } from 'next/navigation';
import { useMediaQuery } from '../../hooks/use-media-query';
import { useSearchValue } from '@/hooks/use-search-value';
import { useMail } from '@/components/mail/use-mail';
Expand Down Expand Up @@ -74,7 +74,7 @@ export function DemoMailLayout() {
const isValidating = false;
const isLoading = false;
const isDesktop = true;
const threadIdParam = useQueryState('threadId');
const [threadIdParam] = useQueryState('threadId');
const [activeCategory, setActiveCategory] = useState('Primary');
const [filteredItems, setFilteredItems] = useState(items);

Expand Down Expand Up @@ -250,10 +250,9 @@ export function MailLayout() {

const [threadId, setThreadId] = useQueryState('threadId');

const handleClose = useCallback(() => {
const handleClose = () => {
setThreadId(null);
router.push(`/mail/${folder}`);
}, [router, folder, setThreadId]);
}

// Search bar is always visible now, no need for keyboard shortcuts to toggle it
useHotKey('Esc', (event) => {
Expand Down
22 changes: 8 additions & 14 deletions apps/mail/components/mail/thread-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Trash,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useSearchParams, useParams } from 'next/navigation';
import { useParams } from 'next/navigation';
import { ScrollArea } from '@/components/ui/scroll-area';

import { moveThreadsTo, ThreadDestination } from '@/lib/thread-actions';
Expand All @@ -31,6 +31,7 @@ import { ParsedMessage } from '@/types';
import { Inbox } from 'lucide-react';
import { toast } from 'sonner';
import { NotesPanel } from './note-panel';
import { useQueryState } from 'nuqs';


interface ThreadDisplayProps {
Expand Down Expand Up @@ -134,18 +135,16 @@ function ThreadActionButton({
);
}

export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisplayProps) {
export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
const { data: emailData, isLoading, mutate: mutateThread } = useThread(id ?? null);
const { mutate: mutateThreads } = useThreads();
const searchParams = useSearchParams();
const [isMuted, setIsMuted] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [mail, setMail] = useMail();
const t = useTranslations();
const { mutate: mutateStats } = useStats();
const { folder } = useParams<{ folder: string }>();
const threadIdParam = searchParams.get('threadId');
const threadId = threadParam ?? threadIdParam ?? '';
const [threadId, setThreadId] = useQueryState('threadId');

// Check if thread contains any images (excluding sender avatars)
const hasImages = useMemo(() => {
Expand Down Expand Up @@ -188,17 +187,12 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
const isInSpam = folder === FOLDERS.SPAM;
const isInBin = folder === FOLDERS.BIN;
const handleClose = useCallback(() => {
// Reset reply composer state when closing thread display
setMail((prev) => ({
...prev,
replyComposerOpen: false,
forwardComposerOpen: false
}));
onClose?.();
}, [onClose, setMail]);
setThreadId(null)
}, []);

const moveThreadTo = useCallback(
async (destination: ThreadDestination) => {
if (!threadId) return;
const promise = async () => {
await moveThreadsTo({
threadIds: [threadId],
Expand Down Expand Up @@ -330,7 +324,7 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp
<ThreadSubject subject={emailData[0]?.subject} />
</div>
<div className="flex items-center md:gap-2">
<NotesPanel threadId={threadId} />
{threadId ? <NotesPanel threadId={threadId} /> : null}
<ThreadActionButton
icon={Expand}
label={
Expand Down
2 changes: 1 addition & 1 deletion apps/mail/components/ui/nav-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ function NavItem(item: NavItemProps & { href: string }) {
return (
<Collapsible defaultOpen={item.isActive}>
<CollapsibleTrigger asChild>
<Link {...linkProps} target={item.target}>{buttonContent}</Link>
<Link {...linkProps} prefetch target={item.target}>{buttonContent}</Link>
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

Improved navigation with prefetching

Adding the prefetch attribute to the Link component will optimize performance by preloading the linked page when the link is in the viewport. Also, properly passing the target prop ensures links open correctly according to their configuration.

</CollapsibleTrigger>
</Collapsible>
);
Expand Down
1 change: 0 additions & 1 deletion apps/mail/components/ui/nav-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export function NavUser() {
if (!isRendered) return null;

const handleAccountSwitch = (connection: IConnection) => async () => {
router.push('/mail/inbox'); // this is temp, its not good. bad. we change later.
await putConnection(connection.id);
refetch();
mutate();
Expand Down
6 changes: 4 additions & 2 deletions apps/mail/hooks/use-notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import useSWR from 'swr';

export type { Note };

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export const useThreadNotes = (threadId: string) => {
const t = useTranslations();
const { data: session } = useSession();
Expand All @@ -17,10 +19,10 @@ export const useThreadNotes = (threadId: string) => {
isLoading,
mutate,
} = useSWR<Note[]>(
session?.connectionId ? `notes-${threadId}-${session.connectionId}` : null,
session?.connectionId && threadId ? ['notes', session.connectionId, threadId] : null,
async () => {
try {
const result = await fetchThreadNotes(threadId);
const result = await fetcher('/api/driver/notes?threadId=' + threadId);
return result.data || [];
} catch (err: any) {
console.error('Error fetching notes:', err);
Expand Down
7 changes: 4 additions & 3 deletions apps/mail/hooks/use-threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useSWRInfinite from 'swr/infinite';
import useSWR, { preload } from 'swr';
import { useMemo } from 'react';
import axios from 'axios';
import { useQueryState } from 'nuqs';

export const preloadThread = async (userId: string, threadId: string, connectionId: string) => {
console.log(`🔄 Prefetching email ${threadId}...`);
Expand Down Expand Up @@ -99,7 +100,7 @@ export const useThreads = () => {
);

// Flatten threads from all pages and sort by receivedOn date (newest first)
const threads = useMemo(() => (data ? data.flatMap((e) => e.threads) : []), [data]);
const threads = useMemo(() => (data ? data.flatMap((e) => e.threads) : []), [data, session]);
const isEmpty = useMemo(() => threads.length === 0, [threads]);
const isReachingEnd = isEmpty || (data && !data[data.length - 1]?.nextPageToken);
const loadMore = async () => {
Expand All @@ -123,8 +124,8 @@ export const useThreads = () => {

export const useThread = (threadId: string | null) => {
const { data: session } = useSession();
const searchParams = useSearchParams();
const id = threadId ? threadId : searchParams.get('threadId');
const [_threadId] = useQueryState('threadId');
const id = threadId ? threadId : _threadId

const { data, isLoading, error, mutate } = useSWR<ParsedMessage[]>(
session?.user.id && id ? [session.user.id, id, session.connectionId] : null,
Expand Down