From 50c10dac8996b32c957e5ab9382d5d27a7653a57 Mon Sep 17 00:00:00 2001 From: AnjanyKumarJaiswal <136046942+AnjanyKumarJaiswal@users.noreply.github.com> Date: Sun, 29 Jun 2025 12:35:52 +0530 Subject: [PATCH 01/10] fix: fixed conflict issues and drafts featurs are done --- apps/mail/components/create/create-email.tsx | 13 +- .../mail/components/create/email-composer.tsx | 145 +++++++++++++++--- apps/mail/components/ui/draftnotification.tsx | 142 +++++++++++++++++ apps/server/src/lib/driver/google.ts | 124 ++++++++++++--- apps/server/src/lib/driver/microsoft.ts | 88 ++++++++--- apps/server/src/lib/driver/types.ts | 6 + apps/server/src/routes/chat.ts | 22 +++ apps/server/src/trpc/routes/drafts.ts | 24 ++- 8 files changed, 485 insertions(+), 79 deletions(-) create mode 100644 apps/mail/components/ui/draftnotification.tsx diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index 74a992226e..b415f20cee 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -61,6 +61,7 @@ export function CreateEmail({ data: draft, isLoading: isDraftLoading, error: draftError, + refetch: refetchDraft } = useDraft(draftId ?? propDraftId ?? null); const t = useTranslations(); const [, setIsDraftFailed] = useState(false); @@ -165,14 +166,6 @@ export function CreateEmail({ - {isDraftLoading ? ( -
-
-
-

Loading draft...

-
-
- ) : ( { + refetchDraft(); + }} initialSubject={typedDraft?.subject || initialSubject} autofocus={false} settingsLoading={settingsLoading} /> - )} diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index fbc0571c04..7ce84042ec 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -13,7 +13,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Check, Command, Loader, Paperclip, Plus, X as XIcon } from 'lucide-react'; +import { Check, Command, Loader, Paperclip, Plus, Trash, X as XIcon } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TextEffect } from '@/components/motion-primitives/text-effect'; import { useActiveConnection } from '@/hooks/use-connections'; @@ -25,7 +25,7 @@ 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'; @@ -42,6 +42,7 @@ import { ImageCompressionSettings } from './image-compression-settings'; import { compressImages } from '@/lib/image-compression'; import type { ImageQuality } from '@/lib/image-compression'; import { useIsMobile } from '@/hooks/use-mobile'; +import { DraftNotification, useDraftNotification } from '../ui/draftnotification'; type ThreadContent = { from: string; @@ -69,6 +70,7 @@ interface EmailComposerProps { fromEmail?: string; }) => Promise; onClose?: () => void; + onDraftUpdate?: () => void; className?: string; autofocus?: boolean; settingsLoading?: boolean; @@ -101,6 +103,7 @@ export function EmailComposer({ initialAttachments = [], onSendEmail, onClose, + onDraftUpdate, className, autofocus = false, settingsLoading = false, @@ -114,6 +117,7 @@ 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 [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [messageLength, setMessageLength] = useState(0); const fileInputRef = useRef(null); @@ -206,6 +210,8 @@ export function EmailComposer({ } } }; + // const [showDraftMessage, setshowDraftMessage] = useState(""); + const { notification, showSaveNotification, hideNotification } = useDraftNotification(); // Add this function to handle clicks outside the input fields useEffect(() => { @@ -240,6 +246,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(), ); @@ -538,6 +546,8 @@ export function EmailComposer({ } }; + + //this whole method is for saving draft and its called when the useeffect is triggered const saveDraft = async () => { const values = getValues(); @@ -571,11 +581,29 @@ export function EmailComposer({ fromEmail: values.fromEmail ? values.fromEmail : null, }; + if(draftId){ + const response = await updateDraft(draftData); + if(response?.id){ + setDraftId(response?.id); + onDraftUpdate?.(); + showSaveNotification('Your Draft has been Successfully Saved'); + } + else{ + const response = await createDraft(draftData); + if(response?.id){ + setDraftId(response?.id); + showSaveNotification('Your Draft has been Successfully Saved'); + } + console.error("Failed Setting up Draft Id") + toast.error("Failed Setting up Draft Id") + } + } else { const response = await createDraft(draftData); - - if (response?.id && response.id !== draftId) { - setDraftId(response.id); + if(response?.id){ + setDraftId(response?.id); + showSaveNotification('Your Draft has been Successfully Saved'); } + } } catch (error) { console.error('Error saving draft:', error); toast.error('Failed to save draft'); @@ -587,6 +615,76 @@ export function EmailComposer({ } }; + useEffect(() => { + if (!hasUnsavedChanges) return; + + const autoSaveTimer = setTimeout(() => { + console.log('Draft Save TimeOut'); + saveDraft(); + }, 3000); + + return () => clearTimeout(autoSaveTimer); + }, [hasUnsavedChanges, saveDraft]); + + // this handleSaveclick for saving drafts might be used in future when save button is introduced + // const handleSaveClick = () =>{ + // const hasContent = editor?.getText()?.trim().length > 0; + // if(hasContent){ + // onDraftUpdate?.(); + // showSaveNotification('Your Draft has been Successfully Saved'); + // } + // } + + //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 draftData = { + to: values.to.join(', '), + cc: values.cc?.join(', '), + bcc: values.bcc?.join(', '), + subject: values.subject, + message: editor.getHTML(), + attachments: await serializeFiles(values.attachments ?? []), + id: draftId, + threadId: threadId ? threadId : null, + fromEmail: values.fromEmail ? values.fromEmail : null, + }; + + if(draftId){ + const response = await deleteDraft(draftData); + if(response === ''){ + setDraftId(null); + setIsComposeOpen(null); + setTimeout(() => { + const currentUrl = new URL(window.location.href); + window.location.href = currentUrl.toString(); + }, 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); @@ -608,14 +706,6 @@ export function EmailComposer({ } }; - const handleClose = () => { - const hasContent = editor?.getText()?.trim().length > 0; - if (hasContent) { - setShowLeaveConfirmation(true); - } else { - onClose?.(); - } - }; const confirmLeave = () => { setShowLeaveConfirmation(false); @@ -639,16 +729,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) => { @@ -901,7 +981,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} - > + > )} @@ -1428,7 +1508,13 @@ export function EmailComposer({ )} -
+
+
{aiGeneratedMessage !== null ? ( @@ -1562,6 +1648,15 @@ export function EmailComposer({ + {notification && ( + + )}
); } diff --git a/apps/mail/components/ui/draftnotification.tsx b/apps/mail/components/ui/draftnotification.tsx new file mode 100644 index 0000000000..c784163909 --- /dev/null +++ b/apps/mail/components/ui/draftnotification.tsx @@ -0,0 +1,142 @@ +import React, { useState, useEffect } from 'react'; +import { CheckCircle, Trash, X} from 'lucide-react'; + +interface DraftNotificationProps { + message: string; + type: 'save' | 'delete'; + isVisible: boolean; + onClose: () => void; + duration?: number; +} + +export const DraftNotification: React.FC = ({ + message, + type, + isVisible, + onClose, + duration = 3000 +}) => { + const [progress, setProgress] = useState(100); + const [isClosing, setIsClosing] = useState(false); + + useEffect(() => { + if (!isVisible) { + setProgress(100); + setIsClosing(false); + return; + } + + const interval = setInterval(() => { + setProgress((prev) => { + const newProgress = prev - (100 / (duration / 100)); + if (newProgress <= 0) { + clearInterval(interval); + handleClose(); + return 0; + } + return newProgress; + }); + }, 100); + + return () => clearInterval(interval); + }, [isVisible, duration]); + + const handleClose = () => { + setIsClosing(true); + setTimeout(() => { + onClose(); + }, 300); + }; + + if (!isVisible) return null; + + const isSave = type === 'save'; + + return ( +
+
+
+
+
+ +
+
+ {isSave ? ( + + ) : ( + + )} +
+ + {/* Message */} +
+

+ {message} +

+
+ + {/* Close button */} + +
+ + {/* Bottom accent line */} +
+
+
+ ); +}; + +// Hook for managing notifications +export const useDraftNotification = () => { + const [notification, setNotification] = useState<{ + type: 'save' | 'delete'; + message: string; + isVisible: boolean; + } | null>(null); + + const showSaveNotification = (message: string = 'Your Draft has been Successfully Saved', onComplete?: () => void) => { + setNotification({ + type: 'save', + message, + isVisible: true + }); + + // If onComplete callback is provided, execute it after the duration + if (onComplete) { + setTimeout(onComplete, 5000); + } + }; + + const hideNotification = () => { + setNotification(null); + }; + + return { + notification, + showSaveNotification, + hideNotification + }; +}; \ No newline at end of file diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index 8fcc4d5406..b484ae2d36 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -562,7 +562,7 @@ export class GoogleMailManager implements MailManager { const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message); const msg = createMimeMessage(); msg.setSender('me'); - // name + const to = data.to.split(', ').map((recipient: string) => { if (recipient.includes('<')) { const [name, email] = recipient.split('<'); @@ -572,12 +572,10 @@ export class GoogleMailManager implements MailManager { }); msg.setTo(to); - if (data.cc) - msg.setCc(data.cc?.split(', ').map((recipient: string) => ({ addr: recipient }))); - if (data.bcc) - msg.setBcc(data.bcc?.split(', ').map((recipient: string) => ({ addr: recipient }))); - + if (data.cc) msg.setCc(data.cc.split(', ').map((addr) => ({ addr }))); + if (data.bcc) msg.setBcc(data.bcc.split(', ').map((addr) => ({ addr }))); msg.setSubject(data.subject); + msg.addMessage({ contentType: 'text/html', data: message || '', @@ -598,9 +596,11 @@ export class GoogleMailManager implements MailManager { } } - if (data.attachments && data.attachments?.length > 0) { + if (data.attachments?.length) { for (const attachment of data.attachments) { - const base64Data = attachment.base64; + // const arrayBuffer = await attachment.arrayBuffer(); + // const base64Data = Buffer.from(arrayBuffer).toString('base64'); + const base64Data = await attachment.base64; msg.addAttachment({ filename: attachment.name, contentType: attachment.type, @@ -623,24 +623,106 @@ export class GoogleMailManager implements MailManager { }, }; - let res; + const res = await this.gmail.users.drafts.create({ + userId: 'me', + requestBody, + }); - if (data.id) { - res = await this.gmail.users.drafts.update({ - userId: 'me', - id: data.id, - requestBody, - }); - } else { - res = await this.gmail.users.drafts.create({ - userId: 'me', - requestBody, - }); + return res.data; + }, + { data } + ); + } + public updateDraft(data: CreateDraftData) { + return this.withErrorHandler( + 'updateDraft', + async () => { + if (!data.id) throw new Error('Missing draft ID for update'); + + const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message); + const msg = createMimeMessage(); + msg.setSender('me'); + + const to = data.to.split(', ').map((recipient: string) => { + if (recipient.includes('<')) { + const [name, email] = recipient.split('<'); + return { addr: email.replace('>', ''), name: name.replace('>', '') }; + } + return { addr: recipient }; + }); + + msg.setTo(to); + if (data.cc) msg.setCc(data.cc.split(', ').map((addr) => ({ addr }))); + if (data.bcc) msg.setBcc(data.bcc.split(', ').map((addr) => ({ addr }))); + msg.setSubject(data.subject); + + msg.addMessage({ + contentType: 'text/html', + data: message || '', + }); + if (inlineImages.length > 0) { + for (const image of inlineImages) { + msg.addAttachment({ + inline: true, + filename: `${image.cid}`, + contentType: image.mimeType, + data: image.data, + headers: { + 'Content-ID': `<${image.cid}>`, + 'Content-Disposition': 'inline', + }, + }); + } } + if (data.attachments?.length) { + for (const attachment of data.attachments) { + const base64Data = attachment.base64; + msg.addAttachment({ + filename: attachment.name, + contentType: attachment.type, + data: base64Data, + }); + } + } + + const mimeMessage = msg.asRaw(); + const encodedMessage = Buffer.from(mimeMessage) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + const requestBody = { + message: { + raw: encodedMessage, + threadId: data.threadId, + }, + }; + + const res = await this.gmail.users.drafts.update({ + userId: 'me', + id: data.id, + requestBody, + }); + return res.data; }, - { data }, + { data } + ); + } + public deleteDraft(data: CreateDraftData){ + return this.withErrorHandler( + 'deleteDraft', + async () =>{ + if (!data.id) throw new Error('Missing draft ID to delete'); + + const res = await this.gmail.users.drafts.delete({ + userId: 'me', + id: data.id, + }) + return res.data; + } , {data} ); } public async getUserLabels() { diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts index b6c3b857e6..599ef2bec1 100644 --- a/apps/server/src/lib/driver/microsoft.ts +++ b/apps/server/src/lib/driver/microsoft.ts @@ -636,7 +636,6 @@ export class OutlookMailManager implements MailManager { const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message); const toRecipients = Array.isArray(data.to) ? data.to : data.to.split(', '); - const outlookMessage: Message = { subject: data.subject, body: { @@ -707,35 +706,84 @@ export class OutlookMailManager implements MailManager { if (allAttachments.length > 0) { outlookMessage.attachments = allAttachments; } + const res = await this.graphClient + .api('/me/mailfolders/drafts/messages') + .post(outlookMessage); - let res; + return res; + }, + { data }, + ); + } + public updateDraft(data: CreateDraftData) { + return this.withErrorHandler( + 'updateDraft', + async () => { + if (!data.id) throw new Error('Draft ID is required to update a draft'); - if (data.id) { - try { - res = await this.graphClient - .api(`/me/mailfolders/drafts/messages/${data.id}`) - .patch(outlookMessage); - } catch (error) { - console.warn(`Failed to update draft ${data.id}, creating a new one`, error); - try { - await this.graphClient.api(`/me/mailfolders/drafts/messages/${data.id}`).delete(); - } catch (deleteError) { - console.error(`Failed to delete draft ${data.id}`, deleteError); - } + const message = await sanitizeTipTapHtml(data.message); - res = await this.graphClient - .api('/me/mailfolders/drafts/messages') - .post(outlookMessage); - } - } else { - res = await this.graphClient.api('/me/mailfolders/drafts/messages').post(outlookMessage); + const toRecipients = Array.isArray(data.to) ? data.to : data.to.split(', '); + const outlookMessage: Message = { + subject: data.subject, + body: { + contentType: 'html', + content: message || '', + }, + toRecipients: toRecipients.map((recipient) => ({ + emailAddress: { + address: typeof recipient === 'string' ? recipient : recipient.email, + name: typeof recipient === 'string' ? undefined : recipient.name || undefined, + }, + })), + }; + + if (data.cc) { + const ccRecipients = Array.isArray(data.cc) ? data.cc : data.cc.split(', '); + outlookMessage.ccRecipients = ccRecipients.map((recipient) => ({ + emailAddress: { + address: typeof recipient === 'string' ? recipient : recipient.email, + name: typeof recipient === 'string' ? undefined : recipient.name || undefined, + }, + })); + } + + if (data.bcc) { + const bccRecipients = Array.isArray(data.bcc) ? data.bcc : data.bcc.split(', '); + outlookMessage.bccRecipients = bccRecipients.map((recipient) => ({ + emailAddress: { + address: typeof recipient === 'string' ? recipient : recipient.email, + name: typeof recipient === 'string' ? undefined : recipient.name || undefined, + }, + })); } + if (data.attachments && data.attachments.length > 0) { + outlookMessage.attachments = await Promise.all( + data.attachments.map(async (file) => { + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const base64Content = buffer.toString('base64'); + + return { + '@odata.type': '#microsoft.graph.fileAttachment', + name: file.name, + contentType: file.type || 'application/octet-stream', + contentBytes: base64Content, + }; + }), + ); + } + const res = await this.graphClient + .api(`/me/mailfolders/drafts/messages/${data.id}`) + .patch(outlookMessage); + return res; }, { data }, ); } + public async getUserLabels() { try { // Get root mail folders diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index 0e442833bf..4b8dd3a80c 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -55,6 +55,12 @@ export interface MailManager { createDraft( data: CreateDraftData, ): Promise<{ id?: string | null; success?: boolean; error?: string }>; + updateDraft( + data: CreateDraftData, + ) : Promise<{id?: string | null, success?: boolean, error?: string}>; + deleteDraft( + data: CreateDraftData + ) : Promise; getDraft(id: string): Promise; listDrafts(params: { q?: string; maxResults?: number; pageToken?: string }): Promise<{ threads: { id: string; historyId: string | null; $raw: unknown }[]; diff --git a/apps/server/src/routes/chat.ts b/apps/server/src/routes/chat.ts index 35d36bd758..be8adeee49 100644 --- a/apps/server/src/routes/chat.ts +++ b/apps/server/src/routes/chat.ts @@ -209,6 +209,14 @@ export class AgentRpcDO extends RpcTarget { return await this.mainDo.createDraft(draftData); } + async updateDraft(draftData: CreateDraftData){ + return await this.mainDo.updateDraft(draftData); + } + + async deleteDraft(draftData: CreateDraftData){ + return await this.mainDo.deleteDraft(draftData); + } + async getDraft(id: string) { return await this.mainDo.getDraft(id); } @@ -720,6 +728,20 @@ export class ZeroAgent extends AIChatAgent { return await this.driver.createDraft(draftData); } + async updateDraft(draftData: CreateDraftData){ + if(!this.driver){ + throw new Error("No driver available") + } + return await this.driver.updateDraft(draftData); + } + + async deleteDraft(draftData: CreateDraftData){ + if(!this.driver){ + throw new Error("No driver available") + } + return await this.driver.deleteDraft(draftData); + } + async getDraft(id: string) { if (!this.driver) { throw new Error('No driver available'); diff --git a/apps/server/src/trpc/routes/drafts.ts b/apps/server/src/trpc/routes/drafts.ts index d3c1775a4f..74ccfe948a 100644 --- a/apps/server/src/trpc/routes/drafts.ts +++ b/apps/server/src/trpc/routes/drafts.ts @@ -10,11 +10,28 @@ export const draftsRouter = router({ const agent = await getZeroAgent(activeConnection.id); return agent.createDraft(input); }), + update: activeDriverProcedure + .input(createDraftData) + .mutation(async ({input, ctx}) =>{ + const {activeConnection} = ctx; + const agent = await getZeroAgent(activeConnection.id); + const res = agent.updateDraft(input) + return res; + }), + delete: activeDriverProcedure + .input(createDraftData) + .mutation(async({input,ctx})=>{ + const {activeConnection} = ctx; + const agent = await getZeroAgent(activeConnection.id); + const res = agent.deleteDraft(input); + return res; + }), get: activeDriverProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { const { activeConnection } = ctx; const agent = await getZeroAgent(activeConnection.id); const { id } = input; - return agent.getDraft(id) as Awaited>; + const res = agent.getDraft(id) as Awaited> + return res; }), list: activeDriverProcedure .input( @@ -28,8 +45,7 @@ export const draftsRouter = router({ const { activeConnection } = ctx; const agent = await getZeroAgent(activeConnection.id); const { q, max, pageToken } = input; - return agent.listDrafts({ q, maxResults: max, pageToken }) as Awaited< - ReturnType - >; + const res = agent.listDrafts({ q, maxResults: max, pageToken }) as Awaited>; + return res; }), }); From 0028ed945b0bd337f89965013ee0e4eba07347a9 Mon Sep 17 00:00:00 2001 From: AnjanyKumarJaiswal <136046942+AnjanyKumarJaiswal@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:22:54 +0530 Subject: [PATCH 02/10] fix: fixed conflicts --- apps/mail/components/create/create-email.tsx | 2 ++ apps/mail/components/create/email-composer.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index d8e5400254..91c490da80 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -157,6 +157,8 @@ export function CreateEmail({ } }; + + const base64ToFile = (base64: string, filename: string, mimeType: string): File | null => { try { const byteString = atob(base64); diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index c8b58a83a7..596c70fa52 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -13,6 +13,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; + +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'; import { Check, Command, Loader, Paperclip, Plus, X as XIcon } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { TextEffect } from '@/components/motion-primitives/text-effect'; @@ -72,6 +74,7 @@ interface EmailComposerProps { }) => Promise; onClose?: () => void; onDraftUpdate?: () => void; + // onDeleteDrafts?: () => void; className?: string; autofocus?: boolean; settingsLoading?: boolean; @@ -110,6 +113,7 @@ export function EmailComposer({ onSendEmail, onClose, onDraftUpdate, + // onDeleteDrafts, className, autofocus = false, settingsLoading = false, @@ -643,7 +647,7 @@ export function EmailComposer({ // } //ths function is going to be used to delete drafts - const handleDeleteDraft = async () => { + const DeleteDraft = async () => { const values = getValues(); if (!draftId) { toast.error('No draft Id available to delete any Draft.'); @@ -1552,12 +1556,12 @@ export function EmailComposer({
- + Discard */}
{aiGeneratedMessage !== null ? ( From 3551c1963d05a7e62cf5465821e072828632e1fb Mon Sep 17 00:00:00 2001 From: AnjanyKumarJaiswal <136046942+AnjanyKumarJaiswal@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:04:36 +0530 Subject: [PATCH 03/10] feat: finally done with Auto-Save , Update and Delete Drafts --- apps/mail/components/create/create-email.tsx | 4 +- .../mail/components/create/email-composer.tsx | 16 +- apps/mail/components/ui/draftnotification.tsx | 142 ------------------ 3 files changed, 13 insertions(+), 149 deletions(-) delete mode 100644 apps/mail/components/ui/draftnotification.tsx diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index f147dbe365..01c853c9cd 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -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'; @@ -62,6 +63,7 @@ export function CreateEmail({ const { mutateAsync: sendEmail } = useMutation(trpc.mail.send.mutationOptions()); const [isComposeOpen, setIsComposeOpen] = useQueryState('isComposeOpen'); const [, setThreadId] = useQueryState('threadId'); + const [{ isFetching, refetch: refetchThreads }] = useThreads(); const [, setActiveReplyId] = useQueryState('activeReplyId'); const { data: activeConnection } = useActiveConnection(); const { data: settings, isLoading: settingsLoading } = useSettings(); @@ -172,7 +174,7 @@ export function CreateEmail({
- */} @@ -1651,7 +1655,7 @@ export function EmailComposer({ Stay diff --git a/apps/mail/components/ui/draftnotification.tsx b/apps/mail/components/ui/draftnotification.tsx deleted file mode 100644 index c784163909..0000000000 --- a/apps/mail/components/ui/draftnotification.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { CheckCircle, Trash, X} from 'lucide-react'; - -interface DraftNotificationProps { - message: string; - type: 'save' | 'delete'; - isVisible: boolean; - onClose: () => void; - duration?: number; -} - -export const DraftNotification: React.FC = ({ - message, - type, - isVisible, - onClose, - duration = 3000 -}) => { - const [progress, setProgress] = useState(100); - const [isClosing, setIsClosing] = useState(false); - - useEffect(() => { - if (!isVisible) { - setProgress(100); - setIsClosing(false); - return; - } - - const interval = setInterval(() => { - setProgress((prev) => { - const newProgress = prev - (100 / (duration / 100)); - if (newProgress <= 0) { - clearInterval(interval); - handleClose(); - return 0; - } - return newProgress; - }); - }, 100); - - return () => clearInterval(interval); - }, [isVisible, duration]); - - const handleClose = () => { - setIsClosing(true); - setTimeout(() => { - onClose(); - }, 300); - }; - - if (!isVisible) return null; - - const isSave = type === 'save'; - - return ( -
-
-
-
-
- -
-
- {isSave ? ( - - ) : ( - - )} -
- - {/* Message */} -
-

- {message} -

-
- - {/* Close button */} - -
- - {/* Bottom accent line */} -
-
-
- ); -}; - -// Hook for managing notifications -export const useDraftNotification = () => { - const [notification, setNotification] = useState<{ - type: 'save' | 'delete'; - message: string; - isVisible: boolean; - } | null>(null); - - const showSaveNotification = (message: string = 'Your Draft has been Successfully Saved', onComplete?: () => void) => { - setNotification({ - type: 'save', - message, - isVisible: true - }); - - // If onComplete callback is provided, execute it after the duration - if (onComplete) { - setTimeout(onComplete, 5000); - } - }; - - const hideNotification = () => { - setNotification(null); - }; - - return { - notification, - showSaveNotification, - hideNotification - }; -}; \ No newline at end of file From f669ca349df697e037d4538791640066b0610f51 Mon Sep 17 00:00:00 2001 From: AnjanyKumarJaiswal <136046942+AnjanyKumarJaiswal@users.noreply.github.com> Date: Tue, 15 Jul 2025 00:33:58 +0530 Subject: [PATCH 04/10] fix: applied AI recommendations and implemented to code structure --- apps/mail/components/create/create-email.tsx | 9 ++++- .../mail/components/create/email-composer.tsx | 38 +++---------------- apps/server/src/lib/driver/google.ts | 8 ++-- apps/server/src/lib/driver/microsoft.ts | 2 +- apps/server/src/lib/driver/types.ts | 2 +- apps/server/src/routes/chat.ts | 12 +++--- apps/server/src/trpc/routes/drafts.ts | 7 ++-- 7 files changed, 29 insertions(+), 49 deletions(-) diff --git a/apps/mail/components/create/create-email.tsx b/apps/mail/components/create/create-email.tsx index 01c853c9cd..fd44c79fd1 100644 --- a/apps/mail/components/create/create-email.tsx +++ b/apps/mail/components/create/create-email.tsx @@ -63,7 +63,7 @@ export function CreateEmail({ const { mutateAsync: sendEmail } = useMutation(trpc.mail.send.mutationOptions()); const [isComposeOpen, setIsComposeOpen] = useQueryState('isComposeOpen'); const [, setThreadId] = useQueryState('threadId'); - const [{ isFetching, refetch: refetchThreads }] = useThreads(); + const [{refetch: refetchThreads }] = useThreads(); const [, setActiveReplyId] = useQueryState('activeReplyId'); const { data: activeConnection } = useActiveConnection(); const { data: settings, isLoading: settingsLoading } = useSettings(); @@ -174,7 +174,12 @@ export function CreateEmail({
-
- {/* */}
{aiGeneratedMessage !== null ? ( From 664ec9e42f2f3a13082dbb78c29595427fdeb4f1 Mon Sep 17 00:00:00 2001 From: Anjany Date: Wed, 23 Jul 2025 21:53:42 +0530 Subject: [PATCH 08/10] feat: updated logic recommended by ai Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/mail/components/create/email-composer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index 99589088c7..519c565eef 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -636,7 +636,7 @@ export function EmailComposer({ }, 3000); return () => clearTimeout(autoSaveTimer); - }, [hasUnsavedChanges, saveDraft]); + }, [hasUnsavedChanges]); const handledeleteDraft = async () => { From c40082923402ea95d9b7428f00a19d8a206a7c30 Mon Sep 17 00:00:00 2001 From: Anjany Date: Wed, 23 Jul 2025 21:54:21 +0530 Subject: [PATCH 09/10] feat: updated logic recommended by ai Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/server/src/trpc/routes/drafts.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/server/src/trpc/routes/drafts.ts b/apps/server/src/trpc/routes/drafts.ts index 850519c1fb..fd79214b7a 100644 --- a/apps/server/src/trpc/routes/drafts.ts +++ b/apps/server/src/trpc/routes/drafts.ts @@ -19,13 +19,11 @@ export const draftsRouter = router({ return res; }), delete: activeDriverProcedure - .input(z.object({id: z.string()})) - .mutation(async({input,ctx})=>{ - const {activeConnection} = ctx; + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + const { activeConnection } = ctx; const agent = await getZeroAgent(activeConnection.id); - const {id} = input; - const res = await agent.deleteDraft(id); - return res; + return agent.deleteDraft(input.id); }), get: activeDriverProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { const { activeConnection } = ctx; From 64da0bd79b4614cfc951c99c4642cf5a382daa0e Mon Sep 17 00:00:00 2001 From: Anjany Date: Wed, 23 Jul 2025 21:55:13 +0530 Subject: [PATCH 10/10] feat: updated logic recommended by ai Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/mail/components/create/email-composer.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/mail/components/create/email-composer.tsx b/apps/mail/components/create/email-composer.tsx index 519c565eef..726c7217b8 100644 --- a/apps/mail/components/create/email-composer.tsx +++ b/apps/mail/components/create/email-composer.tsx @@ -639,8 +639,7 @@ export function EmailComposer({ }, [hasUnsavedChanges]); - const handledeleteDraft = async () => { - const values = getValues(); + const handleDeleteDraft = async () => { if (!draftId) { toast.error('No draft Id available to delete the draft.'); return; @@ -658,11 +657,8 @@ export function EmailComposer({ } 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;