rework
This commit is contained in:
+39
-23
@@ -108,6 +108,7 @@ export default function Home() {
|
|||||||
const [pendingMailtoAccountChoice, setPendingMailtoAccountChoice] = useState<ParsedMailto | null>(null);
|
const [pendingMailtoAccountChoice, setPendingMailtoAccountChoice] = useState<ParsedMailto | null>(null);
|
||||||
const [isProtocolAccountSwitching, setIsProtocolAccountSwitching] = useState(false);
|
const [isProtocolAccountSwitching, setIsProtocolAccountSwitching] = useState(false);
|
||||||
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const lastUndoToastSubmissionRef = useRef<string | null>(null);
|
||||||
const { isAuthenticated, client, logout, checkAuth, switchAccount, activeAccountId, isLoading: authLoading, connectionLost, isRateLimited, rateLimitUntil } = useAuthStore();
|
const { isAuthenticated, client, logout, checkAuth, switchAccount, activeAccountId, isLoading: authLoading, connectionLost, isRateLimited, rateLimitUntil } = useAuthStore();
|
||||||
const { identities } = useIdentityStore();
|
const { identities } = useIdentityStore();
|
||||||
useIdentitySync();
|
useIdentitySync();
|
||||||
@@ -1256,6 +1257,44 @@ export default function Home() {
|
|||||||
if (isMobile) setActiveView('viewer');
|
if (isMobile) setActiveView('viewer');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingUndoSend || !client) return;
|
||||||
|
if (lastUndoToastSubmissionRef.current === pendingUndoSend.submissionId) return;
|
||||||
|
|
||||||
|
lastUndoToastSubmissionRef.current = pendingUndoSend.submissionId;
|
||||||
|
const pending = pendingUndoSend;
|
||||||
|
|
||||||
|
toast.info(t('email_viewer.undo_send_scheduled'), {
|
||||||
|
duration: 8000,
|
||||||
|
action: {
|
||||||
|
label: t('email_viewer.undo_send'),
|
||||||
|
onClick: () => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const restored = await cancelUndoSend(client, pending);
|
||||||
|
if (restored && !pending.isSmime) {
|
||||||
|
await handleEditDraft(restored);
|
||||||
|
}
|
||||||
|
if (isScheduledView) await fetchScheduledEmails(client);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to undo scheduled send:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const current = useEmailStore.getState().pendingUndoSend;
|
||||||
|
if (current?.submissionId === pending.submissionId) {
|
||||||
|
clearPendingUndoSend();
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [cancelUndoSend, clearPendingUndoSend, client, fetchScheduledEmails, isScheduledView, pendingUndoSend?.submissionId, t]);
|
||||||
|
|
||||||
const handleReplyAll = async () => {
|
const handleReplyAll = async () => {
|
||||||
if (selectedEmail) {
|
if (selectedEmail) {
|
||||||
const ok = await emailHooks.onBeforeReplyAll.intercept({
|
const ok = await emailHooks.onBeforeReplyAll.intercept({
|
||||||
@@ -2608,11 +2647,6 @@ export default function Home() {
|
|||||||
setShowComposer(true);
|
setShowComposer(true);
|
||||||
if (isMobile) setActiveView('viewer');
|
if (isMobile) setActiveView('viewer');
|
||||||
}}
|
}}
|
||||||
onRescheduleScheduled={async (email, delayedUntil) => {
|
|
||||||
if (client && email.emailSubmissionId && email.scheduledIdentityId) {
|
|
||||||
await rescheduleScheduledEmail(client, email.emailSubmissionId, email.id, email.scheduledIdentityId, delayedUntil);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onEmailSelect={handleEmailSelect}
|
onEmailSelect={handleEmailSelect}
|
||||||
onEmailDoubleClick={isEmbedded ? ((email) => {
|
onEmailDoubleClick={isEmbedded ? ((email) => {
|
||||||
useProTabStore.getState().openEmailTab({
|
useProTabStore.getState().openEmailTab({
|
||||||
@@ -2964,24 +2998,6 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
<ConfirmDialog {...confirmDialogProps} />
|
<ConfirmDialog {...confirmDialogProps} />
|
||||||
<PromptDialog {...promptDialogProps} />
|
<PromptDialog {...promptDialogProps} />
|
||||||
{pendingUndoSend && (
|
|
||||||
<div className="fixed bottom-4 left-1/2 z-[70] flex -translate-x-1/2 items-center gap-3 rounded-lg border border-border bg-background px-4 py-3 shadow-lg">
|
|
||||||
<span className="text-sm text-foreground">{t('email_viewer.undo_send_scheduled')}</span>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!client) return;
|
|
||||||
const restored = await cancelUndoSend(client, pendingUndoSend);
|
|
||||||
if (restored && !pendingUndoSend.isSmime) {
|
|
||||||
await handleEditDraft(restored);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('email_viewer.undo_send')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<TotpReauthDialog />
|
<TotpReauthDialog />
|
||||||
</div>
|
</div>
|
||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useFocusTrap } from "@/hooks/use-focus-trap";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock, CalendarClock } from "lucide-react";
|
import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock, CalendarClock, ChevronDown } from "lucide-react";
|
||||||
import { cn, formatFileSize, formatDateTime, generateUUID } from "@/lib/utils";
|
import { cn, formatFileSize, formatDateTime, generateUUID } from "@/lib/utils";
|
||||||
import { debug } from "@/lib/debug";
|
import { debug } from "@/lib/debug";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
@@ -160,6 +160,18 @@ function buildEmbeddedSignatureHtml(
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLocalDateTimeInput(date: Date): string {
|
||||||
|
const pad = (value: number) => String(value).padStart(2, '0');
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultScheduleValue(): string {
|
||||||
|
const tomorrowAtEight = new Date();
|
||||||
|
tomorrowAtEight.setDate(tomorrowAtEight.getDate() + 1);
|
||||||
|
tomorrowAtEight.setHours(8, 0, 0, 0);
|
||||||
|
return formatLocalDateTimeInput(tomorrowAtEight);
|
||||||
|
}
|
||||||
|
|
||||||
export function EmailComposer({
|
export function EmailComposer({
|
||||||
onSend,
|
onSend,
|
||||||
onScheduledSendCreated,
|
onScheduledSendCreated,
|
||||||
@@ -373,6 +385,8 @@ export function EmailComposer({
|
|||||||
const [showScheduleDialog, setShowScheduleDialog] = useState(false);
|
const [showScheduleDialog, setShowScheduleDialog] = useState(false);
|
||||||
const [scheduleValue, setScheduleValue] = useState('');
|
const [scheduleValue, setScheduleValue] = useState('');
|
||||||
const [scheduleError, setScheduleError] = useState('');
|
const [scheduleError, setScheduleError] = useState('');
|
||||||
|
const [showSendMenu, setShowSendMenu] = useState(false);
|
||||||
|
const sendMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const saveTemplateModalRef = useFocusTrap({
|
const saveTemplateModalRef = useFocusTrap({
|
||||||
isActive: showSaveAsTemplate,
|
isActive: showSaveAsTemplate,
|
||||||
@@ -465,6 +479,23 @@ export function EmailComposer({
|
|||||||
}
|
}
|
||||||
}, [signatureIdentity?.id, signatureIdentity?.htmlSignature, signatureIdentity?.textSignature, signatureSeparatorEnabled, signaturePosition, mode, plainTextMode]);
|
}, [signatureIdentity?.id, signatureIdentity?.htmlSignature, signatureIdentity?.textSignature, signatureSeparatorEnabled, signaturePosition, mode, plainTextMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutsideSendMenu = (event: MouseEvent) => {
|
||||||
|
if (!sendMenuRef.current?.contains(event.target as Node)) {
|
||||||
|
setShowSendMenu(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handleClickOutsideSendMenu);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutsideSendMenu);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openScheduleDialog = useCallback(() => {
|
||||||
|
setScheduleError('');
|
||||||
|
setScheduleValue(getDefaultScheduleValue());
|
||||||
|
setShowScheduleDialog(true);
|
||||||
|
setShowSendMenu(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoSelectReplyIdentity) return;
|
if (!autoSelectReplyIdentity) return;
|
||||||
if (selectedIdentityId || initialData?.selectedIdentityId) return;
|
if (selectedIdentityId || initialData?.selectedIdentityId) return;
|
||||||
@@ -1550,8 +1581,10 @@ export function EmailComposer({
|
|||||||
if (e.defaultPrevented) return;
|
if (e.defaultPrevented) return;
|
||||||
|
|
||||||
const isPlainEscape = e.key === 'Escape' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
|
const isPlainEscape = e.key === 'Escape' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
|
||||||
const isSendShortcut = e.key === 'Enter' && e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
|
const hasPrimaryModifier = e.ctrlKey || e.metaKey;
|
||||||
if (!isPlainEscape && !isSendShortcut) return;
|
const isSendShortcut = e.key === 'Enter' && hasPrimaryModifier && !e.altKey && !e.shiftKey;
|
||||||
|
const isScheduleShortcut = e.key === 'Enter' && hasPrimaryModifier && !e.altKey && e.shiftKey;
|
||||||
|
if (!isPlainEscape && !isSendShortcut && !isScheduleShortcut) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
showTemplatePicker ||
|
showTemplatePicker ||
|
||||||
@@ -1577,6 +1610,12 @@ export function EmailComposer({
|
|||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSend();
|
handleSend();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScheduleShortcut) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!e.repeat && client?.hasDelayedSend()) openScheduleDialog();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2007,22 +2046,6 @@ export function EmailComposer({
|
|||||||
>
|
>
|
||||||
<BookmarkPlus className="w-4 h-4" />
|
<BookmarkPlus className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{client?.hasDelayedSend() && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => {
|
|
||||||
setScheduleError('');
|
|
||||||
setScheduleValue('');
|
|
||||||
setShowScheduleDialog(true);
|
|
||||||
}}
|
|
||||||
title={t('schedule_send')}
|
|
||||||
className="h-9 w-9"
|
|
||||||
>
|
|
||||||
<CalendarClock className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* S/MIME toggles */}
|
{/* S/MIME toggles */}
|
||||||
{canSmimeSign && (
|
{canSmimeSign && (
|
||||||
<>
|
<>
|
||||||
@@ -2060,15 +2083,56 @@ export function EmailComposer({
|
|||||||
>
|
>
|
||||||
{t('discard')}
|
{t('discard')}
|
||||||
</button>
|
</button>
|
||||||
<Button
|
{client?.hasDelayedSend() ? (
|
||||||
onClick={() => handleSend()}
|
<div ref={sendMenuRef} className="relative hidden md:inline-flex">
|
||||||
disabled={!canSend}
|
<Button
|
||||||
title={getSendTooltip()}
|
onClick={() => handleSend()}
|
||||||
className="hidden md:inline-flex"
|
disabled={!canSend}
|
||||||
>
|
title={getSendTooltip()}
|
||||||
<Send className="w-4 h-4 mr-2" />
|
className="rounded-r-none border-r border-primary-foreground/20"
|
||||||
{t('send')}
|
>
|
||||||
</Button>
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
{t('send')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowSendMenu((open) => !open)}
|
||||||
|
disabled={!canSend}
|
||||||
|
title={t('schedule_send')}
|
||||||
|
className="rounded-l-none px-2"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={showSendMenu}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
{showSendMenu && (
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
className="absolute right-0 bottom-full z-50 mb-2 min-w-44 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={openScheduleDialog}
|
||||||
|
className="flex w-full items-center gap-2 rounded-sm px-3 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
||||||
|
>
|
||||||
|
<CalendarClock className="w-4 h-4" />
|
||||||
|
{t('schedule_send')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleSend()}
|
||||||
|
disabled={!canSend}
|
||||||
|
title={getSendTooltip()}
|
||||||
|
className="hidden md:inline-flex"
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
{t('send')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||||
import { ThreadListItem } from "./thread-list-item";
|
import { ThreadListItem } from "./thread-list-item";
|
||||||
import { EmailContextMenu } from "./email-context-menu";
|
import { EmailContextMenu } from "./email-context-menu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, formatDateTime } from "@/lib/utils";
|
||||||
import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX, AlertTriangle, CalendarClock, XCircle, Edit3 } from "lucide-react";
|
import { Trash2, Mail, MailX, MailOpen, Loader2, SearchX, AlertTriangle, CalendarClock } from "lucide-react";
|
||||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||||
@@ -18,7 +18,6 @@ import { useTranslations } from "next-intl";
|
|||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { SearchChips } from "@/components/search/search-chips";
|
import { SearchChips } from "@/components/search/search-chips";
|
||||||
import { isFilterEmpty, DEFAULT_SEARCH_FILTERS } from "@/lib/jmap/search-utils";
|
import { isFilterEmpty, DEFAULT_SEARCH_FILTERS } from "@/lib/jmap/search-utils";
|
||||||
import { toast } from "@/stores/toast-store";
|
|
||||||
|
|
||||||
interface EmailListProps {
|
interface EmailListProps {
|
||||||
emails: Email[];
|
emails: Email[];
|
||||||
@@ -44,7 +43,6 @@ interface EmailListProps {
|
|||||||
onLoadMoreScheduled?: () => void;
|
onLoadMoreScheduled?: () => void;
|
||||||
onCancelScheduled?: (email: Email) => void | Promise<void>;
|
onCancelScheduled?: (email: Email) => void | Promise<void>;
|
||||||
onCancelScheduledForEdit?: (email: Email) => void | Promise<void>;
|
onCancelScheduledForEdit?: (email: Email) => void | Promise<void>;
|
||||||
onRescheduleScheduled?: (email: Email, delayedUntil: string) => void | Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmailList({
|
export function EmailList({
|
||||||
@@ -71,10 +69,8 @@ export function EmailList({
|
|||||||
onLoadMoreScheduled,
|
onLoadMoreScheduled,
|
||||||
onCancelScheduled,
|
onCancelScheduled,
|
||||||
onCancelScheduledForEdit,
|
onCancelScheduledForEdit,
|
||||||
onRescheduleScheduled,
|
|
||||||
}: EmailListProps) {
|
}: EmailListProps) {
|
||||||
const t = useTranslations('email_list');
|
const t = useTranslations('email_list');
|
||||||
const tComposer = useTranslations('email_composer');
|
|
||||||
const { client } = useAuthStore();
|
const { client } = useAuthStore();
|
||||||
const {
|
const {
|
||||||
selectedEmailIds,
|
selectedEmailIds,
|
||||||
@@ -119,6 +115,7 @@ export function EmailList({
|
|||||||
const density = useSettingsStore((state) => state.density);
|
const density = useSettingsStore((state) => state.density);
|
||||||
const showPreview = useSettingsStore((state) => state.showPreview);
|
const showPreview = useSettingsStore((state) => state.showPreview);
|
||||||
const mailLayout = useSettingsStore((state) => state.mailLayout);
|
const mailLayout = useSettingsStore((state) => state.mailLayout);
|
||||||
|
const timeFormat = useSettingsStore((state) => state.timeFormat);
|
||||||
const isFocusedMailLayout = mailLayout === 'focus';
|
const isFocusedMailLayout = mailLayout === 'focus';
|
||||||
|
|
||||||
const estimateSize = useCallback(() => {
|
const estimateSize = useCallback(() => {
|
||||||
@@ -237,30 +234,6 @@ export function EmailList({
|
|||||||
}
|
}
|
||||||
}, [client, hasMoreEmails, isLoadingMore, isLoading, isScheduledView, loadMoreEmails, onLoadMoreScheduled]);
|
}, [client, hasMoreEmails, isLoadingMore, isLoading, isScheduledView, loadMoreEmails, onLoadMoreScheduled]);
|
||||||
|
|
||||||
const promptForRescheduleDelayedUntil = useCallback((): string | null => {
|
|
||||||
const value = window.prompt(t('reschedule_prompt'));
|
|
||||||
if (!value) return null;
|
|
||||||
const time = new Date(value).getTime();
|
|
||||||
if (!Number.isFinite(time)) {
|
|
||||||
toast.error(tComposer('schedule_send_invalid'));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (time <= Date.now()) {
|
|
||||||
toast.error(tComposer('schedule_send_future'));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!client?.hasDelayedSend()) {
|
|
||||||
toast.error(tComposer('schedule_send_unsupported'));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const maxDelayedSend = client.getMaxDelayedSend();
|
|
||||||
if (maxDelayedSend > 0 && time > Date.now() + maxDelayedSend * 1000) {
|
|
||||||
toast.error(tComposer('schedule_send_too_late'));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(time).toISOString();
|
|
||||||
}, [client, t, tComposer]);
|
|
||||||
|
|
||||||
const handleToggleThreadExpansion = useCallback(async (threadId: string) => {
|
const handleToggleThreadExpansion = useCallback(async (threadId: string) => {
|
||||||
const isExpanded = expandedThreadIds.has(threadId);
|
const isExpanded = expandedThreadIds.has(threadId);
|
||||||
|
|
||||||
@@ -316,11 +289,6 @@ export function EmailList({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col min-h-0", className)}>
|
<div className={cn("flex flex-col min-h-0", className)}>
|
||||||
{/* Batch Actions Toolbar */}
|
{/* Batch Actions Toolbar */}
|
||||||
{isScheduledView && emails.length > 0 && (
|
|
||||||
<div className="border-b border-border bg-muted/20 px-4 py-2 text-xs text-muted-foreground">
|
|
||||||
{t('scheduled_actions_hint')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all duration-300 ease-in-out overflow-hidden",
|
"transition-all duration-300 ease-in-out overflow-hidden",
|
||||||
@@ -497,7 +465,7 @@ export function EmailList({
|
|||||||
onToggleExpand={() => handleToggleThreadExpansion(thread.threadId)}
|
onToggleExpand={() => handleToggleThreadExpansion(thread.threadId)}
|
||||||
onEmailSelect={(email) => onEmailSelect?.(email)}
|
onEmailSelect={(email) => onEmailSelect?.(email)}
|
||||||
onEmailDoubleClick={onEmailDoubleClick ? (email) => onEmailDoubleClick(email) : undefined}
|
onEmailDoubleClick={onEmailDoubleClick ? (email) => onEmailDoubleClick(email) : undefined}
|
||||||
onContextMenu={openContextMenu}
|
onContextMenu={isScheduledView ? undefined : openContextMenu}
|
||||||
onOpenConversation={onOpenConversation}
|
onOpenConversation={onOpenConversation}
|
||||||
onToggleStar={onToggleStar ? (email) => onToggleStar(email) : undefined}
|
onToggleStar={onToggleStar ? (email) => onToggleStar(email) : undefined}
|
||||||
onMarkAsRead={onMarkAsRead ? (email, read) => onMarkAsRead(email, read) : undefined}
|
onMarkAsRead={onMarkAsRead ? (email, read) => onMarkAsRead(email, read) : undefined}
|
||||||
@@ -508,39 +476,10 @@ export function EmailList({
|
|||||||
/>
|
/>
|
||||||
{isScheduledView && thread.latestEmail.isScheduled && (
|
{isScheduledView && thread.latestEmail.isScheduled && (
|
||||||
<div className="flex flex-wrap items-center gap-2 border-b border-border bg-muted/10 px-4 py-2 text-xs">
|
<div className="flex flex-wrap items-center gap-2 border-b border-border bg-muted/10 px-4 py-2 text-xs">
|
||||||
{thread.latestEmail.scheduledUndoStatus && thread.latestEmail.scheduledUndoStatus !== 'pending' && (
|
|
||||||
<span className="rounded-full bg-muted px-2 py-0.5 text-muted-foreground">
|
|
||||||
{thread.latestEmail.scheduledUndoStatus}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="flex items-center gap-1 text-muted-foreground">
|
<span className="flex items-center gap-1 text-muted-foreground">
|
||||||
<CalendarClock className="w-3.5 h-3.5" />
|
<CalendarClock className="w-3.5 h-3.5" />
|
||||||
{new Date(thread.latestEmail.scheduledSendAt || '').toLocaleString()}
|
{thread.latestEmail.scheduledSendAt ? formatDateTime(thread.latestEmail.scheduledSendAt, timeFormat) : ''}
|
||||||
</span>
|
</span>
|
||||||
{thread.latestEmail.scheduledUndoStatus === 'pending' && (
|
|
||||||
<>
|
|
||||||
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={() => onCancelScheduled?.(thread.latestEmail)}>
|
|
||||||
<XCircle className="w-3.5 h-3.5 mr-1" />
|
|
||||||
{t('cancel_scheduled_send')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 px-2"
|
|
||||||
onClick={() => {
|
|
||||||
const delayedUntil = promptForRescheduleDelayedUntil();
|
|
||||||
if (delayedUntil) onRescheduleScheduled?.(thread.latestEmail, delayedUntil);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CalendarClock className="w-3.5 h-3.5 mr-1" />
|
|
||||||
{t('reschedule_send')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={() => onCancelScheduledForEdit?.(thread.latestEmail)}>
|
|
||||||
<Edit3 className="w-3.5 h-3.5 mr-1" />
|
|
||||||
{thread.latestEmail.isSmimeScheduled ? t('cancel_and_compose_again') : t('cancel_and_edit')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -590,12 +529,9 @@ export function EmailList({
|
|||||||
onMarkAsSpam={() => onMarkAsSpam?.(contextMenu.data!)}
|
onMarkAsSpam={() => onMarkAsSpam?.(contextMenu.data!)}
|
||||||
onUndoSpam={() => onUndoSpam?.(contextMenu.data!)}
|
onUndoSpam={() => onUndoSpam?.(contextMenu.data!)}
|
||||||
onEditDraft={() => onEditDraft?.(contextMenu.data!)}
|
onEditDraft={() => onEditDraft?.(contextMenu.data!)}
|
||||||
onCancelScheduled={() => onCancelScheduled?.(contextMenu.data!)}
|
onCancelScheduled={isScheduledView ? undefined : () => onCancelScheduled?.(contextMenu.data!)}
|
||||||
onCancelScheduledForEdit={() => onCancelScheduledForEdit?.(contextMenu.data!)}
|
onCancelScheduledForEdit={isScheduledView ? undefined : () => onCancelScheduledForEdit?.(contextMenu.data!)}
|
||||||
onRescheduleScheduled={() => {
|
onRescheduleScheduled={undefined}
|
||||||
const delayedUntil = promptForRescheduleDelayedUntil();
|
|
||||||
if (delayedUntil) onRescheduleScheduled?.(contextMenu.data!, delayedUntil);
|
|
||||||
}}
|
|
||||||
onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)}
|
onBatchMarkAsRead={(read) => client && batchMarkAsRead(client, read)}
|
||||||
onBatchDelete={() => client && batchDelete(client)}
|
onBatchDelete={() => client && batchDelete(client)}
|
||||||
onBatchArchive={async () => {
|
onBatchArchive={async () => {
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ export const KEYBOARD_SHORTCUTS = {
|
|||||||
{ key: "x", description: "shortcuts.threads.expand_collapse" },
|
{ key: "x", description: "shortcuts.threads.expand_collapse" },
|
||||||
],
|
],
|
||||||
composer: [
|
composer: [
|
||||||
|
{ key: "Ctrl + Enter", description: "shortcuts.composer.send" },
|
||||||
|
{ key: "Ctrl + Shift + Enter", description: "shortcuts.composer.schedule_send" },
|
||||||
{ key: "t", description: "shortcuts.composer.template_picker" },
|
{ key: "t", description: "shortcuts.composer.template_picker" },
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Rozbalit/sbalit vlákno"
|
"expand_collapse": "Rozbalit/sbalit vlákno"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Odeslat e-mail",
|
||||||
|
"schedule_send": "Naplánovat odeslání",
|
||||||
"template_picker": "Otevřít výběr šablon"
|
"template_picker": "Otevřít výběr šablon"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1811,6 +1811,8 @@
|
|||||||
"expand_collapse": "Udvid/skjul tråd"
|
"expand_collapse": "Udvid/skjul tråd"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Send e-mail",
|
||||||
|
"schedule_send": "Planlæg afsendelse",
|
||||||
"template_picker": "Åbn skabelonvælger"
|
"template_picker": "Åbn skabelonvælger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Unterhaltung erweitern/einklappen"
|
"expand_collapse": "Unterhaltung erweitern/einklappen"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "E-Mail senden",
|
||||||
|
"schedule_send": "Senden planen",
|
||||||
"template_picker": "Vorlagenauswahl öffnen"
|
"template_picker": "Vorlagenauswahl öffnen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1858,6 +1858,8 @@
|
|||||||
"expand_collapse": "Expand/collapse thread"
|
"expand_collapse": "Expand/collapse thread"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Send email",
|
||||||
|
"schedule_send": "Schedule send",
|
||||||
"template_picker": "Open template picker"
|
"template_picker": "Open template picker"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Expandir/contraer conversación"
|
"expand_collapse": "Expandir/contraer conversación"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Enviar correo",
|
||||||
|
"schedule_send": "Programar envío",
|
||||||
"template_picker": "Abrir selector de plantillas"
|
"template_picker": "Abrir selector de plantillas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Développer/réduire la conversation"
|
"expand_collapse": "Développer/réduire la conversation"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Envoyer l'e-mail",
|
||||||
|
"schedule_send": "Planifier l'envoi",
|
||||||
"template_picker": "Ouvrir le sélecteur de modèles"
|
"template_picker": "Ouvrir le sélecteur de modèles"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Espandi/comprimi conversazione"
|
"expand_collapse": "Espandi/comprimi conversazione"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Invia email",
|
||||||
|
"schedule_send": "Programma invio",
|
||||||
"template_picker": "Apri selettore modelli"
|
"template_picker": "Apri selettore modelli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "スレッドの展開/折りたたみ"
|
"expand_collapse": "スレッドの展開/折りたたみ"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "メールを送信",
|
||||||
|
"schedule_send": "送信を予約",
|
||||||
"template_picker": "テンプレートピッカーを開く"
|
"template_picker": "テンプレートピッカーを開く"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "대화 펼치기/접기"
|
"expand_collapse": "대화 펼치기/접기"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "이메일 보내기",
|
||||||
|
"schedule_send": "보내기 예약",
|
||||||
"template_picker": "템플릿 선택기 열기"
|
"template_picker": "템플릿 선택기 열기"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Izvērst/sairt sarunu"
|
"expand_collapse": "Izvērst/sairt sarunu"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Nosūtīt e-pastu",
|
||||||
|
"schedule_send": "Ieplānot nosūtīšanu",
|
||||||
"template_picker": "Atvērt veidņu izvēli"
|
"template_picker": "Atvērt veidņu izvēli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Gesprek uitklappen/inklappen"
|
"expand_collapse": "Gesprek uitklappen/inklappen"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "E-mail verzenden",
|
||||||
|
"schedule_send": "Verzenden plannen",
|
||||||
"template_picker": "Sjabloonkiezer openen"
|
"template_picker": "Sjabloonkiezer openen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Rozwiń/zwiń wątek"
|
"expand_collapse": "Rozwiń/zwiń wątek"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Wyślij e-mail",
|
||||||
|
"schedule_send": "Zaplanuj wysyłkę",
|
||||||
"template_picker": "Otwórz wybór szablonu"
|
"template_picker": "Otwórz wybór szablonu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Expandir/recolher conversa"
|
"expand_collapse": "Expandir/recolher conversa"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Enviar e-mail",
|
||||||
|
"schedule_send": "Agendar envio",
|
||||||
"template_picker": "Abrir seletor de modelos"
|
"template_picker": "Abrir seletor de modelos"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Развернуть/свернуть цепочку"
|
"expand_collapse": "Развернуть/свернуть цепочку"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Отправить письмо",
|
||||||
|
"schedule_send": "Запланировать отправку",
|
||||||
"template_picker": "Открыть выбор шаблонов"
|
"template_picker": "Открыть выбор шаблонов"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "İleti dizisini genişlet/daralt"
|
"expand_collapse": "İleti dizisini genişlet/daralt"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "E-posta gönder",
|
||||||
|
"schedule_send": "Göndermeyi planla",
|
||||||
"template_picker": "Şablon seçiciyi aç"
|
"template_picker": "Şablon seçiciyi aç"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "Розгорнути/згорнути ланцюжок"
|
"expand_collapse": "Розгорнути/згорнути ланцюжок"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "Надіслати лист",
|
||||||
|
"schedule_send": "Запланувати надсилання",
|
||||||
"template_picker": "Відкрити засіб вибору шаблону"
|
"template_picker": "Відкрити засіб вибору шаблону"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1854,6 +1854,8 @@
|
|||||||
"expand_collapse": "展开/折叠会话"
|
"expand_collapse": "展开/折叠会话"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
|
"send": "发送邮件",
|
||||||
|
"schedule_send": "定时发送",
|
||||||
"template_picker": "打开模板选择器"
|
"template_picker": "打开模板选择器"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2213,7 +2213,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
const result = await client.rescheduleEmailSubmission(submissionId, emailId, identityId, delayedUntil);
|
const result = await client.rescheduleEmailSubmission(submissionId, emailId, identityId, delayedUntil);
|
||||||
const pendingUndoSend = get().pendingUndoSend;
|
const pendingUndoSend = get().pendingUndoSend;
|
||||||
if (pendingUndoSend?.submissionId === submissionId) {
|
if (pendingUndoSend?.submissionId === submissionId) {
|
||||||
set({ pendingUndoSend: { ...pendingUndoSend, sendAt: result.sendAt || delayedUntil } });
|
set({ pendingUndoSend: { ...pendingUndoSend, submissionId: result.emailSubmissionId || submissionId, sendAt: result.sendAt || delayedUntil } });
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user