From 256ff7698dfe2a6632bb7e3b02b83c7a9c282452 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:57:06 +0100 Subject: [PATCH] feat: enhance email selection with checkbox functionality and improve session handling --- components/email/email-list-item.tsx | 3 +- components/email/thread-email-item.tsx | 27 ++++++++++- components/email/thread-list-item.tsx | 62 ++++++++++++++++++++++-- hooks/use-email-drag.ts | 2 +- lib/jmap/client.ts | 67 +++++++++++++++++++++++--- 5 files changed, 147 insertions(+), 14 deletions(-) diff --git a/components/email/email-list-item.tsx b/components/email/email-list-item.tsx index 39367377..a00d6d3c 100644 --- a/components/email/email-list-item.tsx +++ b/components/email/email-list-item.tsx @@ -22,7 +22,7 @@ interface EmailListItemProps { export function EmailListItem({ email, selected, onClick, onContextMenu }: EmailListItemProps) { const t = useTranslations('email_viewer'); - const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox } = useEmailStore(); + const { selectedEmailIds, toggleEmailSelection, selectRangeEmails, selectedMailbox, clearSelection } = useEmailStore(); const showPreview = useSettingsStore((state) => state.showPreview); const emailKeywords = useSettingsStore((state) => state.emailKeywords); const { identities } = useAuthStore(); @@ -80,6 +80,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email e.preventDefault(); selectRangeEmails(email.id); } else { + if (selectedEmailIds.size > 0) clearSelection(); onClick?.(); } }} diff --git a/components/email/thread-email-item.tsx b/components/email/thread-email-item.tsx index e80bd0a6..84f7b9fe 100644 --- a/components/email/thread-email-item.tsx +++ b/components/email/thread-email-item.tsx @@ -4,7 +4,7 @@ import { formatDate } from "@/lib/utils"; import { Email } from "@/lib/jmap/types"; import { cn } from "@/lib/utils"; import { Avatar } from "@/components/ui/avatar"; -import { Paperclip, Star, Circle } from "lucide-react"; +import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react"; import { useEmailDrag } from "@/hooks/use-email-drag"; import { useEmailStore } from "@/stores/email-store"; @@ -26,7 +26,7 @@ export function ThreadEmailItem({ const isUnread = !email.keywords?.$seen; const isStarred = email.keywords?.$flagged; const sender = email.from?.[0]; - const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails } = useEmailStore(); + const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore(); const isChecked = selectedEmailIds.has(email.id); const { dragHandlers, isDragging } = useEmailDrag({ @@ -38,6 +38,11 @@ export function ThreadEmailItem({ onContextMenu?.(e, email); }; + const handleCheckboxClick = (e: React.MouseEvent) => { + e.stopPropagation(); + toggleEmailSelection(email.id); + }; + const handleClick = (e: React.MouseEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); @@ -46,6 +51,7 @@ export function ThreadEmailItem({ e.preventDefault(); selectRangeEmails(email.id); } else { + if (selectedEmailIds.size > 0) clearSelection(); onClick?.(); } }; @@ -69,6 +75,23 @@ export function ThreadEmailItem({ onContextMenu={handleContextMenu} >
+ {/* Checkbox */} + + {/* Unread indicator */} {isUnread && (
diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index 61812b63..4e83313a 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -5,7 +5,7 @@ import { formatDate } from "@/lib/utils"; import { Email, ThreadGroup } from "@/lib/jmap/types"; import { cn } from "@/lib/utils"; import { Avatar } from "@/components/ui/avatar"; -import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare } from "lucide-react"; +import { Paperclip, Star, Circle, ChevronRight, ChevronDown, Loader2, MessageSquare, CheckSquare, Square } from "lucide-react"; import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store"; import { useUIStore } from "@/stores/ui-store"; import { useEmailStore } from "@/stores/email-store"; @@ -40,7 +40,7 @@ const SingleEmailItem = React.forwardRef( const isUnread = !email.keywords?.$seen; const isStarred = email.keywords?.$flagged; const sender = email.from?.[0]; - const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails } = useEmailStore(); + const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore(); const emailKeywords = useSettingsStore((state) => state.emailKeywords); const isChecked = selectedEmailIds.has(email.id); @@ -57,6 +57,11 @@ const SingleEmailItem = React.forwardRef( sourceMailboxId: selectedMailbox, }); + const handleCheckboxClick = (e: React.MouseEvent) => { + e.stopPropagation(); + toggleEmailSelection(email.id); + }; + const handleContextMenu = (e: React.MouseEvent) => { onContextMenu?.(e, email); }; @@ -69,6 +74,7 @@ const SingleEmailItem = React.forwardRef( e.preventDefault(); selectRangeEmails(email.id); } else { + if (selectedEmailIds.size > 0) clearSelection(); onClick(); } }; @@ -96,6 +102,23 @@ const SingleEmailItem = React.forwardRef( style={{ minHeight: 'var(--list-item-height)' }} >
+ {/* Checkbox */} + + {isUnread && (
@@ -193,7 +216,7 @@ export const ThreadListItem = React.forwardRef state.isMobile); const { latestEmail, participantNames, hasUnread, hasStarred, hasAttachment, emailCount } = thread; - const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails } = useEmailStore(); + const { selectedMailbox, selectedEmailIds, toggleEmailSelection, selectRangeEmails, clearSelection } = useEmailStore(); const { dragHandlers, isDragging: isThreadDragging } = useEmailDrag({ email: latestEmail, @@ -227,6 +250,21 @@ export const ThreadListItem = React.forwardRef { + e.stopPropagation(); + // Toggle selection for all emails in this thread + const allSelected = thread.emails.every(em => selectedEmailIds.has(em.id)); + const newSelection = new Set(selectedEmailIds); + thread.emails.forEach(em => { + if (allSelected) { + newSelection.delete(em.id); + } else { + newSelection.add(em.id); + } + }); + useEmailStore.setState({ selectedEmailIds: newSelection, lastSelectedEmailId: latestEmail.id }); + }; + const handleHeaderClick = (e: React.MouseEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); @@ -249,6 +287,7 @@ export const ThreadListItem = React.forwardRef 0) clearSelection(); if (!isExpanded) { onToggleExpand(); } @@ -283,6 +322,23 @@ export const ThreadListItem = React.forwardRef
+ {/* Checkbox for thread selection */} + + {!isMobile && (