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); setShowEventModal(true);
}, []); }, []);
const handleSelectEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
setDetailEvent(event);
setDetailAnchorRect(anchorRect);
}, []);
const closeDetail = useCallback(() => { const closeDetail = useCallback(() => {
if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; }
setDetailEvent(null); setDetailEvent(null);
setDetailAnchorRect(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(() => { const handleEditFromDetail = useCallback(() => {
if (detailEvent) { if (detailEvent) {
const ev = detailEvent; const ev = detailEvent;
@@ -581,6 +600,8 @@ export default function CalendarPage() {
calendars={calendars} calendars={calendars}
onSelectDate={handleSelectDate} onSelectDate={handleSelectDate}
onSelectEvent={handleSelectEvent} onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
firstDayOfWeek={firstDayOfWeek} firstDayOfWeek={firstDayOfWeek}
isMobile={isMobile} isMobile={isMobile}
/> />
@@ -593,6 +614,8 @@ export default function CalendarPage() {
calendars={calendars} calendars={calendars}
onSelectDate={handleSelectDate} onSelectDate={handleSelectDate}
onSelectEvent={handleSelectEvent} onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
onCreateAtTime={openCreateModal} onCreateAtTime={openCreateModal}
firstDayOfWeek={firstDayOfWeek} firstDayOfWeek={firstDayOfWeek}
timeFormat={timeFormat} timeFormat={timeFormat}
@@ -606,6 +629,8 @@ export default function CalendarPage() {
events={visibleEvents} events={visibleEvents}
calendars={calendars} calendars={calendars}
onSelectEvent={handleSelectEvent} onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
onCreateAtTime={openCreateModal} onCreateAtTime={openCreateModal}
timeFormat={timeFormat} timeFormat={timeFormat}
isMobile={isMobile} isMobile={isMobile}
@@ -618,6 +643,8 @@ export default function CalendarPage() {
events={visibleEvents} events={visibleEvents}
calendars={calendars} calendars={calendars}
onSelectEvent={handleSelectEvent} onSelectEvent={handleSelectEvent}
onHoverEvent={handleHoverEvent}
onHoverLeave={handleHoverLeave}
timeFormat={timeFormat} timeFormat={timeFormat}
/> />
); );
@@ -739,6 +766,8 @@ export default function CalendarPage() {
onClose={closeDetail} onClose={closeDetail}
onSaveNote={handleSaveNoteFromDetail} onSaveNote={handleSaveNoteFromDetail}
onRsvp={handleRsvpFromDetail} onRsvp={handleRsvpFromDetail}
onMouseEnter={() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } }}
onMouseLeave={handleHoverLeave}
currentUserEmails={currentUserEmails} currentUserEmails={currentUserEmails}
timeFormat={timeFormat} timeFormat={timeFormat}
isMobile={isMobile} isMobile={isMobile}
+29 -4
View File
@@ -454,10 +454,35 @@ export default function Home() {
const handleDelete = async () => { const handleDelete = async () => {
if (!client || !selectedEmail) return; if (!client || !selectedEmail) return;
try { // Check if we're currently in the trash folder
await deleteEmail(client, selectedEmail.id); const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
} catch (error) { const isInTrash = currentMailbox?.role === 'trash';
console.error("Failed to delete email:", error);
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[]; events: CalendarEvent[];
calendars: Calendar[]; calendars: Calendar[];
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
timeFormat?: "12h" | "24h"; timeFormat?: "12h" | "24h";
} }
@@ -28,6 +30,8 @@ export function CalendarAgendaView({
events, events,
calendars, calendars,
onSelectEvent, onSelectEvent,
onHoverEvent,
onHoverLeave,
timeFormat = "24h", timeFormat = "24h",
}: CalendarAgendaViewProps) { }: CalendarAgendaViewProps) {
const t = useTranslations("calendar"); const t = useTranslations("calendar");
@@ -140,6 +144,8 @@ export function CalendarAgendaView({
<button <button
key={ev.id} key={ev.id}
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())} 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" 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]"> <div className="flex flex-col items-center pt-0.5 min-w-[60px]">
@@ -15,6 +15,8 @@ interface CalendarDayViewProps {
events: CalendarEvent[]; events: CalendarEvent[];
calendars: Calendar[]; calendars: Calendar[];
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
onCreateAtTime: (date: Date, endDate?: Date) => void; onCreateAtTime: (date: Date, endDate?: Date) => void;
timeFormat?: "12h" | "24h"; timeFormat?: "12h" | "24h";
isMobile?: boolean; isMobile?: boolean;
@@ -28,6 +30,8 @@ export function CalendarDayView({
events, events,
calendars, calendars,
onSelectEvent, onSelectEvent,
onHoverEvent,
onHoverLeave,
onCreateAtTime, onCreateAtTime,
timeFormat = "24h", timeFormat = "24h",
isMobile, isMobile,
@@ -134,6 +138,8 @@ export function CalendarDayView({
calendar={calendarMap.get(calId)} calendar={calendarMap.get(calId)}
variant="chip" variant="chip"
onClick={(rect) => onSelectEvent(ev, rect)} onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
/> />
); );
})} })}
@@ -205,6 +211,8 @@ export function CalendarDayView({
calendar={calendarMap.get(calId)} calendar={calendarMap.get(calId)}
variant="block" variant="block"
onClick={(rect) => onSelectEvent(ev, rect)} onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable draggable
/> />
<div <div
+80 -3
View File
@@ -1,6 +1,6 @@
"use client"; "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 { useTranslations, useFormatter } from "next-intl";
import { import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
@@ -20,6 +20,8 @@ interface CalendarMonthViewProps {
calendars: Calendar[]; calendars: Calendar[];
onSelectDate: (date: Date) => void; onSelectDate: (date: Date) => void;
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
firstDayOfWeek?: number; firstDayOfWeek?: number;
isMobile?: boolean; isMobile?: boolean;
} }
@@ -30,6 +32,8 @@ export function CalendarMonthView({
calendars, calendars,
onSelectDate, onSelectDate,
onSelectEvent, onSelectEvent,
onHoverEvent,
onHoverLeave,
firstDayOfWeek = 1, firstDayOfWeek = 1,
isMobile, isMobile,
}: CalendarMonthViewProps) { }: CalendarMonthViewProps) {
@@ -88,6 +92,32 @@ export function CalendarMonthView({
}, [days]); }, [days]);
const [dropDayKey, setDropDayKey] = useState<string | null>(null); 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) => { const handleCellDragOver = useCallback((e: DragEvent<HTMLDivElement>, dayKey: string) => {
if (!e.dataTransfer.types.includes("application/x-calendar-event")) return; if (!e.dataTransfer.types.includes("application/x-calendar-event")) return;
@@ -214,14 +244,20 @@ export function CalendarMonthView({
calendar={calendarMap.get(calId)} calendar={calendarMap.get(calId)}
variant="chip" variant="chip"
onClick={(rect) => onSelectEvent(ev, rect)} onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable draggable
/> />
); );
})} })}
{dayEvents.length > maxVisible && ( {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 })} {t("events.more", { count: dayEvents.length - maxVisible })}
</div> </button>
)} )}
</div> </div>
)} )}
@@ -231,6 +267,47 @@ export function CalendarMonthView({
</div> </div>
))} ))}
</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> </div>
); );
} }
@@ -18,6 +18,8 @@ interface CalendarWeekViewProps {
calendars: Calendar[]; calendars: Calendar[];
onSelectDate: (date: Date) => void; onSelectDate: (date: Date) => void;
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
onHoverLeave?: () => void;
onCreateAtTime: (date: Date, endDate?: Date) => void; onCreateAtTime: (date: Date, endDate?: Date) => void;
firstDayOfWeek?: number; firstDayOfWeek?: number;
timeFormat?: "12h" | "24h"; timeFormat?: "12h" | "24h";
@@ -33,6 +35,8 @@ export function CalendarWeekView({
calendars, calendars,
onSelectDate, onSelectDate,
onSelectEvent, onSelectEvent,
onHoverEvent,
onHoverLeave,
onCreateAtTime, onCreateAtTime,
firstDayOfWeek = 1, firstDayOfWeek = 1,
timeFormat = "24h", timeFormat = "24h",
@@ -162,6 +166,8 @@ export function CalendarWeekView({
calendar={calendarMap.get(calId)} calendar={calendarMap.get(calId)}
variant="chip" variant="chip"
onClick={(rect) => onSelectEvent(ev, rect)} onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
/> />
); );
})} })}
@@ -280,6 +286,8 @@ export function CalendarWeekView({
calendar={calendarMap.get(calId)} calendar={calendarMap.get(calId)}
variant="block" variant="block"
onClick={(rect) => onSelectEvent(ev, rect)} onClick={(rect) => onSelectEvent(ev, rect)}
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
onMouseLeave={onHoverLeave}
draggable draggable
/> />
<div <div
+7 -1
View File
@@ -13,6 +13,8 @@ interface EventCardProps {
calendar?: Calendar; calendar?: Calendar;
variant: "chip" | "block"; variant: "chip" | "block";
onClick?: (anchorRect: DOMRect) => void; onClick?: (anchorRect: DOMRect) => void;
onMouseEnter?: (anchorRect: DOMRect) => void;
onMouseLeave?: () => void;
isSelected?: boolean; isSelected?: boolean;
draggable?: boolean; draggable?: boolean;
} }
@@ -56,7 +58,7 @@ function createEventDragPreview(title: string, timeRange: string, color: string)
return el; 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 t = useTranslations("calendar");
const [isBeingDragged, setIsBeingDragged] = useState(false); const [isBeingDragged, setIsBeingDragged] = useState(false);
const color = getEventColor(event, calendar); const color = getEventColor(event, calendar);
@@ -101,6 +103,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
return ( return (
<button <button
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }} onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
onMouseLeave={() => onMouseLeave?.()}
aria-label={ariaLabel} aria-label={ariaLabel}
{...dragProps} {...dragProps}
className={cn( className={cn(
@@ -124,6 +128,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
return ( return (
<button <button
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }} onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
onMouseLeave={() => onMouseLeave?.()}
aria-label={ariaLabel} aria-label={ariaLabel}
{...dragProps} {...dragProps}
data-calendar-event data-calendar-event
+6 -3
View File
@@ -29,6 +29,8 @@ interface EventDetailPopoverProps {
onClose: () => void; onClose: () => void;
onSaveNote: (note: string) => void; onSaveNote: (note: string) => void;
onRsvp?: (status: CalendarParticipant["participationStatus"]) => void; onRsvp?: (status: CalendarParticipant["participationStatus"]) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
currentUserEmails?: string[]; currentUserEmails?: string[];
timeFormat?: "12h" | "24h"; timeFormat?: "12h" | "24h";
isMobile?: boolean; isMobile?: boolean;
@@ -119,6 +121,8 @@ export function EventDetailPopover({
onClose, onClose,
onSaveNote, onSaveNote,
onRsvp, onRsvp,
onMouseEnter,
onMouseLeave,
currentUserEmails = [], currentUserEmails = [],
timeFormat = "24h", timeFormat = "24h",
isMobile, isMobile,
@@ -203,17 +207,14 @@ export function EventDetailPopover({
onClose(); onClose();
} }
}; };
const handleScroll = () => onClose();
const timer = setTimeout(() => { const timer = setTimeout(() => {
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("scroll", handleScroll, true);
}, 0); }, 0);
return () => { return () => {
clearTimeout(timer); clearTimeout(timer);
document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("scroll", handleScroll, true);
}; };
}, [onClose]); }, [onClose]);
@@ -252,6 +253,8 @@ export function EventDetailPopover({
ref={popoverRef} ref={popoverRef}
role="dialog" role="dialog"
aria-label={event.title || t("events.no_title")} aria-label={event.title || t("events.no_title")}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={cn( className={cn(
"fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out", "fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out",
isMobile isMobile
+12 -3
View File
@@ -146,10 +146,19 @@ export function EmailList({
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
if (!client || isProcessing) return; if (!client || isProcessing) return;
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
const isInTrash = currentMailbox?.role === 'trash';
const confirmed = await confirmDialog({ const confirmed = await confirmDialog({
title: t('batch_actions.delete_confirm_title'), title: isInTrash
message: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }), ? t('permanent_delete_confirm_title')
confirmText: t('batch_actions.delete'), : 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", variant: "destructive",
}); });
if (!confirmed) return; if (!confirmed) return;
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Als gelesen markieren", "mark_read": "Als gelesen markieren",
"mark_unread": "Als ungelesen markieren", "mark_unread": "Als ungelesen markieren",
"delete": "Löschen", "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" "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": { "email_viewer": {
"no_email_selected": "Keine E-Mail ausgewählt", "no_email_selected": "Keine E-Mail ausgewählt",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Mark as read", "mark_read": "Mark as read",
"mark_unread": "Mark as unread", "mark_unread": "Mark as unread",
"delete": "Delete", "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" "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": { "email_viewer": {
"no_email_selected": "No email selected", "no_email_selected": "No email selected",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Marcar como leído", "mark_read": "Marcar como leído",
"mark_unread": "Marcar como no leído", "mark_unread": "Marcar como no leído",
"delete": "Eliminar", "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" "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": { "email_viewer": {
"no_email_selected": "Ningún correo seleccionado", "no_email_selected": "Ningún correo seleccionado",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Marquer comme lu", "mark_read": "Marquer comme lu",
"mark_unread": "Marquer comme non lu", "mark_unread": "Marquer comme non lu",
"delete": "Supprimer", "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" "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": { "email_viewer": {
"no_email_selected": "Aucun email sélectionné", "no_email_selected": "Aucun email sélectionné",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Segna come letto", "mark_read": "Segna come letto",
"mark_unread": "Segna come non letto", "mark_unread": "Segna come non letto",
"delete": "Elimina", "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" "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": { "email_viewer": {
"no_email_selected": "Nessun messaggio selezionato", "no_email_selected": "Nessun messaggio selezionato",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "既読にする", "mark_read": "既読にする",
"mark_unread": "未読にする", "mark_unread": "未読にする",
"delete": "削除", "delete": "削除",
"delete_confirm_title": "メールを削除",
"delete_confirm_message": "{count, plural, one {1件} other {#件}}のメールを削除しますか?",
"clear_selection": "選択を解除" "clear_selection": "選択を解除"
} },
"permanent_delete": "完全に削除",
"permanent_delete_confirm_title": "完全に削除",
"permanent_delete_confirm_message": "このメールは完全に削除されます。この操作は取り消せません。",
"permanent_delete_confirm_batch_message": "{count, plural, one {1件} other {#件}}のメールが完全に削除されます。この操作は取り消せません。"
}, },
"email_viewer": { "email_viewer": {
"no_email_selected": "メールが選択されていません", "no_email_selected": "メールが選択されていません",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Markeren als gelezen", "mark_read": "Markeren als gelezen",
"mark_unread": "Markeren als ongelezen", "mark_unread": "Markeren als ongelezen",
"delete": "Verwijderen", "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" "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": { "email_viewer": {
"no_email_selected": "Geen e-mail geselecteerd", "no_email_selected": "Geen e-mail geselecteerd",
+7 -1
View File
@@ -120,8 +120,14 @@
"mark_read": "Marcar como lido", "mark_read": "Marcar como lido",
"mark_unread": "Marcar como não lido", "mark_unread": "Marcar como não lido",
"delete": "Excluir", "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" "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": { "email_viewer": {
"no_email_selected": "Nenhum e-mail selecionado", "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>; fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
fetchQuota: (client: JMAPClient) => Promise<void>; 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>; 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>; markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>;
moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>; moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>;
searchEmails: (client: JMAPClient, query: 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 { try {
// Get the email to check if it's unread and which mailboxes it belongs to // Get the email to check if it's unread and which mailboxes it belongs to
const email = get().emails.find(e => e.id === emailId); 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 currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined; const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
// If deleteAction is 'trash', try to move to trash mailbox // If deleteAction is 'trash' and not forced permanent delete, try to move to trash mailbox
if (deleteAction === 'trash') { if (deleteAction === 'trash' && !forceDelete) {
// Find trash mailbox for the correct account // Find trash mailbox for the correct account
const trashMailbox = mailboxes.find(mb => { const trashMailbox = mailboxes.find(mb => {
if (accountId) { if (accountId) {