feat: implement hover functionality for calendar events and enhance email deletion confirmation

This commit is contained in:
Linus Rath
2026-03-12 19:53:08 +01:00
parent 260ea7088d
commit ae29e64197
18 changed files with 249 additions and 30 deletions
+33 -4
View File
@@ -211,16 +211,35 @@ export default function CalendarPage() {
setShowEventModal(true);
}, []);
const handleSelectEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => {
setDetailEvent(event);
setDetailAnchorRect(anchorRect);
}, []);
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const closeDetail = useCallback(() => {
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
setDetailEvent(null);
setDetailAnchorRect(null);
}, []);
const handleSelectEvent = useCallback((event: CalendarEvent, _anchorRect: DOMRect) => {
// Click opens the sidebar for viewing/editing
closeDetail();
openEditModal(event);
}, [closeDetail, openEditModal]);
const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => {
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
// Don't show hover popover if the sidebar is already open for this event
if (showEventModal && editEvent?.id === event.id) return;
setDetailEvent(event);
setDetailAnchorRect(anchorRect);
}, [showEventModal, editEvent]);
const handleHoverLeave = useCallback(() => {
hoverTimerRef.current = setTimeout(() => {
setDetailEvent(null);
setDetailAnchorRect(null);
}, 200);
}, []);
const handleEditFromDetail = useCallback(() => {
if (detailEvent) {
const ev = detailEvent;
@@ -581,6 +600,8 @@ export default function CalendarPage() {
calendars={calendars}
onSelectDate={handleSelectDate}
onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
firstDayOfWeek={firstDayOfWeek}
isMobile={isMobile}
/>
@@ -593,6 +614,8 @@ export default function CalendarPage() {
calendars={calendars}
onSelectDate={handleSelectDate}
onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
onCreateAtTime={openCreateModal}
firstDayOfWeek={firstDayOfWeek}
timeFormat={timeFormat}
@@ -606,6 +629,8 @@ export default function CalendarPage() {
events={visibleEvents}
calendars={calendars}
onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
onCreateAtTime={openCreateModal}
timeFormat={timeFormat}
isMobile={isMobile}
@@ -618,6 +643,8 @@ export default function CalendarPage() {
events={visibleEvents}
calendars={calendars}
onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
timeFormat={timeFormat}
/>
);
@@ -739,6 +766,8 @@ export default function CalendarPage() {
onClose={closeDetail}
onSaveNote={handleSaveNoteFromDetail}
onRsvp={handleRsvpFromDetail}
onMouseEnter={() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } }}
onMouseLeave={handleHoverLeave}
currentUserEmails={currentUserEmails}
timeFormat={timeFormat}
isMobile={isMobile}
+29 -4
View File
@@ -454,10 +454,35 @@ export default function Home() {
const handleDelete = async () => {
if (!client || !selectedEmail) return;
try {
await deleteEmail(client, selectedEmail.id);
} catch (error) {
console.error("Failed to delete email:", error);
// Check if we're currently in the trash folder
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
const isInTrash = currentMailbox?.role === 'trash';
if (isInTrash) {
// In trash: confirm before permanently deleting
const confirmed = await confirmDialog({
title: t('email_list.permanent_delete_confirm_title'),
message: t('email_list.permanent_delete_confirm_message'),
confirmText: t('email_list.permanent_delete'),
variant: "destructive",
});
if (!confirmed) return;
try {
await deleteEmail(client, selectedEmail.id, true);
} catch (error) {
console.error("Failed to permanently delete email:", error);
}
} else {
// Not in trash: always move to trash
const trashMailbox = mailboxes.find(m => m.role === 'trash' && !m.isShared);
if (trashMailbox) {
try {
await moveToMailbox(client, selectedEmail.id, trashMailbox.id);
} catch (error) {
console.error("Failed to move email to trash:", error);
}
}
}
};
@@ -15,6 +15,8 @@ interface CalendarAgendaViewProps {
events: CalendarEvent[];
calendars: Calendar[];
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
timeFormat?: "12h" | "24h";
}
@@ -28,6 +30,8 @@ export function CalendarAgendaView({
events,
calendars,
onSelectEvent,
onHoverEvent,
onHoverLeave,
timeFormat = "24h",
}: CalendarAgendaViewProps) {
const t = useTranslations("calendar");
@@ -140,6 +144,8 @@ export function CalendarAgendaView({
<button
key={ev.id}
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())}
onMouseEnter={(e) => onHoverEvent?.(ev, e.currentTarget.getBoundingClientRect())}
onMouseLeave={() => onHoverLeave?.()}
className="w-full flex items-start gap-3 px-4 py-3 hover:bg-muted/50 transition-colors text-left"
>
<div className="flex flex-col items-center pt-0.5 min-w-[60px]">
@@ -15,6 +15,8 @@ interface CalendarDayViewProps {
events: CalendarEvent[];
calendars: Calendar[];
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
onCreateAtTime: (date: Date, endDate?: Date) => void;
timeFormat?: "12h" | "24h";
isMobile?: boolean;
@@ -28,6 +30,8 @@ export function CalendarDayView({
events,
calendars,
onSelectEvent,
onHoverEvent,
onHoverLeave,
onCreateAtTime,
timeFormat = "24h",
isMobile,
@@ -134,6 +138,8 @@ export function CalendarDayView({
calendar={calendarMap.get(calId)}
variant="chip"
onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
/>
);
})}
@@ -205,6 +211,8 @@ export function CalendarDayView({
calendar={calendarMap.get(calId)}
variant="block"
onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable
/>
<div
+80 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useMemo, useState, useCallback, type DragEvent } from "react";
import { useMemo, useState, useCallback, useRef, useEffect, type DragEvent } from "react";
import { useTranslations, useFormatter } from "next-intl";
import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
@@ -20,6 +20,8 @@ interface CalendarMonthViewProps {
calendars: Calendar[];
onSelectDate: (date: Date) => void;
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
firstDayOfWeek?: number;
isMobile?: boolean;
}
@@ -30,6 +32,8 @@ export function CalendarMonthView({
calendars,
onSelectDate,
onSelectEvent,
onHoverEvent,
onHoverLeave,
firstDayOfWeek = 1,
isMobile,
}: CalendarMonthViewProps) {
@@ -88,6 +92,32 @@ export function CalendarMonthView({
}, [days]);
const [dropDayKey, setDropDayKey] = useState<string | null>(null);
const [overflowDay, setOverflowDay] = useState<{ key: string; events: CalendarEvent[]; anchorRect: DOMRect; dayLabel: string } | null>(null);
const overflowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!overflowDay) return;
const handleClickOutside = (e: MouseEvent) => {
if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) {
setOverflowDay(null);
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") setOverflowDay(null);
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
};
}, [overflowDay]);
const handleMoreClick = useCallback((e: React.MouseEvent, dayKey: string, dayEvents: CalendarEvent[], dayLabel: string) => {
e.stopPropagation();
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
setOverflowDay({ key: dayKey, events: dayEvents, anchorRect: rect, dayLabel });
}, []);
const handleCellDragOver = useCallback((e: DragEvent<HTMLDivElement>, dayKey: string) => {
if (!e.dataTransfer.types.includes("application/x-calendar-event")) return;
@@ -214,14 +244,20 @@ export function CalendarMonthView({
calendar={calendarMap.get(calId)}
variant="chip"
onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable
/>
);
})}
{dayEvents.length > maxVisible && (
<div className="text-[10px] text-muted-foreground px-1">
<button
type="button"
className="text-[10px] text-muted-foreground px-1 hover:text-foreground hover:underline cursor-pointer text-left w-full"
onClick={(e) => handleMoreClick(e, key, dayEvents, fullDateLabel)}
>
{t("events.more", { count: dayEvents.length - maxVisible })}
</div>
</button>
)}
</div>
)}
@@ -231,6 +267,47 @@ export function CalendarMonthView({
</div>
))}
</div>
{overflowDay && (() => {
const viewportW = typeof window !== "undefined" ? window.innerWidth : 1024;
const viewportH = typeof window !== "undefined" ? window.innerHeight : 768;
const popoverW = 260;
const popoverMaxH = 320;
let left = overflowDay.anchorRect.left;
let top = overflowDay.anchorRect.bottom + 4;
if (left + popoverW > viewportW - 8) left = viewportW - popoverW - 8;
if (left < 8) left = 8;
if (top + popoverMaxH > viewportH - 8) top = overflowDay.anchorRect.top - popoverMaxH - 4;
if (top < 8) top = 8;
return (
<div
ref={overflowRef}
className="fixed z-50 bg-background border border-border rounded-lg shadow-lg p-3 space-y-1 overflow-y-auto"
style={{ left, top, width: popoverW, maxHeight: popoverMaxH }}
role="dialog"
aria-label={overflowDay.dayLabel}
>
<div className="text-xs font-semibold text-foreground mb-2">{overflowDay.dayLabel}</div>
{overflowDay.events.map((ev) => {
const calId = Object.keys(ev.calendarIds)[0];
return (
<EventCard
key={ev.id}
event={ev}
calendar={calendarMap.get(calId)}
variant="chip"
onClick={(rect) => { setOverflowDay(null); onSelectEvent(ev, rect); }}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable
/>
);
})}
</div>
);
})()}
</div>
);
}
@@ -18,6 +18,8 @@ interface CalendarWeekViewProps {
calendars: Calendar[];
onSelectDate: (date: Date) => void;
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
onCreateAtTime: (date: Date, endDate?: Date) => void;
firstDayOfWeek?: number;
timeFormat?: "12h" | "24h";
@@ -33,6 +35,8 @@ export function CalendarWeekView({
calendars,
onSelectDate,
onSelectEvent,
onHoverEvent,
onHoverLeave,
onCreateAtTime,
firstDayOfWeek = 1,
timeFormat = "24h",
@@ -162,6 +166,8 @@ export function CalendarWeekView({
calendar={calendarMap.get(calId)}
variant="chip"
onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
/>
);
})}
@@ -280,6 +286,8 @@ export function CalendarWeekView({
calendar={calendarMap.get(calId)}
variant="block"
onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable
/>
<div
+7 -1
View File
@@ -13,6 +13,8 @@ interface EventCardProps {
calendar?: Calendar;
variant: "chip" | "block";
onClick?: (anchorRect: DOMRect) => void;
onMouseEnter?: (anchorRect: DOMRect) => void;
onMouseLeave?: () => void;
isSelected?: boolean;
draggable?: boolean;
}
@@ -56,7 +58,7 @@ function createEventDragPreview(title: string, timeRange: string, color: string)
return el;
}
export function EventCard({ event, calendar, variant, onClick, isSelected, draggable: isDraggable }: EventCardProps) {
export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onMouseLeave, isSelected, draggable: isDraggable }: EventCardProps) {
const t = useTranslations("calendar");
const [isBeingDragged, setIsBeingDragged] = useState(false);
const color = getEventColor(event, calendar);
@@ -101,6 +103,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
return (
<button
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
onMouseLeave={() => onMouseLeave?.()}
aria-label={ariaLabel}
{...dragProps}
className={cn(
@@ -124,6 +128,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
return (
<button
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
onMouseLeave={() => onMouseLeave?.()}
aria-label={ariaLabel}
{...dragProps}
data-calendar-event
+6 -3
View File
@@ -29,6 +29,8 @@ interface EventDetailPopoverProps {
onClose: () => void;
onSaveNote: (note: string) => void;
onRsvp?: (status: CalendarParticipant["participationStatus"]) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
currentUserEmails?: string[];
timeFormat?: "12h" | "24h";
isMobile?: boolean;
@@ -119,6 +121,8 @@ export function EventDetailPopover({
onClose,
onSaveNote,
onRsvp,
onMouseEnter,
onMouseLeave,
currentUserEmails = [],
timeFormat = "24h",
isMobile,
@@ -203,17 +207,14 @@ export function EventDetailPopover({
onClose();
}
};
const handleScroll = () => onClose();
const timer = setTimeout(() => {
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("scroll", handleScroll, true);
}, 0);
return () => {
clearTimeout(timer);
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("scroll", handleScroll, true);
};
}, [onClose]);
@@ -252,6 +253,8 @@ export function EventDetailPopover({
ref={popoverRef}
role="dialog"
aria-label={event.title || t("events.no_title")}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={cn(
"fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out",
isMobile
+12 -3
View File
@@ -146,10 +146,19 @@ export function EmailList({
const handleBatchDelete = async () => {
if (!client || isProcessing) return;
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
const isInTrash = currentMailbox?.role === 'trash';
const confirmed = await confirmDialog({
title: t('batch_actions.delete_confirm_title'),
message: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
confirmText: t('batch_actions.delete'),
title: isInTrash
? t('permanent_delete_confirm_title')
: t('batch_actions.delete_confirm_title'),
message: isInTrash
? t('permanent_delete_confirm_batch_message', { count: selectedEmailIds.size })
: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
confirmText: isInTrash
? t('permanent_delete')
: t('batch_actions.delete'),
variant: "destructive",
});
if (!confirmed) return;
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Als gelesen markieren",
"mark_unread": "Als ungelesen markieren",
"delete": "Löschen",
"delete_confirm_title": "E-Mails löschen",
"delete_confirm_message": "Möchten Sie {count, plural, one {1 E-Mail} other {# E-Mails}} wirklich löschen?",
"clear_selection": "Auswahl aufheben"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "Keine E-Mail ausgewählt",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Mark as read",
"mark_unread": "Mark as unread",
"delete": "Delete",
"delete_confirm_title": "Delete emails",
"delete_confirm_message": "Are you sure you want to delete {count, plural, one {1 email} other {# emails}}?",
"clear_selection": "Clear selection"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "No email selected",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Marcar como leído",
"mark_unread": "Marcar como no leído",
"delete": "Eliminar",
"delete_confirm_title": "Eliminar correos",
"delete_confirm_message": "¿Estás seguro de que quieres eliminar {count, plural, one {1 correo} other {# correos}}?",
"clear_selection": "Limpiar selección"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "Ningún correo seleccionado",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Marquer comme lu",
"mark_unread": "Marquer comme non lu",
"delete": "Supprimer",
"delete_confirm_title": "Supprimer les e-mails",
"delete_confirm_message": "Voulez-vous vraiment supprimer {count, plural, one {1 e-mail} other {# e-mails}} ?",
"clear_selection": "Effacer la sélection"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "Aucun email sélectionné",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Segna come letto",
"mark_unread": "Segna come non letto",
"delete": "Elimina",
"delete_confirm_title": "Elimina e-mail",
"delete_confirm_message": "Sei sicuro di voler eliminare {count, plural, one {1 e-mail} other {# e-mail}}?",
"clear_selection": "Cancella selezione"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "Nessun messaggio selezionato",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "既読にする",
"mark_unread": "未読にする",
"delete": "削除",
"delete_confirm_title": "メールを削除",
"delete_confirm_message": "{count, plural, one {1件} other {#件}}のメールを削除しますか?",
"clear_selection": "選択を解除"
}
},
"permanent_delete": "完全に削除",
"permanent_delete_confirm_title": "完全に削除",
"permanent_delete_confirm_message": "このメールは完全に削除されます。この操作は取り消せません。",
"permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。"
},
"email_viewer": {
"no_email_selected": "メールが選択されていません",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Markeren als gelezen",
"mark_unread": "Markeren als ongelezen",
"delete": "Verwijderen",
"delete_confirm_title": "E-mails verwijderen",
"delete_confirm_message": "Weet u zeker dat u {count, plural, one {1 e-mail} other {# e-mails}} wilt verwijderen?",
"clear_selection": "Selectie wissen"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "Geen e-mail geselecteerd",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Marcar como lido",
"mark_unread": "Marcar como não lido",
"delete": "Excluir",
"delete_confirm_title": "Excluir e-mails",
"delete_confirm_message": "Tem certeza que deseja excluir {count, plural, one {1 e-mail} other {# e-mails}}?",
"clear_selection": "Limpar seleção"
}
},
"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."
},
"email_viewer": {
"no_email_selected": "Nenhum e-mail selecionado",
+4 -4
View File
@@ -60,7 +60,7 @@ interface EmailStore {
fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
fetchQuota: (client: JMAPClient) => Promise<void>;
sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string) => Promise<void>;
deleteEmail: (client: JMAPClient, emailId: string) => Promise<void>;
deleteEmail: (client: JMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>;
moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>;
searchEmails: (client: JMAPClient, query: string) => Promise<void>;
@@ -384,7 +384,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
}
},
deleteEmail: async (client, emailId) => {
deleteEmail: async (client, emailId, forceDelete) => {
try {
// Get the email to check if it's unread and which mailboxes it belongs to
const email = get().emails.find(e => e.id === emailId);
@@ -401,8 +401,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
// If deleteAction is 'trash', try to move to trash mailbox
if (deleteAction === 'trash') {
// 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
const trashMailbox = mailboxes.find(mb => {
if (accountId) {