Skip to content

Comments

ui: add cross in the undo email toast, decrease time from 30s to 15s#1979

Merged
MrgSub merged 1 commit intostagingfrom
x-fifteen
Aug 11, 2025
Merged

ui: add cross in the undo email toast, decrease time from 30s to 15s#1979
MrgSub merged 1 commit intostagingfrom
x-fifteen

Conversation

@retrogtx
Copy link
Contributor

@retrogtx retrogtx commented Aug 11, 2025

Summary by cubic

Added a close (cross) button to the undo email toast and reduced the undo window from 30 seconds to 15 seconds.

  • UI Improvements
  • The undo toast now shows a close button for quick dismissal.
  • The undo option is available for 15 seconds instead of 30.

Summary by CodeRabbit

  • New Features

    • Improved Undo experience: separate flows for scheduled vs. immediate sends with clearer toasts. Undoing a scheduled send cancels scheduling; undoing an immediate send restores the draft (including attachments) and reopens the composer. Toasts now last 15 seconds and include a close button.
    • Default delay before processing a send/schedule reduced from 30 seconds to 15 seconds.
  • Bug Fixes

    • Prevented negative time remaining, ensuring Undo is reliably available and consistent with the 5-second gating.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Walkthrough

The undo-send hook now branches for scheduled vs immediate sends, clamps negative time remaining to zero, standardizes 15s undo/toast behavior, and refines toast actions and post-undo restoration. Server-side, the default scheduling delay is reduced from 30s to 15s when no scheduleAt is provided.

Changes

Cohort / File(s) Summary
Undo-send hook logic
apps/mail/hooks/use-undo-send.ts
Clamps timeRemaining to non-negative; introduces wasUserScheduled; splits toast/undo flows for scheduled vs immediate; adds 15,000 ms toast duration and closeButton; on undo of immediate send, serializes attachments, stores undoEmailData, updates URL to reopen composer; maintains gating when timeRemaining > 5,000.
Server scheduling timing
apps/server/src/trpc/routes/mail.ts
Changes default targetTime when scheduling enabled and scheduleAt absent from +30s to +15s, affecting rawDelaySeconds and the path between scheduling vs immediate queueing; no API surface changes.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as Mail UI (Composer)
  participant Hook as use-undo-send
  participant Server

  User->>UI: Send
  UI->>Hook: onSend(emailData)
  alt emailData.scheduleAt provided
    Hook->>Hook: wasUserScheduled = true
    Hook->>UI: Show "Email scheduled" toast (15s, Undo)
    opt User clicks Undo within window
      Hook->>Server: unsendEmail(messageId)
      Server-->>Hook: success/failure
      Hook->>UI: Show "Schedule cancelled"/"Failed to cancel"
    end
  else immediate send (no scheduleAt)
    Hook->>Hook: wasUserScheduled = false
    Hook->>UI: Show "Email sent" toast (15s, Undo)
    opt User clicks Undo within window
      Hook->>Server: unsendEmail(messageId)
      Server-->>Hook: success/failure
      Hook->>Hook: serialize attachments -> undoEmailData
      Hook->>UI: Update URL params to reopen composer
      Hook->>UI: Show "Send cancelled"/"Failed to cancel"
    end
  end
Loading
sequenceDiagram
  participant UI as Mail UI
  participant Server
  UI->>Server: sendMail({ scheduleAt? })
  alt scheduleAt provided
    Server->>Server: targetTime = scheduleAt
  else no scheduleAt, scheduling enabled
    Server->>Server: targetTime = now + 15s (was 30s)
  end
  Server->>Server: rawDelaySeconds = floor((targetTime - now)/1000)
  Server-->>UI: queued/scheduled response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–20 minutes

Possibly related PRs

Suggested labels

design

Suggested reviewers

  • MrgSub
  • ahmetskilinc

Poem

Fifteen seconds on the launchpad, go for send—then wait.
A tap on Undo bends time back; destiny can hesitate.
Scheduled birds hold in the queue, T-15 to fly,
One click cancels the countdown, clears the sky.
Code whispers: not yet—compose, retry. 🚀

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch x-fifteen

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.
  • 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.

Support

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

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 generate unit tests to generate unit tests for 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.

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 analysis

1 issue found across 2 files • Review in cubic

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

}
},
},
duration: 15_000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Toast duration is hard-coded to 15 000 ms instead of matching the computed timeRemaining, so the undo button may remain visible after the backend window has closed

Prompt for AI agents
Address the following comment on apps/mail/hooks/use-undo-send.ts at line 88:

<comment>Toast duration is hard-coded to 15 000 ms instead of matching the computed timeRemaining, so the undo button may remain visible after the backend window has closed</comment>

<file context>
@@ -68,13 +68,31 @@ export const useUndoSend = () =&gt; {
     if (isSendResult(result) &amp;&amp; settings?.settings?.undoSendEnabled) {
       const { messageId, sendAt } = result;
 
-      const timeRemaining = sendAt ? sendAt - Date.now() : 15_000;
+      const timeRemaining = sendAt ? Math.max(0, sendAt - Date.now()) : 15_000;
+      const wasUserScheduled = Boolean(emailData?.scheduleAt);
 
       if (timeRemaining &gt; 5_000) {
-        toast.success(&#39;Email scheduled&#39;, {
</file context>
Suggested change
duration: 15_000,
duration: timeRemaining,

@coderabbitai coderabbitai bot added the design Improvements & changes to design & UX label Aug 11, 2025
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: 1

🔭 Outside diff range comments (1)
apps/mail/hooks/use-undo-send.ts (1)

93-124: Decouple undo cancellation from attachment restore; fix misleading error toast.

Right now, any failure during serialization/storage will show “Failed to cancel,” even if unsend succeeded. Let’s separate concerns: show failure only if unsend fails; treat restore failures as non-blocking warnings. Also add a small guard to avoid blowing up localStorage when attachments are large.

Apply this diff:

-              onClick: async () => {
-              try {
-                await unsendEmail({ messageId });
-                
-                if (emailData) {
-                  const serializedAttachments = await serializeFiles(emailData.attachments);
-                  const serializableData: SerializableEmailData = {
-                    ...emailData,
-                    attachments: serializedAttachments,
-                  };
-                  localStorage.setItem('undoEmailData', JSON.stringify(serializableData));
-                }
-                
-                const url = new URL(window.location.href);
-                url.searchParams.delete('activeReplyId');
-                url.searchParams.delete('mode');
-                url.searchParams.delete('draftId');
-                url.searchParams.set('isComposeOpen', 'true');
-                window.history.replaceState({}, '', url.toString());
-                
-                toast.info('Send cancelled');
-              } catch {
-                toast.error('Failed to cancel');
-              }
-              },
+              onClick: async () => {
+                // 1) Cancel send. If this fails, bail early with an error toast.
+                try {
+                  await unsendEmail({ messageId });
+                } catch {
+                  toast.error('Failed to cancel');
+                  return;
+                }
+
+                // 2) Best-effort restore of compose state (non-blocking for UX).
+                try {
+                  if (emailData) {
+                    // Avoid localStorage quota explosions; ~1.5MB cap.
+                    const totalBytes = emailData.attachments?.reduce((s, f) => s + (f?.size || 0), 0) || 0;
+                    if (totalBytes <= 1.5 * 1024 * 1024) {
+                      const serializedAttachments = await serializeFiles(emailData.attachments);
+                      const serializableData: SerializableEmailData = {
+                        ...emailData,
+                        attachments: serializedAttachments,
+                      };
+                      localStorage.setItem('undoEmailData', JSON.stringify(serializableData));
+                    } else {
+                      console.warn('[undo-send] Attachments too large to persist in localStorage; skipping restore.');
+                    }
+                  }
+                } catch (e) {
+                  console.warn('[undo-send] Failed to serialize/store attachments for undo restore', e);
+                  // Continue; unsend already succeeded.
+                }
+
+                // 3) Reopen composer via URL flags.
+                const url = new URL(window.location.href);
+                url.searchParams.delete('activeReplyId');
+                url.searchParams.delete('mode');
+                url.searchParams.delete('draftId');
+                url.searchParams.set('isComposeOpen', 'true');
+                window.history.replaceState({}, '', url.toString());
+
+                toast.info('Send cancelled');
+              },
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between df4f38d and 74393dd.

📒 Files selected for processing (2)
  • apps/mail/hooks/use-undo-send.ts (2 hunks)
  • apps/server/src/trpc/routes/mail.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{css,js,ts,jsx,tsx,mdx}

📄 CodeRabbit Inference Engine (.cursor/rules/tailwind-css-v4.mdc)

**/*.{css,js,ts,jsx,tsx,mdx}: Chain variants together for composable variants (e.g., group-has-data-potato:opacity-100).
Use new variants such as starting, not-*, inert, nth-*, in-*, open (for :popover-open), and ** for all descendants.
Do not use deprecated utilities like bg-opacity-*, text-opacity-*, border-opacity-*, and divide-opacity-*; use the new syntax (e.g., bg-black/50).
Use renamed utilities: shadow-sm is now shadow-xs, shadow is now shadow-sm, drop-shadow-sm is now drop-shadow-xs, drop-shadow is now drop-shadow-sm, blur-sm is now blur-xs, blur is now blur-sm, rounded-sm is now rounded-xs, rounded is now rounded-sm, outline-none is now outline-hidden.
Use bg-(--brand-color) syntax for CSS variables in arbitrary values instead of bg-[--brand-color].
Stacked variants now apply left-to-right instead of right-to-left.

Files:

  • apps/server/src/trpc/routes/mail.ts
  • apps/mail/hooks/use-undo-send.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.{js,jsx,ts,tsx}: Use 2-space indentation
Use single quotes
Limit lines to 100 characters
Semicolons are required

Files:

  • apps/server/src/trpc/routes/mail.ts
  • apps/mail/hooks/use-undo-send.ts
**/*.{js,jsx,ts,tsx,css}

📄 CodeRabbit Inference Engine (AGENT.md)

Use Prettier with sort-imports and Tailwind plugins

Files:

  • apps/server/src/trpc/routes/mail.ts
  • apps/mail/hooks/use-undo-send.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (AGENT.md)

Enable TypeScript strict mode

Files:

  • apps/server/src/trpc/routes/mail.ts
  • apps/mail/hooks/use-undo-send.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: AnjanyKumarJaiswal
PR: Mail-0/Zero#1732
File: apps/mail/components/create/email-composer.tsx:634-657
Timestamp: 2025-07-15T03:31:14.991Z
Learning: In draft deletion operations, using setTimeout with a delay (like 500ms) before showing success toast notifications improves UX by allowing UI state changes (like closing composers and clearing IDs) to complete before displaying the toast, preventing jarring immediate toast appearances that could disappear quickly during interface transitions.
Learnt from: retrogtx
PR: Mail-0/Zero#1573
File: apps/mail/components/create/template-button.tsx:197-216
Timestamp: 2025-07-28T05:37:50.566Z
Learning: retrogtx prefers less nitpicky code review suggestions and may dismiss detailed accessibility/best practice recommendations with casual responses like "bro please".
📚 Learning: 2025-06-28T03:56:09.376Z
Learnt from: retrogtx
PR: Mail-0/Zero#1468
File: apps/server/src/trpc/routes/mail.ts:331-331
Timestamp: 2025-06-28T03:56:09.376Z
Learning: In apps/server/src/trpc/routes/mail.ts, the user indicated they are not using ISO format for the scheduleAt parameter, despite the frontend code showing toISOString() usage in the ScheduleSendPicker component.

Applied to files:

  • apps/server/src/trpc/routes/mail.ts
📚 Learning: 2025-06-27T04:59:29.731Z
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.

Applied to files:

  • apps/server/src/trpc/routes/mail.ts
📚 Learning: 2025-07-15T03:31:14.991Z
Learnt from: AnjanyKumarJaiswal
PR: Mail-0/Zero#1732
File: apps/mail/components/create/email-composer.tsx:634-657
Timestamp: 2025-07-15T03:31:14.991Z
Learning: In draft deletion operations, using setTimeout with a delay (like 500ms) before showing success toast notifications improves UX by allowing UI state changes (like closing composers and clearing IDs) to complete before displaying the toast, preventing jarring immediate toast appearances that could disappear quickly during interface transitions.

Applied to files:

  • apps/mail/hooks/use-undo-send.ts
⏰ 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). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
apps/mail/hooks/use-undo-send.ts (3)

71-73: Clamp + 15s default: robust and consistent.

Clamping negative time and defaulting to 15_000 makes this resilient and consistent with server. The wasUserScheduled flag is the right axis for branching UX.


93-124: undoEmailData is properly cleared after consumption

I’ve confirmed that localStorage.removeItem('undoEmailData') is invoked in apps/mail/components/create/create-email.tsx (around line 151), so the data is purged once it’s used. No further changes needed here—onward and upward! 🚀


75-91: closeButton prop validated: Ran a repo-wide audit—closeButton is our standard toast option and dismissible doesn’t exist. Sonner supports closeButton in our version. Let’s launch!

Comment on lines 516 to 521
targetTime = parsedTime;
} else {
targetTime = Date.now() + 30_000;
targetTime = Date.now() + 15_000;
}

const rawDelaySeconds = Math.floor((targetTime - Date.now()) / 1000);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

15s default undo window set server-side — aligned and faster.

Good change. This halves the latency tax on users and syncs with the new UI toast duration.

Run a quick sweep to ensure no 30s remnants linger elsewhere (server or client):


🏁 Script executed:

#!/bin/bash
# Find hard-coded 30s durations/delays
rg -n --hidden -S '30_000|30000|duration:\s*30000|delaySeconds\W*30'

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Inspect the usage of the 30s RESYNC_COOLDOWN_MS constant in mail.ts
rg -n -C 3 'RESYNC_COOLDOWN_MS' apps/server/src/trpc/routes/mail.ts

Length of output: 553


Remaining 30 s constant in mail.ts must be addressed

Quick sweep turned up one true 30 000 ms delay in your TRPC route:

• apps/server/src/trpc/routes/mail.ts (around line 174):
const RESYNC_COOLDOWN_MS = 30000;

Please confirm whether this should be reduced to 15 000 ms to match the new UI toast duration—or left intentionally at 30 s if it serves a different purpose.

(The “cpu_ms”: 300000 in wrangler.jsonc is just your worker’s CPU limit and can be ignored here.)

🤖 Prompt for AI Agents
In apps/server/src/trpc/routes/mail.ts around lines 516 to 521, there is a
hardcoded 30_000 ms cooldown constant (RESYNC_COOLDOWN_MS) that conflicts with
the new UI toast duration; change RESYNC_COOLDOWN_MS from 30000 to 15000 to
match the UI (or, if 30s is intentional for a different reason, add a clear
comment explaining the intentional difference and leave it as-is), and update
any dependent calculations or tests that assume 30_000 ms to use the new value.

Copy link
Collaborator

MrgSub commented Aug 11, 2025

Merge activity

  • Aug 11, 5:32 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Aug 11, 5:33 PM UTC: @MrgSub merged this pull request with Graphite.

@MrgSub MrgSub merged commit 6e93fad into staging Aug 11, 2025
7 checks passed
@MrgSub MrgSub deleted the x-fifteen branch August 11, 2025 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

design Improvements & changes to design & UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants