Skip to content

Comments

feat: finally done with Auto-Save , Update and Delete Drafts#1718

Closed
AnjanyKumarJaiswal wants to merge 12 commits intoMail-0:stagingfrom
AnjanyKumarJaiswal:feat/autoSave_Email_drafts
Closed

feat: finally done with Auto-Save , Update and Delete Drafts#1718
AnjanyKumarJaiswal wants to merge 12 commits intoMail-0:stagingfrom
AnjanyKumarJaiswal:feat/autoSave_Email_drafts

Conversation

@AnjanyKumarJaiswal
Copy link

@AnjanyKumarJaiswal AnjanyKumarJaiswal commented Jul 11, 2025

Auto-Save , Auto-Update and Delete Draft

Remove bullet points that are not relevant.

PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI.

  • Pull requests that do not follow these guidelines will be closed without review or comment.
  • If you use AI to write your PR description your pr will be close without review or comment.
  • If you are unsure about anything, feel free to ask for clarification.

Description

Please provide a clear description of your changes.


Type of Change

Please delete options that are not relevant.

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • [x ] ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature with breaking changes)
  • 📝 Documentation update
  • 🎨 UI/UX improvement
  • 🔒 Security enhancement
  • ⚡ Performance improvement

Areas Affected

Please check all that apply:

  • Email Integration (Gmail, IMAP, etc.)
  • [x ] User Interface/Experience
  • Authentication/Authorization
  • Data Storage/Management
  • API Endpoints
  • Documentation
  • Testing Infrastructure
  • Development Workflow
  • Deployment/Infrastructure

Testing Done

Describe the tests you've done:

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • Cross-browser testing (if UI changes)
  • Mobile responsiveness verified (if UI changes)

Security Considerations

For changes involving data or authentication:

  • No sensitive data is exposed
  • Authentication checks are in place
  • Input validation is implemented
  • Rate limiting is considered (if applicable)

Checklist

  • I have read the CONTRIBUTING document
  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in complex areas
  • I have updated the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix/feature works
  • All tests pass locally
  • Any dependent changes are merged and published

Additional Notes

Add any other context about the pull request here.

Screenshots/Recordings

Add screenshots or recordings here if applicable.


By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.


Summary by cubic

Added auto-save, update, and delete features for email drafts, allowing users to save changes automatically and remove drafts as needed.

  • New Features
    • Auto-saves draft changes after edits.
    • Allows updating existing drafts.
    • Enables deleting drafts from the composer UI.

Summary by CodeRabbit

  • New Features

    • Added the ability to update and delete email drafts directly from the email composer.
    • Introduced auto-save for email drafts after changes.
    • Added confirmation dialogs and improved UI for discarding drafts, including a new discard icon.
    • Success and error notifications are now shown for draft operations.
  • Bug Fixes

    • Improved handling of draft and thread refresh, ensuring up-to-date data after draft updates or dialog closure.
  • Refactor

    • Streamlined draft management for better reliability and user experience.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

Implements comprehensive draft management functionality across both frontend and backend components of the Zero mail application.

  • Added auto-save feature in apps/mail/components/create/email-composer.tsx with 3-second debounce and toast notifications for draft operations
  • Implemented new draft management methods (updateDraft, deleteDraft) in Microsoft and Google drivers (apps/server/src/lib/driver/*.ts)
  • Enhanced MailManager interface in apps/server/src/lib/driver/types.ts with new draft management methods
  • Added TRPC procedures for draft operations in apps/server/src/trpc/routes/drafts.ts with improved handling
  • Optimized draft fetching and updates in apps/mail/components/create/create-email.tsx with immediate composer display

7 files reviewed, 18 comments
Edit PR Review Bot Settings | Greptile

Comment on lines +60 to +62
updateDraft(
data: CreateDraftData,
) : Promise<{id?: string | null, success?: boolean, error?: string}>;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Similar return type to createDraft, but consider consolidating into a shared type definition like DraftOperationResult to maintain consistency

Comment on lines 63 to 65
deleteDraft(
data: CreateDraftData
) : Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Delete operation using full CreateDraftData seems excessive. Consider using just the draft ID

Suggested change
deleteDraft(
data: CreateDraftData
) : Promise<void>;
deleteDraft(
id: string
): Promise<void>;

.mutation(async ({input, ctx}) =>{
const {activeConnection} = ctx;
const agent = await getZeroAgent(activeConnection.id);
const res = agent.updateDraft(input)
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Missing semicolon at line end. Keep consistent with rest of codebase style.

Suggested change
const res = agent.updateDraft(input)
const res = agent.updateDraft(input);

Comment on lines 22 to 23
.input(createDraftData)
.mutation(async({input,ctx})=>{
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Delete endpoint using createDraftData schema. Should use simpler schema with just ID.

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

Choose a reason for hiding this comment

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

syntax: isFetching is destructured but never used. Remove unused variable.

Suggested change
const [{ isFetching, refetch: refetchThreads }] = useThreads();
const [{ refetch: refetchThreads }] = useThreads();

Comment on lines 225 to 227
async deleteDraft(draftData: CreateDraftData){
return await this.mainDo.deleteDraft(draftData);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Inconsistent spacing after function parameters - missing space before opening brace

Suggested change
async deleteDraft(draftData: CreateDraftData){
return await this.mainDo.deleteDraft(draftData);
}
async deleteDraft(draftData: CreateDraftData) {
return await this.mainDo.deleteDraft(draftData);
}

Comment on lines 775 to 777
if(!this.driver){
throw new Error("No driver available")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Double quotes used instead of single quotes for string literal - inconsistent with codebase style

Suggested change
if(!this.driver){
throw new Error("No driver available")
}
if(!this.driver){
throw new Error('No driver available')
}

Comment on lines 774 to 786
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Error message format inconsistent with other driver check errors in file (missing period at end)

Comment on lines +623 to +632
useEffect(() => {
if (!hasUnsavedChanges) return;

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

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

Choose a reason for hiding this comment

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

style: Complex save logic inside useEffect. Consider moving to useMemo or useCallback to optimize performance.

Suggested change
useEffect(() => {
if (!hasUnsavedChanges) return;
const autoSaveTimer = setTimeout(() => {
console.log('Draft Save TimeOut');
saveDraft();
}, 3000);
return () => clearTimeout(autoSaveTimer);
}, [hasUnsavedChanges, saveDraft]);
const saveDraftCallback = useCallback(async () => {
const values = getValues();
if (!hasUnsavedChanges) return;
const messageText = editor.getText();
if (messageText.trim() === initialMessage.trim()) return;
if (editor.getHTML() === initialMessage.trim()) return;
if (!values.to.length || !values.subject.length || !messageText.length) return;
if (aiGeneratedMessage || aiIsLoading || isGeneratingSubject) return;
try {
setIsSavingDraft(true);
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 updateDraft(draftData);
if(response?.id){
setDraftId(response?.id);
onDraftUpdate?.();
toast.success("Your Draft has been Successfully Saved")
}
else{
const response = await createDraft(draftData);
if(response?.id){
setDraftId(response?.id);
toast.success("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){
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');
setIsSavingDraft(false);
setHasUnsavedChanges(false);
} finally {
setIsSavingDraft(false);
setHasUnsavedChanges(false);
}
}, [hasUnsavedChanges, getValues, editor, initialMessage, aiGeneratedMessage, aiIsLoading, isGeneratingSubject, draftId, threadId, updateDraft, createDraft, onDraftUpdate]);
useEffect(() => {
if (!hasUnsavedChanges) return;
const autoSaveTimer = setTimeout(() => {
console.log('Draft Save TimeOut');
saveDraftCallback();
}, 3000);
return () => clearTimeout(autoSaveTimer);
}, [hasUnsavedChanges, saveDraftCallback]);

Comment on lines 644 to 682
const DeleteDraft = 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();
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);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: DeleteDraft function is unnecessarily re-serializing attachment data when only draftId is needed for deletion.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 11, 2025

Walkthrough

This change introduces full draft lifecycle management for email drafts, including explicit creation, updating, and deletion. It adds new API endpoints, server-side methods, and client-side hooks and handlers for updating and deleting drafts. The email composer now supports auto-saving, discarding, and immediate UI refreshes on draft updates or dialog closure.

Changes

File(s) Change Summary
apps/mail/components/create/create-email.tsx Integrated useThreads hook, added explicit thread refetch on dialog close, and draft refetch on update. Removed conditional draft loading UI.
apps/mail/components/create/email-composer.tsx Added onDraftUpdate prop, draft update/delete logic, auto-save, discard UI, and success/error toasts.
apps/server/src/lib/driver/google.ts Refactored draft logic: separated createDraft, added updateDraft and deleteDraft methods.
apps/server/src/lib/driver/microsoft.ts Added updateDraft method and updated createDraft for explicit API response return.
apps/server/src/lib/driver/types.ts Extended MailManager interface with updateDraft and deleteDraft methods.
apps/server/src/routes/chat.ts Added updateDraft and deleteDraft methods to AgentRpcDO and ZeroAgent classes.
apps/server/src/trpc/routes/drafts.ts Added update and delete mutations to draftsRouter for draft management.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CreateEmail
    participant EmailComposer
    participant ThreadsHook
    participant DraftsAPI

    User->>CreateEmail: Open compose dialog
    CreateEmail->>EmailComposer: Render with draft data
    EmailComposer->>DraftsAPI: Auto-save or update draft (on change)
    DraftsAPI-->>EmailComposer: Save/update response
    EmailComposer->>CreateEmail: onDraftUpdate callback
    CreateEmail->>ThreadsHook: refetchThreads (on close)
    User->>EmailComposer: Discard draft
    EmailComposer->>DraftsAPI: deleteDraft
    DraftsAPI-->>EmailComposer: Deletion response
    EmailComposer->>CreateEmail: Close dialog
Loading

Possibly related PRs

Suggested reviewers

  • ahmetskilinc
  • MrgSub

Poem

In the warren, drafts now dance and play,
Update, delete, or save—hip hip hooray!
With threads refreshed and dialogs neat,
Rabbits code fast—never miss a beat.
Compose and discard, all with ease,
This patch brings drafts a gentle breeze.
🐇💌✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 307660f and a8919cb.

📒 Files selected for processing (7)
  • apps/mail/components/create/create-email.tsx (5 hunks)
  • apps/mail/components/create/email-composer.tsx (13 hunks)
  • apps/server/src/lib/driver/google.ts (4 hunks)
  • apps/server/src/lib/driver/microsoft.ts (1 hunks)
  • apps/server/src/lib/driver/types.ts (1 hunks)
  • apps/server/src/routes/chat.ts (2 hunks)
  • apps/server/src/trpc/routes/drafts.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/server/src/trpc/routes/drafts.ts
  • apps/server/src/lib/driver/types.ts
  • apps/mail/components/create/email-composer.tsx
  • apps/server/src/lib/driver/microsoft.ts
  • apps/server/src/lib/driver/google.ts
  • apps/server/src/routes/chat.ts
  • apps/mail/components/create/create-email.tsx
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

// }

//ths function is going to be used to delete drafts
const DeleteDraft = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Function name DeleteDraft uses PascalCase, which is typically reserved for class or component names in JavaScript/TypeScript. Consider renaming to deleteDraft to follow the standard camelCase convention for functions, maintaining consistency with other functions in this file like saveDraft and handleClose.

Suggested change
const DeleteDraft = async () => {
const deleteDraft = async () => {

Spotted by Diamond (based on custom rules)

Is this helpful? React 👍 or 👎 to let us know.

.mutation(async ({input, ctx}) =>{
const {activeConnection} = ctx;
const agent = await getZeroAgent(activeConnection.id);
const res = agent.updateDraft(input)
Copy link
Contributor

Choose a reason for hiding this comment

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

The async function call to agent.updateDraft(input) is missing the await keyword. This could lead to unexpected behavior since the promise won't be properly resolved before assigning to res. The correct implementation should be:

const res = await agent.updateDraft(input);

This ensures the promise is properly awaited before continuing execution.

Suggested change
const res = agent.updateDraft(input)
const res = await agent.updateDraft(input)

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic found 5 issues across 7 files. Review them in cubic.dev

React with 👍 or 👎 to teach cubic. Tag @cubic-dev-ai to give specific feedback.

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

Choose a reason for hiding this comment

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

The isFetching variable is destructured but never used, which will trigger the no-unused-vars ESLint rule and can cause CI lint failures.

Suggested change
const [{ isFetching, refetch: refetchThreads }] = useThreads();
const [{ refetch: refetchThreads }] = useThreads();

toast.success("Your Draft has been Successfully Saved")
}
console.error("Failed Setting up Draft Id")
toast.error("Failed Setting up Draft Id")
Copy link
Contributor

Choose a reason for hiding this comment

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

Error toast is shown unconditionally, causing users to see a failure message even when the draft saved successfully.

} catch (deleteError) {
console.error(`Failed to delete draft ${data.id}`, deleteError);
}
const message = await sanitizeTipTapHtml(data.message);
Copy link
Contributor

Choose a reason for hiding this comment

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

sanitizeTipTapHtml returns an object with an html property, but the entire object is being assigned to message and later treated as a string. This will serialize to "[object Object]" in the email body sent to Graph, corrupting the draft content.

Suggested change
const message = await sanitizeTipTapHtml(data.message);
const { html: message } = await sanitizeTipTapHtml(data.message);

return agent.createDraft(input);
}),
update: activeDriverProcedure
.input(createDraftData)
Copy link
Contributor

Choose a reason for hiding this comment

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

Using createDraftData for the delete mutation allows id to be null, so the router can be called without a valid draft id and will crash inside deleteDraft. Restrict the schema to require a non-nullable id.

Suggested change
.input(createDraftData)
.input(z.object({ id: z.string() }))

<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">
Copy link
Contributor

Choose a reason for hiding this comment

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

Rule violated: Detect React performance bottlenecks and rule breaking

  Inline anonymous functions are passed directly to JSX props, violating the performance rule that discourages defining functions inside JSX. This causes a new function instance on every render, leading to unnecessary re-renders.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (14)
apps/server/src/lib/driver/types.ts (1)

63-65: Consider simplifying deleteDraft parameter

Using the full CreateDraftData object for deletion seems excessive when typically only an ID is needed for delete operations.

apps/server/src/trpc/routes/drafts.ts (1)

21-28: Simplify delete endpoint and fix formatting

The delete endpoint uses an overly complex schema when it should just use an ID.

Apply this diff to fix formatting:

-  delete: activeDriverProcedure
-    .input(createDraftData)
-    .mutation(async({input,ctx})=>{
-      const {activeConnection} = ctx;
-      const agent = await getZeroAgent(activeConnection.id);
-      const res = agent.deleteDraft(input);
-      return res;
-    }),
+  delete: activeDriverProcedure
+    .input(createDraftData)
+    .mutation(async ({ input, ctx }) => {
+      const { activeConnection } = ctx;
+      const agent = await getZeroAgent(activeConnection.id);
+      const res = await agent.deleteDraft(input);
+      return res;
+    }),
apps/mail/components/create/create-email.tsx (2)

66-66: Remove unused isFetching variable

The isFetching variable is destructured but never used in the component.


177-177: Consider optimizing the refetch call

The inline function call could cause unnecessary re-renders. Consider using a useCallback hook.

apps/server/src/routes/chat.ts (3)

221-223: Fix spacing consistency

Missing space before opening brace - inconsistent with codebase style.


225-227: Fix spacing consistency

Missing space before opening brace - inconsistent with codebase style.


774-786: Fix quote consistency and error message format

The implementation uses double quotes and inconsistent error message formatting.

Apply this diff to fix the issues:

-  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 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);
+  }
apps/server/src/lib/driver/microsoft.ts (2)

724-724: Fix HTML and inline images destructuring.

The sanitizeTipTapHtml function returns both HTML and inline images. This needs to be destructured properly.

Apply this diff:

-        const message = await sanitizeTipTapHtml(data.message);
+        const { html: message, inlineImages } = await sanitizeTipTapHtml(data.message);

761-776: Add inline image handling to updateDraft.

The updateDraft method is missing inline image handling that exists in createDraft. This could cause inline images to be lost when updating drafts.

Add inline image handling after line 760:

         }
 
+        const allAttachments = [];
+
+        if (inlineImages.length > 0) {
+          for (const image of inlineImages) {
+            allAttachments.push({
+              '@odata.type': '#microsoft.graph.fileAttachment',
+              name: image.cid,
+              contentType: image.mimeType,
+              contentBytes: image.data,
+              contentId: image.cid,
+              isInline: true,
+            });
+          }
+        }
+
         if (data.attachments && data.attachments.length > 0) {
-          outlookMessage.attachments = await Promise.all(
+          const regularAttachments = 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,
               };
             }),
           );
+          allAttachments.push(...regularAttachments);
+        }
+
+        if (allAttachments.length > 0) {
+          outlookMessage.attachments = allAttachments;
         }
apps/mail/components/create/email-composer.tsx (2)

564-632: Optimize saveDraft with useCallback to prevent unnecessary re-renders.

The saveDraft function is used as a dependency in useEffect but isn't memoized, which can cause unnecessary effect re-runs.

Wrap the saveDraft function with useCallback:

-  const saveDraft = async () => {
+  const saveDraft = useCallback(async () => {
     const values = getValues();
 
     if (!hasUnsavedChanges) return;
     // ... rest of the function
-  };
+  }, [hasUnsavedChanges, getValues, editor, initialMessage, aiGeneratedMessage, aiIsLoading, isGeneratingSubject, draftId, threadId, updateDraft, createDraft, onDraftUpdate]);

644-682: Remove unnecessary data serialization in DeleteDraft.

The DeleteDraft function only needs the draftId but is creating and serializing the entire draft data including attachments.

Simplify the function to only use the required draftId:

   const DeleteDraft = 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);
+      const response = await deleteDraft({ id: draftId });
         if(response === ''){
           setDraftId(null); 
           setIsComposeOpen(null);
           setTimeout(() => {
-          // const currentUrl = new URL(window.location.href);
-          // window.location.href = currentUrl.toString();
           toast.success("Successfully Deleted Draft");
           refetchThreads();
         }, 500);
         } 
-        }
     } catch (error) {
apps/server/src/lib/driver/google.ts (3)

607-611: Verify that attachment.base64 is a Promise before awaiting

The code awaits attachment.base64, but this may not be a Promise if the attachment object already contains a base64 string.

Run the following script to check how attachments are structured in the codebase:

#!/bin/bash
# Description: Check attachment structure and base64 property usage

# Search for attachment type definitions and interfaces
ast-grep --pattern 'interface $_ {
  $$$
  base64: $_;
  $$$
}'

# Search for places where attachments are created or processed
rg -A 5 "attachments.*base64" --type ts

722-735: Fix inconsistent spacing in function declaration

The function has spacing issues that affect code readability and consistency.

Apply this diff to fix the spacing:

-  public deleteDraft(data: CreateDraftData){
+  public deleteDraft(data: CreateDraftData) {
     return this.withErrorHandler(
       'deleteDraft',
-      async () =>{
+      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}
+      }, { data }
     );
   }

644-721: Extract common MIME message creation logic to reduce duplication

The updateDraft method duplicates most of the logic from createDraft. This violates the DRY principle and makes maintenance difficult.

Additionally, there's an inconsistency in attachment handling: createDraft awaits attachment.base64 (line 611) while updateDraft doesn't (line 688).

Extract the common MIME message creation logic into a private helper method:

+  private async createMimeMessageForDraft(data: CreateDraftData) {
+    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 = await 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(/=+$/, '');
+
+    return {
+      raw: encodedMessage,
+      threadId: data.threadId,
+    };
+  }

Then simplify both methods:

 public createDraft(data: CreateDraftData) {
   return this.withErrorHandler(
     'createDraft',
     async () => {
-      // ... all the duplicated code ...
+      const { raw, threadId } = await this.createMimeMessageForDraft(data);
+      const res = await this.gmail.users.drafts.create({
+        userId: 'me',
+        requestBody: {
+          message: { raw, threadId },
+        },
+      });
+      return res.data;
     },
     { data }
   );
 }

 public updateDraft(data: CreateDraftData) {
   return this.withErrorHandler(
     'updateDraft',
     async () => {
       if (!data.id) throw new Error('Missing draft ID for update');
-      // ... all the duplicated code ...
+      const { raw, threadId } = await this.createMimeMessageForDraft(data);
+      const res = await this.gmail.users.drafts.update({
+        userId: 'me',
+        id: data.id,
+        requestBody: {
+          message: { raw, threadId },
+        },
+      });
+      return res.data;
     },
     { data }
   );
 }
🧹 Nitpick comments (1)
apps/server/src/trpc/routes/drafts.ts (1)

13-20: Fix missing semicolon and formatting inconsistencies

The implementation is correct but has style issues that need to be addressed.

Apply this diff to fix the styling:

-  update: activeDriverProcedure
-    .input(createDraftData)
-    .mutation(async ({input, ctx}) =>{
-      const {activeConnection} = ctx;
-      const agent = await getZeroAgent(activeConnection.id);
-      const res = agent.updateDraft(input)
-      return res;
-    }),
+  update: activeDriverProcedure
+    .input(createDraftData)
+    .mutation(async ({ input, ctx }) => {
+      const { activeConnection } = ctx;
+      const agent = await getZeroAgent(activeConnection.id);
+      const res = await agent.updateDraft(input);
+      return res;
+    }),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ceab96 and 50d99eb.

📒 Files selected for processing (7)
  • apps/mail/components/create/create-email.tsx (5 hunks)
  • apps/mail/components/create/email-composer.tsx (13 hunks)
  • apps/server/src/lib/driver/google.ts (4 hunks)
  • apps/server/src/lib/driver/microsoft.ts (1 hunks)
  • apps/server/src/lib/driver/types.ts (1 hunks)
  • apps/server/src/routes/chat.ts (2 hunks)
  • apps/server/src/trpc/routes/drafts.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
apps/server/src/lib/driver/types.ts (1)
Learnt from: retrogtx
PR: Mail-0/Zero#1468
File: apps/server/src/trpc/routes/mail.ts:386-391
Timestamp: 2025-06-27T04:59:29.731Z
Learning: In apps/server/src/trpc/routes/mail.ts, the attachment processing logic conditionally handles mixed attachment types - it preserves existing File-like objects with arrayBuffer methods while only converting serialized attachments that need processing through toAttachmentFiles.
apps/mail/components/create/create-email.tsx (2)
Learnt from: retrogtx
PR: Mail-0/Zero#1328
File: apps/mail/lib/hotkeys/mail-list-hotkeys.tsx:202-209
Timestamp: 2025-06-18T17:26:50.918Z
Learning: In apps/mail/lib/hotkeys/mail-list-hotkeys.tsx, the switchCategoryByIndex function using hardcoded indices for category hotkeys does not break when users reorder categories, contrary to the theoretical index-shifting issue. The actual implementation has constraints or mechanisms that prevent hotkey targeting issues.
Learnt from: retrogtx
PR: Mail-0/Zero#1354
File: apps/mail/components/ui/prompts-dialog.tsx:85-88
Timestamp: 2025-06-20T05:03:16.944Z
Learning: In React Hook Form, avoid using useEffect for form state synchronization when the values prop can handle reactive updates automatically. The values prop is specifically designed for this purpose and is more optimal than manual useEffect-based synchronization.
apps/server/src/lib/driver/google.ts (1)
Learnt from: retrogtx
PR: Mail-0/Zero#1468
File: apps/server/src/trpc/routes/mail.ts:386-391
Timestamp: 2025-06-27T04:59:29.731Z
Learning: In apps/server/src/trpc/routes/mail.ts, the attachment processing logic conditionally handles mixed attachment types - it preserves existing File-like objects with arrayBuffer methods while only converting serialized attachments that need processing through toAttachmentFiles.
apps/mail/components/create/email-composer.tsx (2)
Learnt from: retrogtx
PR: Mail-0/Zero#1354
File: apps/mail/components/ui/prompts-dialog.tsx:85-88
Timestamp: 2025-06-20T05:03:16.944Z
Learning: In React Hook Form, avoid using useEffect for form state synchronization when the values prop can handle reactive updates automatically. The values prop is specifically designed for this purpose and is more optimal than manual useEffect-based synchronization.
Learnt from: retrogtx
PR: Mail-0/Zero#1468
File: apps/server/src/trpc/routes/mail.ts:386-391
Timestamp: 2025-06-27T04:59:29.731Z
Learning: In apps/server/src/trpc/routes/mail.ts, the attachment processing logic conditionally handles mixed attachment types - it preserves existing File-like objects with arrayBuffer methods while only converting serialized attachments that need processing through toAttachmentFiles.
🧬 Code Graph Analysis (3)
apps/server/src/lib/driver/types.ts (1)
apps/server/src/lib/schemas.ts (1)
  • CreateDraftData (37-37)
apps/mail/components/create/create-email.tsx (2)
apps/mail/hooks/use-drafts.ts (1)
  • useDraft (5-15)
apps/mail/hooks/use-threads.ts (1)
  • useThreads (13-61)
apps/server/src/routes/chat.ts (1)
apps/server/src/lib/schemas.ts (1)
  • CreateDraftData (37-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (5)
apps/server/src/lib/driver/types.ts (1)

60-62: LGTM: Consistent return type with createDraft

The return type structure is consistent with the existing createDraft method, maintaining API consistency.

apps/mail/components/create/create-email.tsx (1)

208-210: Well-integrated draft update callback

The onDraftUpdate prop properly connects the draft update functionality with the UI refetch logic, ensuring data consistency.

apps/server/src/routes/chat.ts (1)

767-786: Well-implemented draft lifecycle methods

The methods properly implement the draft update and delete functionality with appropriate error handling and delegation patterns consistent with the existing codebase.

apps/server/src/lib/driver/microsoft.ts (1)

709-717: LGTM! Good practice to explicitly return the response.

The explicit return of the draft creation response improves code clarity and aligns with the updateDraft method pattern.

apps/mail/components/create/email-composer.tsx (1)

685-693: Good implementation of auto-save on close.

The handleClose function properly saves the draft before showing the leave confirmation dialog, ensuring user data is preserved.

Comment on lines 643 to 644
//ths function is going to be used to delete drafts
const DeleteDraft = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The function name DeleteDraft uses PascalCase instead of the standard camelCase convention for JavaScript/TypeScript functions. Consider renaming it to deleteDraft to maintain consistent naming conventions throughout the codebase. This will improve code readability and align with the existing style where other functions like saveDraft and handleClose follow camelCase.

Suggested change
//ths function is going to be used to delete drafts
const DeleteDraft = async () => {
//this function is going to be used to delete drafts
const deleteDraft = async () => {

Spotted by Diamond (based on custom rules)

Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +589 to +604
if(draftId){
const response = await updateDraft(draftData);
if(response?.id){
setDraftId(response?.id);
onDraftUpdate?.();
toast.success("Your Draft has been Successfully Saved")
}
else{
const response = await createDraft(draftData);
if(response?.id){
setDraftId(response?.id);
toast.success("Your Draft has been Successfully Saved")
}
console.error("Failed Setting up Draft Id")
toast.error("Failed Setting up Draft Id")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The error handling logic in this section has a structural issue. When updateDraft fails, the fallback to createDraft is nested inside the else block, making it inaccessible when response?.id is falsy. Consider restructuring to:

if (draftId) {
  try {
    const response = await updateDraft(draftData);
    if (response?.id) {
      setDraftId(response.id);
      onDraftUpdate?.();
      toast.success("Your Draft has been Successfully Saved");
    } else {
      throw new Error("Update failed - missing ID in response");
    }
  } catch (error) {
    // Fall back to creating a new draft if update fails
    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");
    }
  }
} else {
  // No existing draft ID, create new
  const response = await createDraft(draftData);
  // ...rest of create logic
}

This ensures proper error handling and fallback behavior when updating drafts fails.

Suggested change
if(draftId){
const response = await updateDraft(draftData);
if(response?.id){
setDraftId(response?.id);
onDraftUpdate?.();
toast.success("Your Draft has been Successfully Saved")
}
else{
const response = await createDraft(draftData);
if(response?.id){
setDraftId(response?.id);
toast.success("Your Draft has been Successfully Saved")
}
console.error("Failed Setting up Draft Id")
toast.error("Failed Setting up Draft Id")
}
if(draftId){
try {
const response = await updateDraft(draftData);
if(response?.id){
setDraftId(response.id);
onDraftUpdate?.();
toast.success("Your Draft has been Successfully Saved")
} else {
throw new Error("Update failed - missing ID in response");
}
} catch (error) {
// Fall back to creating a new draft if update fails
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")
}
}
} else {
// No existing draft ID, create new
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")
}
}

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

.mutation(async({input,ctx})=>{
const {activeConnection} = ctx;
const agent = await getZeroAgent(activeConnection.id);
const res = agent.deleteDraft(input);
Copy link
Contributor

Choose a reason for hiding this comment

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

The deleteDraft call is missing the await keyword, which means this function will return a Promise rather than the resolved value. This could lead to unexpected behavior when the caller tries to use the return value. Consider updating to:

const res = await agent.deleteDraft(input);

This ensures the operation completes before returning the result to the caller.

Suggested change
const res = agent.deleteDraft(input);
const res = await agent.deleteDraft(input);

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

.mutation(async ({input, ctx}) =>{
const {activeConnection} = ctx;
const agent = await getZeroAgent(activeConnection.id);
const res = agent.updateDraft(input);
Copy link
Contributor

Choose a reason for hiding this comment

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

The updateDraft call is missing the await keyword. Since this is an asynchronous operation, without await it will return a Promise rather than the resolved value:

const res = await agent.updateDraft(input);

This ensures the response contains the actual data rather than a Promise object, which would cause issues when the client tries to access properties like id from the response.

Suggested change
const res = agent.updateDraft(input);
const res = await agent.updateDraft(input);

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant