feat: add email selection functionality with toggle and update translations
This commit is contained in:
+32
-1
@@ -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
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user