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 && (