feat: implement empty folder functionality for junk and trash mailboxes with confirmation dialog

This commit is contained in:
Linus Rath
2026-03-15 15:47:58 +01:00
parent c24fabe977
commit b7fa25c314
14 changed files with 232 additions and 13 deletions
+5 -3
View File
@@ -473,12 +473,14 @@ export default function Home() {
const handleDelete = async () => {
if (!client || !selectedEmail) return;
// Check if we're currently in the trash folder
// Check if we're currently in the trash or junk folder
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
const isInTrash = currentMailbox?.role === 'trash';
const isInJunk = currentMailbox?.role === 'junk';
const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk;
if (isInTrash) {
// In trash: confirm before permanently deleting
if (isInTrash || (isInJunk && permanentlyDeleteJunk)) {
// In trash or junk with permanent delete enabled: confirm before permanently deleting
const confirmed = await confirmDialog({
title: t('email_list.permanent_delete_confirm_title'),
message: t('email_list.permanent_delete_confirm_message'),
+47 -2
View File
@@ -4,7 +4,7 @@ import { Email, ThreadGroup } from "@/lib/jmap/types";
import { ThreadListItem } from "./thread-list-item";
import { EmailContextMenu } from "./email-context-menu";
import { cn } from "@/lib/utils";
import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX } from "lucide-react";
import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX, AlertTriangle } from "lucide-react";
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
@@ -74,6 +74,7 @@ export function EmailList({
isLoadingMore,
mailboxes,
selectedMailbox,
emptyMailbox,
expandedThreadIds,
threadEmailsCache,
isLoadingThread,
@@ -172,6 +173,28 @@ export function EmailList({
}
};
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
const isEmptyableFolder = currentMailbox?.role === 'trash' || currentMailbox?.role === 'junk';
const handleEmptyFolder = async () => {
if (!client || isProcessing || !currentMailbox) return;
const confirmed = await confirmDialog({
title: t('empty_folder.confirm_title'),
message: t('empty_folder.confirm_message'),
confirmText: t('empty_folder.confirm_button'),
variant: "destructive",
});
if (!confirmed) return;
setIsProcessing(true);
try {
await emptyMailbox(client, currentMailbox.id);
} finally {
setTimeout(() => setIsProcessing(false), 500);
}
};
const handleLoadMore = useCallback(() => {
if (client && hasMoreEmails && !isLoadingMore && !isLoading) {
loadMoreEmails(client);
@@ -308,7 +331,29 @@ export function EmailList({
/>
)}
{/* Empty Folder Banner for Junk/Trash */}
{isEmptyableFolder && emails.length > 0 && !hasSelection && (
<div className="px-4 py-2 border-b border-border bg-muted/30 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<AlertTriangle className="w-4 h-4" />
<span>{currentMailbox?.role === 'junk' ? t('empty_folder.junk_hint') : t('empty_folder.trash_hint')}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={handleEmptyFolder}
disabled={isProcessing}
className="text-destructive border-destructive/30 hover:bg-destructive/10 text-xs"
>
{isProcessing ? (
<Loader2 className="w-3 h-3 animate-spin mr-1" />
) : (
<Trash2 className="w-3 h-3 mr-1" />
)}
{t('empty_folder.button')}
</Button>
</div>
)}
{/* Email List */}
<div ref={parentRef} className="flex-1 overflow-y-auto bg-background relative">
+9
View File
@@ -14,6 +14,7 @@ export function EmailSettings() {
const {
markAsReadDelay,
deleteAction,
permanentlyDeleteJunk,
showPreview,
emailsPerPage,
externalContentPolicy,
@@ -65,6 +66,14 @@ export function EmailSettings() {
</div>
</SettingItem>
{/* Permanently Delete Junk */}
<SettingItem label={t('permanently_delete_junk.label')} description={t('permanently_delete_junk.description')}>
<ToggleSwitch
checked={permanentlyDeleteJunk}
onChange={(checked) => updateSetting('permanentlyDeleteJunk', checked)}
/>
</SettingItem>
{/* Show Preview */}
<SettingItem label={t('show_preview.label')} description={t('show_preview.description')}>
<ToggleSwitch checked={showPreview} onChange={(checked) => updateSetting('showPreview', checked)} />
+28
View File
@@ -808,6 +808,34 @@ export class JMAPClient {
}
}
async emptyMailbox(mailboxId: string): Promise<number> {
let totalDestroyed = 0;
let hasMore = true;
while (hasMore) {
const response = await this.request([
["Email/query", {
accountId: this.accountId,
filter: { inMailbox: mailboxId },
limit: 500,
}, "0"],
["Email/set", {
accountId: this.accountId,
"#destroy": { resultOf: "0", name: "Email/query", path: "/ids" },
}, "1"],
]);
const queryResult = response.methodResponses?.[0]?.[1];
const setResult = response.methodResponses?.[1]?.[1];
const destroyed = setResult?.destroyed?.length || 0;
totalDestroyed += destroyed;
hasMore = destroyed > 0 && (queryResult?.total || 0) > destroyed;
}
return totalDestroyed;
}
async markAsSpam(emailId: string, accountId?: string): Promise<void> {
const targetAccountId = accountId || this.accountId;
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Endgültig löschen",
"permanent_delete_confirm_title": "Endgültig löschen",
"permanent_delete_confirm_message": "Diese E-Mail wird endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"permanent_delete_confirm_batch_message": "Diese {count, plural, one {1 E-Mail} other {# E-Mails}} werden endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
"permanent_delete_confirm_batch_message": "Diese {count, plural, one {1 E-Mail} other {# E-Mails}} werden endgültig gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"empty_folder": {
"button": "Ordner leeren",
"confirm_title": "Ordner leeren",
"confirm_message": "Alle E-Mails in diesem Ordner werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"confirm_button": "Ordner leeren",
"junk_hint": "Sie können den Spam-Ordner leeren, um alle Nachrichten dauerhaft zu entfernen.",
"trash_hint": "Sie können den Papierkorb leeren, um alle Nachrichten dauerhaft zu entfernen."
}
},
"email_viewer": {
"no_email_selected": "Keine E-Mail ausgewählt",
@@ -594,6 +602,10 @@
"permanent": "Dauerhaft löschen",
"warning": "E-Mails werden dauerhaft gelöscht und können nicht wiederhergestellt werden. Diese Aktion ist unwiderruflich."
},
"permanently_delete_junk": {
"label": "Spam dauerhaft löschen",
"description": "E-Mails aus dem Spam-Ordner dauerhaft löschen, anstatt sie in den Papierkorb zu verschieben"
},
"show_preview": {
"label": "Vorschautext anzeigen",
"description": "E-Mail-Vorschau in der Liste anzeigen"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Delete permanently",
"permanent_delete_confirm_title": "Permanently delete",
"permanent_delete_confirm_message": "This email will be permanently deleted. This action cannot be undone.",
"permanent_delete_confirm_batch_message": "These {count, plural, one {1 email} other {# emails}} will be permanently deleted. This action cannot be undone."
"permanent_delete_confirm_batch_message": "These {count, plural, one {1 email} other {# emails}} will be permanently deleted. This action cannot be undone.",
"empty_folder": {
"button": "Empty folder",
"confirm_title": "Empty folder",
"confirm_message": "All emails in this folder will be permanently deleted. This action cannot be undone.",
"confirm_button": "Empty folder",
"junk_hint": "You can empty the Junk folder to permanently remove all messages.",
"trash_hint": "You can empty the Trash folder to permanently remove all messages."
}
},
"email_viewer": {
"no_email_selected": "No email selected",
@@ -598,6 +606,10 @@
"permanent": "Delete Permanently",
"warning": "Emails will be permanently deleted and cannot be recovered. This action is irreversible."
},
"permanently_delete_junk": {
"label": "Permanently Delete Junk",
"description": "Permanently delete emails from the Junk/Spam folder instead of moving them to Trash"
},
"show_preview": {
"label": "Show Preview Text",
"description": "Display email preview in the list"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Eliminar permanentemente",
"permanent_delete_confirm_title": "Eliminar permanentemente",
"permanent_delete_confirm_message": "Este correo se eliminará permanentemente. Esta acción no se puede deshacer.",
"permanent_delete_confirm_batch_message": "Estos {count, plural, one {1 correo} other {# correos}} se eliminarán permanentemente. Esta acción no se puede deshacer."
"permanent_delete_confirm_batch_message": "Estos {count, plural, one {1 correo} other {# correos}} se eliminarán permanentemente. Esta acción no se puede deshacer.",
"empty_folder": {
"button": "Vaciar carpeta",
"confirm_title": "Vaciar carpeta",
"confirm_message": "Todos los correos de esta carpeta se eliminarán permanentemente. Esta acción no se puede deshacer.",
"confirm_button": "Vaciar carpeta",
"junk_hint": "Puede vaciar la carpeta de Spam para eliminar permanentemente todos los mensajes.",
"trash_hint": "Puede vaciar la Papelera para eliminar permanentemente todos los mensajes."
}
},
"email_viewer": {
"no_email_selected": "Ningún correo seleccionado",
@@ -594,6 +602,10 @@
"permanent": "Eliminar Permanentemente",
"warning": "Los correos se eliminarán permanentemente y no se podrán recuperar. Esta acción es irreversible."
},
"permanently_delete_junk": {
"label": "Eliminar spam permanentemente",
"description": "Eliminar permanentemente los correos de la carpeta Spam en lugar de moverlos a la Papelera"
},
"show_preview": {
"label": "Mostrar Vista Previa",
"description": "Mostrar vista previa del correo en la lista"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Supprimer définitivement",
"permanent_delete_confirm_title": "Supprimer définitivement",
"permanent_delete_confirm_message": "Cet e-mail sera définitivement supprimé. Cette action est irréversible.",
"permanent_delete_confirm_batch_message": "Ces {count, plural, one {1 e-mail} other {# e-mails}} seront définitivement supprimés. Cette action est irréversible."
"permanent_delete_confirm_batch_message": "Ces {count, plural, one {1 e-mail} other {# e-mails}} seront définitivement supprimés. Cette action est irréversible.",
"empty_folder": {
"button": "Vider le dossier",
"confirm_title": "Vider le dossier",
"confirm_message": "Tous les e-mails de ce dossier seront définitivement supprimés. Cette action est irréversible.",
"confirm_button": "Vider le dossier",
"junk_hint": "Vous pouvez vider le dossier Indésirables pour supprimer définitivement tous les messages.",
"trash_hint": "Vous pouvez vider la corbeille pour supprimer définitivement tous les messages."
}
},
"email_viewer": {
"no_email_selected": "Aucun email sélectionné",
@@ -594,6 +602,10 @@
"permanent": "Supprimer définitivement",
"warning": "Les emails seront supprimés définitivement et ne pourront pas être récupérés. Cette action est irréversible."
},
"permanently_delete_junk": {
"label": "Supprimer définitivement les indésirables",
"description": "Supprimer définitivement les e-mails du dossier Indésirables au lieu de les déplacer vers la corbeille"
},
"show_preview": {
"label": "Afficher l'aperçu",
"description": "Afficher l'aperçu de l'email dans la liste"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Elimina definitivamente",
"permanent_delete_confirm_title": "Elimina definitivamente",
"permanent_delete_confirm_message": "Questa e-mail verrà eliminata definitivamente. Questa azione non può essere annullata.",
"permanent_delete_confirm_batch_message": "Queste {count, plural, one {1 e-mail} other {# e-mail}} verranno eliminate definitivamente. Questa azione non può essere annullata."
"permanent_delete_confirm_batch_message": "Queste {count, plural, one {1 e-mail} other {# e-mail}} verranno eliminate definitivamente. Questa azione non può essere annullata.",
"empty_folder": {
"button": "Svuota cartella",
"confirm_title": "Svuota cartella",
"confirm_message": "Tutti i messaggi in questa cartella verranno eliminati definitivamente. Questa azione non può essere annullata.",
"confirm_button": "Svuota cartella",
"junk_hint": "Puoi svuotare la cartella Spam per rimuovere definitivamente tutti i messaggi.",
"trash_hint": "Puoi svuotare il Cestino per rimuovere definitivamente tutti i messaggi."
}
},
"email_viewer": {
"no_email_selected": "Nessun messaggio selezionato",
@@ -594,6 +602,10 @@
"permanent": "Elimina definitivamente",
"warning": "I messaggi verranno eliminati definitivamente e non potranno essere recuperati. Questa azione è irreversibile."
},
"permanently_delete_junk": {
"label": "Elimina spam definitivamente",
"description": "Elimina definitivamente i messaggi dalla cartella Spam invece di spostarli nel cestino"
},
"show_preview": {
"label": "Mostra anteprima testo",
"description": "Visualizza l'anteprima del messaggio nell'elenco"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "完全に削除",
"permanent_delete_confirm_title": "完全に削除",
"permanent_delete_confirm_message": "このメールは完全に削除されます。この操作は取り消せません。",
"permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。"
"permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。",
"empty_folder": {
"button": "フォルダを空にする",
"confirm_title": "フォルダを空にする",
"confirm_message": "このフォルダ内のすべてのメールが完全に削除されます。この操作は取り消せません。",
"confirm_button": "フォルダを空にする",
"junk_hint": "迷惑メールフォルダを空にして、すべてのメールを完全に削除できます。",
"trash_hint": "ゴミ箱を空にして、すべてのメールを完全に削除できます。"
}
},
"email_viewer": {
"no_email_selected": "メールが選択されていません",
@@ -594,6 +602,10 @@
"permanent": "完全に削除",
"warning": "メールは完全に削除され、復元できません。この操作は元に戻せません。"
},
"permanently_delete_junk": {
"label": "迷惑メールを完全に削除",
"description": "迷惑メールフォルダのメールをゴミ箱に移動せずに完全に削除する"
},
"show_preview": {
"label": "プレビューテキストを表示",
"description": "リストにメールのプレビューを表示"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Definitief verwijderen",
"permanent_delete_confirm_title": "Definitief verwijderen",
"permanent_delete_confirm_message": "Deze e-mail wordt definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.",
"permanent_delete_confirm_batch_message": "Deze {count, plural, one {1 e-mail} other {# e-mails}} worden definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt."
"permanent_delete_confirm_batch_message": "Deze {count, plural, one {1 e-mail} other {# e-mails}} worden definitief verwijderd. Deze actie kan niet ongedaan worden gemaakt.",
"empty_folder": {
"button": "Map legen",
"confirm_title": "Map legen",
"confirm_message": "Alle e-mails in deze map worden permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.",
"confirm_button": "Map legen",
"junk_hint": "U kunt de map Spam legen om alle berichten permanent te verwijderen.",
"trash_hint": "U kunt de prullenbak legen om alle berichten permanent te verwijderen."
}
},
"email_viewer": {
"no_email_selected": "Geen e-mail geselecteerd",
@@ -594,6 +602,10 @@
"permanent": "Permanent verwijderen",
"warning": "E-mails worden permanent verwijderd en kunnen niet worden hersteld. Deze actie is onomkeerbaar."
},
"permanently_delete_junk": {
"label": "Spam permanent verwijderen",
"description": "E-mails uit de map Spam permanent verwijderen in plaats van naar de prullenbak te verplaatsen"
},
"show_preview": {
"label": "Voorbeeldtekst tonen",
"description": "E-mailvoorbeeld weergeven in de lijst"
+13 -1
View File
@@ -130,7 +130,15 @@
"permanent_delete": "Excluir permanentemente",
"permanent_delete_confirm_title": "Excluir permanentemente",
"permanent_delete_confirm_message": "Este e-mail será permanentemente excluído. Esta ação não pode ser desfeita.",
"permanent_delete_confirm_batch_message": "Estes {count, plural, one {1 e-mail} other {# e-mails}} serão permanentemente excluídos. Esta ação não pode ser desfeita."
"permanent_delete_confirm_batch_message": "Estes {count, plural, one {1 e-mail} other {# e-mails}} serão permanentemente excluídos. Esta ação não pode ser desfeita.",
"empty_folder": {
"button": "Esvaziar pasta",
"confirm_title": "Esvaziar pasta",
"confirm_message": "Todos os e-mails nesta pasta serão permanentemente excluídos. Esta ação não pode ser desfeita.",
"confirm_button": "Esvaziar pasta",
"junk_hint": "Você pode esvaziar a pasta de Spam para remover permanentemente todas as mensagens.",
"trash_hint": "Você pode esvaziar a Lixeira para remover permanentemente todas as mensagens."
}
},
"email_viewer": {
"no_email_selected": "Nenhum e-mail selecionado",
@@ -594,6 +602,10 @@
"permanent": "Excluir Permanentemente",
"warning": "Os e-mails serão excluídos permanentemente e não poderão ser recuperados. Esta ação é irreversível."
},
"permanently_delete_junk": {
"label": "Excluir spam permanentemente",
"description": "Excluir permanentemente e-mails da pasta Spam em vez de movê-los para a Lixeira"
},
"show_preview": {
"label": "Mostrar Texto de Visualização",
"description": "Exibir visualização do e-mail na lista"
+37
View File
@@ -102,6 +102,7 @@ interface EmailStore {
renameMailbox: (client: JMAPClient, mailboxId: string, name: string) => Promise<void>;
deleteMailbox: (client: JMAPClient, mailboxId: string) => Promise<void>;
setMailboxRole: (client: JMAPClient, mailboxId: string, role: string | null) => Promise<void>;
emptyMailbox: (client: JMAPClient, mailboxId: string) => Promise<void>;
// Mock data for demo
loadMockData: () => void;
@@ -411,6 +412,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
// Get delete action preference from settings
const deleteAction = useSettingsStore.getState().deleteAction;
const permanentlyDeleteJunk = useSettingsStore.getState().permanentlyDeleteJunk;
// Determine accountId for shared folders
const selectedMailboxId = get().selectedMailbox;
@@ -418,6 +420,12 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
// If in junk folder and setting is enabled, permanently delete
const isInJunk = currentMailbox?.role === 'junk';
if (isInJunk && permanentlyDeleteJunk) {
forceDelete = true;
}
// If deleteAction is 'trash' and not forced permanent delete, try to move to trash mailbox
if (deleteAction === 'trash' && !forceDelete) {
// Find trash mailbox for the correct account
@@ -1287,6 +1295,35 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
}
},
emptyMailbox: async (client, mailboxId) => {
try {
set({ isLoading: true, error: null });
await client.emptyMailbox(mailboxId);
// Clear emails from local state if we're viewing this mailbox
const currentMailbox = get().selectedMailbox;
if (currentMailbox === mailboxId) {
set({ emails: [], selectedEmail: null });
}
// Update mailbox counters
set({
mailboxes: get().mailboxes.map(mb =>
mb.id === mailboxId
? { ...mb, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0 }
: mb
),
isLoading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to empty folder',
isLoading: false,
});
throw error;
}
},
loadMockData: () => {
const mockEmails: Email[] = [
{
+2
View File
@@ -77,6 +77,7 @@ interface SettingsState {
// Email Behavior
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
deleteAction: DeleteAction;
permanentlyDeleteJunk: boolean; // Permanently delete emails from junk/spam instead of moving to trash
showPreview: boolean;
emailsPerPage: number;
externalContentPolicy: ExternalContentPolicy;
@@ -154,6 +155,7 @@ const DEFAULT_SETTINGS = {
// Email Behavior
markAsReadDelay: 0, // Instant
deleteAction: 'trash' as DeleteAction,
permanentlyDeleteJunk: false,
showPreview: true,
emailsPerPage: 50,
externalContentPolicy: 'ask' as ExternalContentPolicy,