Skip to content

Comments

feat: implement smooth infinite email deletion experience#1609

Closed
retrogtx wants to merge 1 commit intoMail-0:stagingfrom
retrogtx:infinite-delete
Closed

feat: implement smooth infinite email deletion experience#1609
retrogtx wants to merge 1 commit intoMail-0:stagingfrom
retrogtx:infinite-delete

Conversation

@retrogtx
Copy link
Contributor

@retrogtx retrogtx commented Jul 3, 2025

before:
https://github.com/user-attachments/assets/47b8d395-d772-484c-9861-79d7f28b43ac

after:
https://github.com/user-attachments/assets/3d6e03d8-7b5c-4465-ac84-b5bc656a3f15

Summary by CodeRabbit

  • New Features

    • Mail count statistics now reflect real-time changes when moving or deleting threads, providing more accurate counts during optimistic updates.
    • Additional pages of mail threads are automatically prefetched for smoother navigation and faster loading.
  • Bug Fixes

    • Improved navigation when threads are deleted, ensuring focus moves correctly and no errors occur.
    • Threads being moved out of a folder are now immediately hidden from the thread list for a more responsive experience.
  • Refactor

    • Sidebar labels and navigation components now fetch their own statistics internally, simplifying how data is passed between components.
    • Optimized hotkey handling for better performance.
  • Chores

    • Updated internal data structures to support enhanced optimistic updates and statistics adjustments.

- Filter deleted threads at list level to prevent UI gaps
- Fix navigation to jump to immediate next email (not skip by 2)
- Add aggressive prefetching to maintain thread buffer
- Ensure seamless deletion flow with no blank screens
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 3, 2025

Walkthrough

This update introduces optimistic UI improvements for thread actions in the mail app. It adds a new useAdjustedStats hook to reflect optimistic changes in folder counts, updates thread filtering and navigation logic to handle thread moves and deletions, and simplifies prop usage in sidebar components. Several components and hooks now internally manage optimistic state.

Changes

File(s) Change Summary
apps/mail/hooks/use-stats.ts Added useAdjustedStats hook to adjust stats with optimistic actions.
apps/mail/store/optimistic-updates.ts Extended OptimisticAction type with source; added placeholder adjustedStatsAtom.
apps/mail/hooks/use-optimistic-actions.ts Added source property to optimistic move and delete action objects.
apps/mail/hooks/use-threads.ts Integrated optimistic move filtering, added thread hiding, and enhanced data prefetching logic.
apps/mail/hooks/use-mail-navigation.ts Improved navigation to handle thread deletion, track previous thread ID, and refactored palette state tracking.
apps/mail/lib/hotkeys/navigation-hotkeys.tsx Memoized handlers and shortcuts for performance optimization.
apps/mail/lib/hotkeys/thread-display-hotkeys.tsx Switched to optimistic thread deletion using useOptimisticActions.
apps/mail/components/ui/app-sidebar.tsx
apps/mail/components/ui/nav-main.tsx
Updated to use useAdjustedStats instead of useStats.
apps/mail/components/ui/sidebar-labels.tsx Removed stats prop; now uses useAdjustedStats internally.
apps/mail/components/mail/mail-list.tsx Simplified conditional rendering for ThreadContextMenu in Thread component.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI
    participant useOptimisticActions
    participant useAdjustedStats
    participant Server

    User->>UI: Move or delete thread
    UI->>useOptimisticActions: Create optimistic action (with source)
    useOptimisticActions-->>UI: Update optimistic state
    UI->>useAdjustedStats: Read stats + optimistic actions
    useAdjustedStats-->>UI: Return adjusted stats
    UI->>Server: Perform actual move/delete
    Server-->>UI: Confirm or reject
    UI->>useOptimisticActions: Reconcile optimistic state
Loading

Possibly related PRs

  • thread movement & context menu #498: Introduces and modifies the ThreadContextMenu component and its integration within mail-list.tsx, directly related to simplified conditional rendering in this PR.
  • context #595: Adds the ThreadContextMenu and ThreadWrapper in mail-list.tsx, which this PR further refines by adjusting rendering logic.
  • optimistic thread state for thread context menu #1300: Adds optimistic label toggling and bulk label support in ThreadContextMenu, related to this PR's optimistic UI enhancements.

Suggested reviewers

  • MrgSub
  • ahmetskilinc

Poem

In the mailbox garden, threads hop and hide,
With optimistic leaps, counts adjust with pride.
Sidebars now smarter, stats always just right—
No thread left behind, even out of sight!
As actions grow bolder, the UI keeps its cheer,
A rabbit’s code dream, hopping ever near.
🐇✨

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

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 smooth infinite email deletion with optimistic updates across the UI, featuring intelligent prefetching and real-time count adjustments for folders and labels.

  • Added useAdjustedStats hook in apps/mail/hooks/use-stats.ts to handle optimistic updates for folder counts before server confirmation
  • Enhanced thread prefetching in apps/mail/hooks/use-threads.ts with multiple strategies including threshold-based and upfront page loading
  • Improved navigation handling in apps/mail/hooks/use-mail-navigation.ts with thread ID tracking and focus management during deletions
  • Integrated optimistic updates throughout sidebar components for immediate UI feedback during bulk operations
  • Replaced direct mutations with optimistic actions in hotkeys for consistent deletion behavior

11 files reviewed, 4 comments
Edit PR Review Bot Settings | Greptile

Comment on lines +23 to +26
const globalShortcuts = React.useMemo(
() => keyboardShortcuts.filter((shortcut) => shortcut.scope === scope),
[],
);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: globalShortcuts memoization has empty deps array but uses keyboardShortcuts from outside the component. Should add keyboardShortcuts to dependency array if it can change.

Comment on lines +42 to +43
const source = action.source.toLowerCase();
const destination = action.destination?.toLowerCase();
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: source is accessed without null check which could crash if action.source is undefined

Suggested change
const source = action.source.toLowerCase();
const destination = action.destination?.toLowerCase();
const source = action.source?.toLowerCase();
const destination = action.destination?.toLowerCase();

Comment on lines +91 to +96
const optimisticlyRemovedCount = Object.values(optimisticActions).reduce((count, action) => {
if (action.type === 'MOVE' && action.source === folder) {
return count + action.threadIds.length;
}
return count;
}, 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: This count calculation may over-count if there are multiple MOVE actions for the same threadId. Use a Set to deduplicate threadIds first

// Update the tracked focused thread ID
prevFocusedThreadId.current = threadId;

const currentThreadId = window.location.search.includes('threadId=');
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: This check may return true for any query param containing 'threadId='. Use URLSearchParams to properly parse the query string

Copy link
Collaborator

Choose a reason for hiding this comment

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

please fix

prevFocusedThreadId.current = null;
return null;
} else if (currentItem) {
// Current item existts and wasnt deleted, then navigate to the next one
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a typo in this comment: existts should be exists and wasnt should be wasn't. The corrected comment would be: "Current item exists and wasn't deleted, then navigate to the next one"

Suggested change
// Current item existts and wasnt deleted, then navigate to the next one
// Current item exists and wasn't deleted, then navigate to the next one

Spotted by Diamond

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BRO STOP

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: 3

🧹 Nitpick comments (3)
apps/mail/hooks/use-stats.ts (1)

22-66: Solid implementation of optimistic stats adjustment.

The logic correctly:

  • Calculates deltas for source/destination folders based on MOVE actions
  • Prevents negative counts with Math.max(0, ...)
  • Uses memoization for performance optimization
  • Maintains the same interface as the original useStats hook

Consider adding null safety for the label property:

-      const delta = deltas[stat.label?.toLowerCase() ?? ''] ?? 0;
+      const labelKey = stat.label?.toLowerCase();
+      const delta = labelKey ? (deltas[labelKey] ?? 0) : 0;

This makes the intent clearer when stats don't have labels.

apps/mail/hooks/use-mail-navigation.ts (2)

102-206: Consider refactoring navigateNext for improved maintainability

The navigateNext function has become quite complex with multiple nested conditions. Consider extracting helper functions to improve readability:

  • handleOutOfBoundsIndex(prevIndex, items) - for lines 119-133
  • handleDeletedThread(prevIndex, items, prevThreadId) - for lines 135-169
  • handleNormalNavigation(prevIndex, items) - for lines 170-204

This would make the main logic flow clearer and easier to test.


279-280: Apply consistent formatting for hotkey configurations

-  useHotkeys('Enter', handleEnter, { preventDefault: true,enabled: !isCommandPaletteOpen });
-  useHotkeys('Escape', handleEscape, { preventDefault: true,enabled: !isCommandPaletteOpen });
+  useHotkeys('Enter', handleEnter, { preventDefault: true, enabled: !isCommandPaletteOpen });
+  useHotkeys('Escape', handleEscape, { preventDefault: true, enabled: !isCommandPaletteOpen });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 142bbaa and 09e25a6.

📒 Files selected for processing (11)
  • apps/mail/components/mail/mail-list.tsx (1 hunks)
  • apps/mail/components/ui/app-sidebar.tsx (2 hunks)
  • apps/mail/components/ui/nav-main.tsx (4 hunks)
  • apps/mail/components/ui/sidebar-labels.tsx (1 hunks)
  • apps/mail/hooks/use-mail-navigation.ts (6 hunks)
  • apps/mail/hooks/use-optimistic-actions.ts (2 hunks)
  • apps/mail/hooks/use-stats.ts (2 hunks)
  • apps/mail/hooks/use-threads.ts (2 hunks)
  • apps/mail/lib/hotkeys/navigation-hotkeys.tsx (1 hunks)
  • apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (3 hunks)
  • apps/mail/store/optimistic-updates.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
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.
apps/mail/components/ui/app-sidebar.tsx (1)
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.
apps/mail/lib/hotkeys/navigation-hotkeys.tsx (1)
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.
apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (1)
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.
apps/mail/components/mail/mail-list.tsx (1)
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.
apps/mail/store/optimistic-updates.ts (2)
Learnt from: retrogtx
PR: Mail-0/Zero#1468
File: apps/server/src/trpc/routes/mail.ts:386-391
Timestamp: 2025-06-27T04:59:29.709Z
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.
Learnt from: dakdevs
PR: Mail-0/Zero#764
File: apps/mail/actions/send.ts:6-8
Timestamp: 2025-04-25T06:09:08.027Z
Learning: The `after` function is a valid export from `next/server` in Next.js versions 14.1.0 and later. It's used to schedule code to run after a Server Action has completed and the response has been streamed to the client, making it ideal for non-critical operations like analytics, logging, or background processing.
apps/mail/hooks/use-mail-navigation.ts (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: Fahad-Dezloper
PR: Mail-0/Zero#1440
File: apps/mail/components/create/ai-chat.tsx:101-113
Timestamp: 2025-06-22T19:23:10.599Z
Learning: On small and medium screens (mobile devices), buttons don't naturally lose focus after being clicked like they do on desktop browsers. This requires manual intervention using setTimeout with additional blur() calls and DOM manipulation to properly clear focus and active states in button click handlers for responsive design.
apps/mail/hooks/use-threads.ts (1)
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.
🧬 Code Graph Analysis (6)
apps/mail/lib/hotkeys/navigation-hotkeys.tsx (1)
apps/mail/config/shortcuts.ts (1)
  • keyboardShortcuts (359-365)
apps/mail/components/ui/nav-main.tsx (1)
apps/mail/hooks/use-stats.ts (1)
  • useAdjustedStats (22-66)
apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (1)
apps/mail/hooks/use-optimistic-actions.ts (1)
  • useOptimisticActions (34-473)
apps/mail/components/mail/mail-list.tsx (1)
apps/mail/components/context/thread-context.tsx (1)
  • ThreadContextMenu (127-493)
apps/mail/hooks/use-stats.ts (1)
apps/mail/store/optimistic-updates.ts (1)
  • optimisticActionsAtom (11-11)
apps/mail/hooks/use-threads.ts (3)
apps/mail/hooks/use-search-value.ts (1)
  • useSearchValue (19-21)
apps/mail/store/backgroundQueue.ts (2)
  • backgroundQueueAtom (5-19)
  • isThreadInBackgroundQueueAtom (21-23)
apps/mail/store/optimistic-updates.ts (1)
  • optimisticActionsAtom (11-11)
🔇 Additional comments (16)
apps/mail/components/ui/app-sidebar.tsx (1)

42-42: LGTM: Consistent migration to adjusted stats hook.

The migration from useStats to useAdjustedStats properly supports the optimistic UI improvements by providing mail count statistics that reflect pending thread moves and deletions.

Also applies to: 66-66

apps/mail/hooks/use-optimistic-actions.ts (1)

279-279: LGTM: Enhanced optimistic action tracking with source folder.

Adding the source property enables more precise tracking of thread moves and deletions, which supports:

  • Accurate folder count adjustments in useAdjustedStats
  • Thread filtering in useThreads to hide threads being moved out
  • Better undo functionality by preserving the source folder context

Also applies to: 339-339

apps/mail/components/mail/mail-list.tsx (1)

606-616: LGTM: Improved separation of concerns by centralizing optimistic state logic.

Removing the optimisticState.shouldHide check from the rendering logic simplifies the component and aligns with the centralized thread filtering approach in useThreads. This improves maintainability by keeping optimistic UI state management in one place.

apps/mail/lib/hotkeys/thread-display-hotkeys.tsx (1)

2-2: LGTM: Integration with optimistic deletion system enhances UX.

Replacing the previous deletion logic with optimisticDeleteThreads provides immediate UI feedback for hotkey-triggered deletions, creating a smoother user experience that aligns with the PR's goal of implementing a "smooth infinite email deletion experience."

Also applies to: 5-5, 24-24, 45-45

apps/mail/lib/hotkeys/navigation-hotkeys.tsx (1)

4-4: LGTM: Good performance optimizations with correct memoization.

The React.useMemo additions properly prevent unnecessary re-creations:

  • handlers memoization with [navigate] dependency is appropriate
  • globalShortcuts memoization with empty dependency array is correct for static shortcuts

These optimizations improve performance without changing functionality.

Also applies to: 10-26

apps/mail/components/ui/nav-main.tsx (2)

17-17: LGTM! Clean refactoring to use adjusted stats.

The migration from useStats to useAdjustedStats is consistent across both NavMain and NavItem components, ensuring optimistic UI updates are reflected in the navigation counts.

Also applies to: 59-59, 275-275


263-263: Good decoupling of stats prop dependency.

Removing the stats prop from SidebarLabels and having the component manage its own stats internally improves encapsulation and reduces prop drilling.

apps/mail/hooks/use-stats.ts (1)

4-6: LGTM! Appropriate imports for optimistic UI functionality.

The new imports support the optimistic stats adjustment logic effectively.

apps/mail/components/ui/sidebar-labels.tsx (2)

5-5: LGTM! Updated import for optimistic stats.

The import change aligns with the overall migration to use adjusted stats throughout the application.


7-10: Excellent encapsulation improvement.

Removing the stats prop and having the component manage its own stats internally:

  • Reduces prop drilling
  • Makes the component more self-contained
  • Ensures optimistic updates are automatically reflected

Also applies to: 12-13

apps/mail/store/optimistic-updates.ts (1)

5-5: LGTM! Essential addition for optimistic UI tracking.

Adding the source property to MOVE actions enables proper tracking of threads being moved out of folders, which is crucial for the optimistic filtering logic in useThreads.

apps/mail/hooks/use-threads.ts (3)

13-13: LGTM! Proper integration of optimistic actions.

The import and usage of optimisticActionsAtom enables the filtering of threads that are being optimistically moved out of the current folder.

Also applies to: 21-21


42-52: Excellent optimistic filtering implementation.

The shouldHideThread function efficiently:

  • Uses a Set for O(1) lookup performance
  • Correctly identifies threads being moved out of the current folder
  • Is properly memoized with appropriate dependencies

60-62: Good integration of optimistic filtering in thread list.

The filter integration maintains clean separation of concerns and ensures optimistically moved threads are hidden from the UI immediately.

apps/mail/hooks/use-mail-navigation.ts (2)

27-31: Well-implemented thread deletion tracking

The addition of prevFocusedThreadId ref provides a robust way to detect when threads are deleted during navigation. The ref is properly initialized and consistently updated across all navigation actions.


135-140: Verify behavior with rapid consecutive thread deletions

The deletion detection logic looks solid, but please ensure it handles rapid consecutive deletions correctly, especially when multiple threads are deleted while navigation is in progress.

Consider adding a test case that simulates rapid thread deletions to ensure the navigation remains stable.

Comment on lines +49 to +54
// Derived atom that adjusts server stats with optimistic actions
export const adjustedStatsAtom = atom((get) => {
// We can't directly access trpc query data here, so this will be used differently
// This is a placeholder - actual implementation will be in the hook
return null;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove the unused placeholder atom.

This placeholder atom that returns null serves no purpose and could confuse future developers. Since the actual stats adjustment logic is properly implemented in the useAdjustedStats hook, this atom should be removed.

-// Derived atom that adjusts server stats with optimistic actions
-export const adjustedStatsAtom = atom((get) => {
-  // We can't directly access trpc query data here, so this will be used differently
-  // This is a placeholder - actual implementation will be in the hook
-  return null;
-});
🤖 Prompt for AI Agents
In apps/mail/store/optimistic-updates.ts around lines 49 to 54, remove the
entire placeholder atom named adjustedStatsAtom that returns null, as it is
unused and redundant. The actual logic is implemented in the useAdjustedStats
hook, so deleting this atom will clean up the code and prevent confusion.

Comment on lines +64 to +106
const THRESHOLD = 100;
const PREFETCH_PAGES = 3;

useEffect(() => {
if (
threads.length < THRESHOLD &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [threads.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);

useEffect(() => {
const loadedPages = threadsQuery.data?.pages.length ?? 0;
if (
loadedPages < PREFETCH_PAGES &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [threadsQuery.data?.pages.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);

useEffect(() => {
const optimisticlyRemovedCount = Object.values(optimisticActions).reduce((count, action) => {
if (action.type === 'MOVE' && action.source === folder) {
return count + action.threadIds.length;
}
return count;
}, 0);

if (
optimisticlyRemovedCount > 20 &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [optimisticActions, folder, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Aggressive pagination logic needs refinement.

While the automatic pagination improves UX, the current implementation has several concerns:

  1. Magic numbers should be constants - Extract 100, 3, and 20 to named constants
  2. Performance impact - Multiple effects triggering fetchNextPage() could be expensive
  3. Risk of infinite loops - Need safeguards against continuous fetching
+const PAGINATION_THRESHOLD = 100;
+const MAX_PREFETCH_PAGES = 3;
+const OPTIMISTIC_REMOVAL_THRESHOLD = 20;
+
 useEffect(() => {
   if (
-    threads.length < THRESHOLD &&
+    threads.length < PAGINATION_THRESHOLD &&
     threadsQuery.hasNextPage &&
     !threadsQuery.isFetchingNextPage &&
     !threadsQuery.isLoading
   ) {
     void threadsQuery.fetchNextPage();
   }
 }, [threads.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);

 useEffect(() => {
   const loadedPages = threadsQuery.data?.pages.length ?? 0;
   if (
-    loadedPages < PREFETCH_PAGES &&
+    loadedPages < MAX_PREFETCH_PAGES &&
     threadsQuery.hasNextPage &&
     !threadsQuery.isFetchingNextPage &&
     !threadsQuery.isLoading
   ) {
     void threadsQuery.fetchNextPage();
   }
 }, [threadsQuery.data?.pages.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);

 useEffect(() => {
   const optimisticlyRemovedCount = Object.values(optimisticActions).reduce((count, action) => {
     if (action.type === 'MOVE' && action.source === folder) {
       return count + action.threadIds.length;
     }
     return count;
   }, 0);

   if (
-    optimisticlyRemovedCount > 20 &&
+    optimisticlyRemovedCount > OPTIMISTIC_REMOVAL_THRESHOLD &&
     threadsQuery.hasNextPage &&
     !threadsQuery.isFetchingNextPage &&
     !threadsQuery.isLoading
   ) {
     void threadsQuery.fetchNextPage();
   }
 }, [optimisticActions, folder, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);

Consider consolidating these effects or adding rate limiting to prevent excessive API calls.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const THRESHOLD = 100;
const PREFETCH_PAGES = 3;
useEffect(() => {
if (
threads.length < THRESHOLD &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [threads.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
useEffect(() => {
const loadedPages = threadsQuery.data?.pages.length ?? 0;
if (
loadedPages < PREFETCH_PAGES &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [threadsQuery.data?.pages.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
useEffect(() => {
const optimisticlyRemovedCount = Object.values(optimisticActions).reduce((count, action) => {
if (action.type === 'MOVE' && action.source === folder) {
return count + action.threadIds.length;
}
return count;
}, 0);
if (
optimisticlyRemovedCount > 20 &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [optimisticActions, folder, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
const THRESHOLD = 100;
const PREFETCH_PAGES = 3;
const PAGINATION_THRESHOLD = 100;
const MAX_PREFETCH_PAGES = 3;
const OPTIMISTIC_REMOVAL_THRESHOLD = 20;
useEffect(() => {
if (
threads.length < PAGINATION_THRESHOLD &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [threads.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
useEffect(() => {
const loadedPages = threadsQuery.data?.pages.length ?? 0;
if (
loadedPages < MAX_PREFETCH_PAGES &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [threadsQuery.data?.pages.length, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
useEffect(() => {
const optimisticlyRemovedCount = Object.values(optimisticActions).reduce((count, action) => {
if (action.type === 'MOVE' && action.source === folder) {
return count + action.threadIds.length;
}
return count;
}, 0);
if (
optimisticlyRemovedCount > OPTIMISTIC_REMOVAL_THRESHOLD &&
threadsQuery.hasNextPage &&
!threadsQuery.isFetchingNextPage &&
!threadsQuery.isLoading
) {
void threadsQuery.fetchNextPage();
}
}, [optimisticActions, folder, threadsQuery.hasNextPage, threadsQuery.isFetchingNextPage, threadsQuery.isLoading]);
🤖 Prompt for AI Agents
In apps/mail/hooks/use-threads.ts between lines 64 and 106, the pagination logic
uses hardcoded numbers 100, 3, and 20 which should be extracted as named
constants for clarity. Also, multiple useEffect hooks independently trigger
fetchNextPage(), which can cause performance issues and risk infinite loops.
Refactor by consolidating these effects into a single useEffect that handles all
conditions and add a rate limiting mechanism or a flag to prevent excessive or
continuous fetchNextPage() calls.

prevFocusedThreadId.current = null;
return null;
} else if (currentItem) {
// Current item existts and wasnt deleted, then navigate to the next one
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo: "existts" should be "exists"

-        // Current item existts and wasnt deleted, then navigate to the next one
+        // Current item exists and wasn't deleted, then navigate to the next one
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Current item existts and wasnt deleted, then navigate to the next one
// Current item exists and wasn't deleted, then navigate to the next one
🤖 Prompt for AI Agents
In apps/mail/hooks/use-mail-navigation.ts at line 170, fix the typo in the
comment by changing "existts" to "exists" to correct the spelling.

@retrogtx retrogtx marked this pull request as draft July 4, 2025 03:50
@MrgSub MrgSub closed this Jul 4, 2025
@retrogtx
Copy link
Contributor Author

retrogtx commented Jul 4, 2025

IT'S WIP

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.

2 participants