feat: add email selection functionality with toggle and update translations

This commit is contained in:
Linus Rath
2026-03-12 00:32:36 +01:00
parent 5598c436f2
commit 68d4a9a641
5 changed files with 110 additions and 65 deletions
+32 -1
View File
@@ -36,7 +36,7 @@ import { isFilterEmpty, activeFilterCount } from "@/lib/jmap/search-utils";
import { WelcomeBanner } from "@/components/ui/welcome-banner"; import { WelcomeBanner } from "@/components/ui/welcome-banner";
import { NavigationRail } from "@/components/layout/navigation-rail"; import { NavigationRail } from "@/components/layout/navigation-rail";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine } from "lucide-react"; import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine, CheckSquare, Square } from "lucide-react";
import { ResizeHandle } from "@/components/layout/resize-handle"; import { ResizeHandle } from "@/components/layout/resize-handle";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -73,8 +73,10 @@ export default function Home() {
newEmailNotification, newEmailNotification,
selectEmail, selectEmail,
selectMailbox, selectMailbox,
selectedEmailIds,
selectAllEmails, selectAllEmails,
clearSelection, clearSelection,
toggleEmailSelection,
fetchMailboxes, fetchMailboxes,
fetchEmails, fetchEmails,
fetchQuota, fetchQuota,
@@ -896,6 +898,35 @@ export default function Home() {
<div className="border-b border-border bg-background"> <div className="border-b border-border bg-background">
<div className="px-3 py-3"> <div className="px-3 py-3">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{/* Select / Select All toggle */}
<button
type="button"
onClick={() => {
if (selectedEmailIds.size > 0) {
if (selectedEmailIds.size === emails.length) {
clearSelection();
} else {
selectAllEmails();
}
} else {
// Enter selection mode by selecting the first email
if (emails.length > 0) toggleEmailSelection(emails[0].id);
}
}}
className={cn(
"flex-shrink-0 p-2 rounded-md transition-colors",
selectedEmailIds.size > 0
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)}
title={selectedEmailIds.size > 0 ? (selectedEmailIds.size === emails.length ? t('email_list.batch_actions.clear_selection') : t('email_list.batch_actions.select_all')) : t('email_list.batch_actions.select')}
>
{selectedEmailIds.size > 0 ? (
<CheckSquare className="w-4 h-4" />
) : (
<Square className="w-4 h-4" />
)}
</button>
<form onSubmit={(e) => { e.preventDefault(); if (searchQuery.trim()) handleSearch(searchQuery); }} className="relative flex-1"> <form onSubmit={(e) => { e.preventDefault(); if (searchQuery.trim()) handleSearch(searchQuery); }} className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
+19 -16
View File
@@ -88,22 +88,25 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
style={{ minHeight: 'var(--list-item-height)' }} style={{ minHeight: 'var(--list-item-height)' }}
> >
<div className="flex items-start gap-3 px-4 py-3"> <div className="flex items-start gap-3 px-4 py-3">
{/* Checkbox with smooth animation */} {/* Checkbox - only visible when in selection mode */}
<button {selectedEmailIds.size > 0 && (
onClick={handleCheckboxClick} <button
className={cn( onClick={handleCheckboxClick}
"p-3 lg:p-1 rounded mt-2 flex-shrink-0 transition-all duration-200", className={cn(
"hover:bg-muted/50 hover:scale-110", "p-3 lg:p-1 rounded mt-2 flex-shrink-0 transition-all duration-200",
"active:scale-95", "hover:bg-muted/50 hover:scale-110",
isChecked && "text-primary" "active:scale-95",
)} "animate-in fade-in zoom-in-95 duration-150",
> isChecked && "text-primary"
{isChecked ? ( )}
<CheckSquare className="w-4 h-4 animate-in zoom-in-50 duration-200" /> >
) : ( {isChecked ? (
<Square className="w-4 h-4 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" /> <CheckSquare className="w-4 h-4 animate-in zoom-in-50 duration-200" />
)} ) : (
</button> <Square className="w-4 h-4 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" />
)}
</button>
)}
{/* Unread indicator */} {/* Unread indicator */}
{isUnread && ( {isUnread && (
+19 -16
View File
@@ -75,22 +75,25 @@ export function ThreadEmailItem({
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{/* Checkbox */} {/* Checkbox - only visible when in selection mode */}
<button {selectedEmailIds.size > 0 && (
onClick={handleCheckboxClick} <button
className={cn( onClick={handleCheckboxClick}
"p-1 rounded mt-0.5 flex-shrink-0 transition-all duration-200", className={cn(
"hover:bg-muted/50 hover:scale-110", "p-1 rounded mt-0.5 flex-shrink-0 transition-all duration-200",
"active:scale-95", "hover:bg-muted/50 hover:scale-110",
isChecked && "text-primary" "active:scale-95",
)} "animate-in fade-in zoom-in-95 duration-150",
> isChecked && "text-primary"
{isChecked ? ( )}
<CheckSquare className="w-3.5 h-3.5 animate-in zoom-in-50 duration-200" /> >
) : ( {isChecked ? (
<Square className="w-3.5 h-3.5 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" /> <CheckSquare className="w-3.5 h-3.5 animate-in zoom-in-50 duration-200" />
)} ) : (
</button> <Square className="w-3.5 h-3.5 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" />
)}
</button>
)}
{/* Unread indicator */} {/* Unread indicator */}
{isUnread && ( {isUnread && (
+38 -32
View File
@@ -102,22 +102,25 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
style={{ minHeight: 'var(--list-item-height)' }} style={{ minHeight: 'var(--list-item-height)' }}
> >
<div className="flex items-start gap-3 px-3 py-3"> <div className="flex items-start gap-3 px-3 py-3">
{/* Checkbox */} {/* Checkbox - only visible when in selection mode */}
<button {selectedEmailIds.size > 0 && (
onClick={handleCheckboxClick} <button
className={cn( onClick={handleCheckboxClick}
"p-3 lg:p-1 rounded mt-2 flex-shrink-0 transition-all duration-200", className={cn(
"hover:bg-muted/50 hover:scale-110", "p-3 lg:p-1 rounded mt-2 flex-shrink-0 transition-all duration-200",
"active:scale-95", "hover:bg-muted/50 hover:scale-110",
isChecked && "text-primary" "active:scale-95",
)} "animate-in fade-in zoom-in-95 duration-150",
> isChecked && "text-primary"
{isChecked ? ( )}
<CheckSquare className="w-4 h-4 animate-in zoom-in-50 duration-200" /> >
) : ( {isChecked ? (
<Square className="w-4 h-4 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" /> <CheckSquare className="w-4 h-4 animate-in zoom-in-50 duration-200" />
)} ) : (
</button> <Square className="w-4 h-4 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" />
)}
</button>
)}
{isUnread && ( {isUnread && (
<div className="absolute left-1 top-1/2 -translate-y-1/2"> <div className="absolute left-1 top-1/2 -translate-y-1/2">
@@ -322,22 +325,25 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
style={{ minHeight: 'var(--list-item-height)' }} style={{ minHeight: 'var(--list-item-height)' }}
> >
<div className="flex items-start gap-3 px-3 py-3"> <div className="flex items-start gap-3 px-3 py-3">
{/* Checkbox for thread selection */} {/* Checkbox for thread selection - only visible when in selection mode */}
<button {selectedEmailIds.size > 0 && (
onClick={handleThreadCheckboxClick} <button
className={cn( onClick={handleThreadCheckboxClick}
"p-3 lg:p-1 rounded mt-2 flex-shrink-0 transition-all duration-200", className={cn(
"hover:bg-muted/50 hover:scale-110", "p-3 lg:p-1 rounded mt-2 flex-shrink-0 transition-all duration-200",
"active:scale-95", "hover:bg-muted/50 hover:scale-110",
isChecked && "text-primary" "active:scale-95",
)} "animate-in fade-in zoom-in-95 duration-150",
> isChecked && "text-primary"
{isChecked ? ( )}
<CheckSquare className="w-4 h-4 animate-in zoom-in-50 duration-200" /> >
) : ( {isChecked ? (
<Square className="w-4 h-4 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" /> <CheckSquare className="w-4 h-4 animate-in zoom-in-50 duration-200" />
)} ) : (
</button> <Square className="w-4 h-4 text-muted-foreground opacity-60 hover:opacity-100 transition-opacity" />
)}
</button>
)}
{!isMobile && ( {!isMobile && (
<button <button
+2
View File
@@ -111,6 +111,8 @@
"loading_more": "Loading more emails...", "loading_more": "Loading more emails...",
"no_more_emails": "No more emails to load", "no_more_emails": "No more emails to load",
"batch_actions": { "batch_actions": {
"select": "Select emails",
"select_all": "Select all",
"mark_read": "Mark as read", "mark_read": "Mark as read",
"mark_unread": "Mark as unread", "mark_unread": "Mark as unread",
"delete": "Delete", "delete": "Delete",