Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
50c10da
fix: fixed conflict issues and drafts featurs are done
AnjanyKumarJaiswal Jun 29, 2025
f023d02
Merge branch 'Mail-0:staging' into feat/autoSave_Email_drafts
AnjanyKumarJaiswal Jul 2, 2025
1cbb104
fix: fixing conflicts and rebasing with new changes
AnjanyKumarJaiswal Jul 11, 2025
0028ed9
fix: fixed conflicts
AnjanyKumarJaiswal Jul 11, 2025
1c068b6
fix: fixed conflicts with new fast codebase
AnjanyKumarJaiswal Jul 11, 2025
3551c19
feat: finally done with Auto-Save , Update and Delete Drafts
AnjanyKumarJaiswal Jul 11, 2025
50d99eb
Merge branch 'feat/autoSave_Email_drafts' of https://github.com/Anjan…
AnjanyKumarJaiswal Jul 11, 2025
5bbe1d6
Merge branch 'staging' into feat/autoSave_Email_drafts
AnjanyKumarJaiswal Jul 12, 2025
307660f
Merge branch 'staging' into feat/autoSave_Email_drafts
AnjanyKumarJaiswal Jul 14, 2025
f669ca3
fix: applied AI recommendations and implemented to code structure
AnjanyKumarJaiswal Jul 14, 2025
9c1b4a5
Merge branch 'feat/autoSave_Email_drafts' of https://github.com/Anjan…
AnjanyKumarJaiswal Jul 14, 2025
a8919cb
Merge branch 'staging' into feat/autoSave_Email_drafts
AnjanyKumarJaiswal Jul 14, 2025
b5ef607
fix: implmented AI considerations
AnjanyKumarJaiswal Jul 14, 2025
33c8eef
Merge branch 'feat/autoSave_Email_drafts' of https://github.com/Anjan…
AnjanyKumarJaiswal Jul 14, 2025
c3790a3
Merge branch 'staging' into feat/autoSave_Email_drafts
AnjanyKumarJaiswal Jul 16, 2025
6599c07
feat: updated condition logic for drafts
AnjanyKumarJaiswal Jul 16, 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
24 changes: 14 additions & 10 deletions apps/mail/components/create/create-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EmailComposer } from './email-composer';
import { useSession } from '@/lib/auth-client';
import { serializeFiles } from '@/lib/schemas';
import { useDraft } from '@/hooks/use-drafts';
import { useThreads } from '@/hooks/use-threads';
import { useEffect, useState } from 'react';

import type { Attachment } from '@/types';
Expand Down Expand Up @@ -54,13 +55,15 @@ export function CreateEmail({
data: draft,
isLoading: isDraftLoading,
error: draftError,
refetch: refetchDraft
} = useDraft(draftId ?? propDraftId ?? null);

const [, setIsDraftFailed] = useState(false);
const trpc = useTRPC();
const { mutateAsync: sendEmail } = useMutation(trpc.mail.send.mutationOptions());
const [isComposeOpen, setIsComposeOpen] = useQueryState('isComposeOpen');
const [, setThreadId] = useQueryState('threadId');
const [{refetch: refetchThreads }] = useThreads();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to refetch threads?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually why i did it because when the user clicks the esc button the emails are not refreshed automatically though
it needs to be done manually

const [, setActiveReplyId] = useQueryState('activeReplyId');
const { data: activeConnection } = useActiveConnection();
const { data: settings, isLoading: settingsLoading } = useSettings();
Expand Down Expand Up @@ -144,6 +147,8 @@ export function CreateEmail({
}
};



const base64ToFile = (base64: string, filename: string, mimeType: string): File | null => {
try {
const byteString = atob(base64);
Expand All @@ -169,22 +174,19 @@ export function CreateEmail({
<div className="flex min-h-screen flex-col items-center justify-center gap-1">
<div className="flex w-[750px] justify-start">
<DialogClose asChild className="flex">
<button className="dark:bg-panelDark flex items-center gap-1 rounded-lg bg-[#F0F0F0] px-2 py-1.5">
<button onClick={
() => {
refetchThreads()
}
}
className="dark:bg-panelDark flex items-center gap-1 rounded-lg bg-[#F0F0F0] px-2 py-1.5">
<X className="fill-muted-foreground mt-0.5 h-3.5 w-3.5 dark:fill-[#929292]" />
<span className="text-muted-foreground text-sm font-medium dark:text-white">
esc
</span>
</button>
</DialogClose>
</div>
{isDraftLoading ? (
<div className="flex h-[600px] w-[750px] items-center justify-center rounded-2xl border">
<div className="text-center">
<div className="mx-auto mb-4 h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600"></div>
<p>Loading draft...</p>
</div>
</div>
) : (
<EmailComposer
key={typedDraft?.id || 'composer'}
className="mb-12 rounded-2xl border"
Expand All @@ -208,12 +210,14 @@ export function CreateEmail({
setIsComposeOpen(null);
setDraftId(null);
}}
onDraftUpdate={() => {
refetchDraft();
}}
initialAttachments={files}
initialSubject={typedDraft?.subject || initialSubject}
autofocus={false}
settingsLoading={settingsLoading}
/>
)}
</div>
</Dialog>
</>
Expand Down
122 changes: 95 additions & 27 deletions apps/mail/components/create/email-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '@/components/ui/select';

import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
import { Check, Command, Loader, Paperclip, Plus, Type, X as XIcon } from 'lucide-react';
import { Check, Command, Loader, Paperclip, Trash, Plus, Type, X as XIcon } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { TextEffect } from '@/components/motion-primitives/text-effect';
import { ImageCompressionSettings } from './image-compression-settings';
Expand All @@ -31,11 +31,11 @@ import { AnimatePresence, motion } from 'motion/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Avatar, AvatarFallback } from '../ui/avatar';
import { useTRPC } from '@/providers/query-provider';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useSettings } from '@/hooks/use-settings';

import { cn, formatFileSize } from '@/lib/utils';
import { useThread } from '@/hooks/use-threads';
import { useThread, useThreads } from '@/hooks/use-threads';
import { serializeFiles } from '@/lib/schemas';
import { Input } from '@/components/ui/input';
import { EditorContent } from '@tiptap/react';
Expand Down Expand Up @@ -74,6 +74,7 @@ interface EmailComposerProps {
fromEmail?: string;
}) => Promise<void>;
onClose?: () => void;
onDraftUpdate?: () => void;
className?: string;
autofocus?: boolean;
settingsLoading?: boolean;
Expand Down Expand Up @@ -111,6 +112,8 @@ export function EmailComposer({
initialAttachments = [],
onSendEmail,
onClose,
onDraftUpdate,
// onDeleteDrafts,
className,
autofocus = false,
settingsLoading = false,
Expand All @@ -122,6 +125,8 @@ export function EmailComposer({
const [showBcc, setShowBcc] = useState(initialBcc.length > 0);
const [isLoading, setIsLoading] = useState(false);
const [isSavingDraft, setIsSavingDraft] = useState(false);
const [isDeleteDraft, setIsDeleteDraft] = useState(false);
const [{ isFetching, refetch: refetchThreads }] = useThreads();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [messageLength, setMessageLength] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -251,6 +256,8 @@ export function EmailComposer({
const trpc = useTRPC();
const { mutateAsync: aiCompose } = useMutation(trpc.ai.compose.mutationOptions());
const { mutateAsync: createDraft } = useMutation(trpc.drafts.create.mutationOptions());
const {mutateAsync: updateDraft} = useMutation(trpc.drafts.update.mutationOptions());
const {mutateAsync: deleteDraft} = useMutation(trpc.drafts.delete.mutationOptions());
const { mutateAsync: generateEmailSubject } = useMutation(
trpc.ai.generateEmailSubject.mutationOptions(),
);
Expand Down Expand Up @@ -551,6 +558,8 @@ export function EmailComposer({
}
};



const saveDraft = async () => {
const values = getValues();

Expand All @@ -576,11 +585,34 @@ export function EmailComposer({
fromEmail: values.fromEmail ? values.fromEmail : null,
};

if(draftId){
try {
const response = await updateDraft(draftData);
if(response?.id){
setDraftId(response.id);
onDraftUpdate?.();
toast.success("Your Draft has been Successfully Saved")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no toast please

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but why shouldnt we show a message for confirmation that the user draft is saved?

}
} catch (error) {
console.error("Failed to update draft:", error);
toast.error("Failed to update draft");
}
} else {
const response = await createDraft(draftData);
if(response?.id){
setDraftId(response.id);
toast.success("Your Draft has been Successfully Saved")
} else {
console.error("Failed Setting up Draft Id")
toast.error("Failed Setting up Draft Id")
}
}
const response = await createDraft(draftData);

if (response?.id && response.id !== draftId) {
setDraftId(response.id);
if(response?.id){
setDraftId(response?.id);
toast.success("Your Draft has been Successfully Saved")
}
}
} catch (error) {
console.error('Error saving draft:', error);
toast.error('Failed to save draft');
Expand All @@ -592,6 +624,53 @@ export function EmailComposer({
}
};

useEffect(() => {
if (!hasUnsavedChanges) return;

const autoSaveTimer = setTimeout(() => {
console.log('Draft Save TimeOut');
saveDraft();
}, 3000);

return () => clearTimeout(autoSaveTimer);
}, [hasUnsavedChanges, saveDraft]);

//ths function is going to be used to delete drafts
const handledeleteDraft = async () => {
const values = getValues();
if (!draftId) {
toast.error('No draft Id available to delete any Draft.');
return;
}
try{
const response = await deleteDraft({id: draftId});
if(response === ''){
setDraftId(null);
setIsComposeOpen(null);
setTimeout(() => {
toast.success("Successfully Deleted Draft");
refetchThreads();
}, 500);
}
} catch (error) {
console.error('Failed to delete draft:', error);
toast.error('Failed to delete draft.');
} finally {
setIsDeleteDraft(false);
}
};

// this handleclose button triggeres to auto-save draft upon close
const handleClose = () => {
const hasContent = editor?.getText()?.trim().length > 0;
if (hasContent) {
saveDraft();
setShowLeaveConfirmation(true);
} else {
onClose?.();
}
};

const handleGenerateSubject = async () => {
try {
setIsGeneratingSubject(true);
Expand All @@ -613,17 +692,10 @@ export function EmailComposer({
}
};

const handleClose = () => {
const hasContent = editor?.getText()?.trim().length > 0;
if (hasContent) {
setShowLeaveConfirmation(true);
} else {
onClose?.();
}
};

const confirmLeave = () => {
setShowLeaveConfirmation(false);
handledeleteDraft();
onClose?.();
};

Expand All @@ -644,16 +716,6 @@ export function EmailComposer({
};
}, [editor, showLeaveConfirmation]);

useEffect(() => {
if (!hasUnsavedChanges) return;

const autoSaveTimer = setTimeout(() => {
console.log('timeout set');
saveDraft();
}, 3000);

return () => clearTimeout(autoSaveTimer);
}, [hasUnsavedChanges, saveDraft]);

useEffect(() => {
const handlePasteFiles = (event: ClipboardEvent) => {
Expand Down Expand Up @@ -921,7 +983,7 @@ export function EmailComposer({
tabIndex={-1}
className="flex h-full items-center gap-2 text-sm font-medium text-[#8C8C8C] hover:text-[#A8A8A8]"
onClick={handleClose}
>
>
<X className="h-3.5 w-3.5 fill-[#9A9A9A]" />
</button>
)}
Expand Down Expand Up @@ -1467,7 +1529,13 @@ export function EmailComposer({
</TooltipProvider>
</div>
</div>
<div className="flex items-start justify-start gap-2">
<div className="flex items-start justify-start gap-4">
{/* <Button
className='flex p-2 hover:text-black hover:bg-white max-h-[35px] h-screen bg-black text-center text-zinc-300 text-sm'
onClick={()=>{setShowLeaveConfirmation(true)}}
disabled = {editor.getText().trim().length < 1}
>
<Trash className='w-5 h-5 rounded-md'/>Discard</Button> */}
<div className="relative">
<AnimatePresence>
{aiGeneratedMessage !== null ? (
Expand Down Expand Up @@ -1566,7 +1634,7 @@ export function EmailComposer({
Stay
</Button>
<Button variant="destructive" onClick={confirmLeave}>
Leave
<Trash className='w-5 h-5 rounded-md'/>Discard
</Button>
</DialogFooter>
</DialogContent>
Expand Down
Loading