This commit is contained in:
Lucas Gaitzsch
2026-05-26 18:43:01 +02:00
parent 0137a1a593
commit d1b2206aa0
5 changed files with 52 additions and 18 deletions
+14 -2
View File
@@ -26,7 +26,10 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
const client = useAuthStore((s) => s.client); const client = useAuthStore((s) => s.client);
const sendEmail = useEmailStore((s) => s.sendEmail); const sendEmail = useEmailStore((s) => s.sendEmail);
const fetchEmails = useEmailStore((s) => s.fetchEmails); const fetchEmails = useEmailStore((s) => s.fetchEmails);
const fetchScheduledEmails = useEmailStore((s) => s.fetchScheduledEmails);
const refreshScheduledMetadata = useEmailStore((s) => s.refreshScheduledMetadata);
const selectedMailbox = useEmailStore((s) => s.selectedMailbox); const selectedMailbox = useEmailStore((s) => s.selectedMailbox);
const isScheduledView = useEmailStore((s) => s.isScheduledView);
const closeTab = useProTabStore((s) => s.closeTab); const closeTab = useProTabStore((s) => s.closeTab);
const updateTabTitle = useProTabStore((s) => s.updateTabTitle); const updateTabTitle = useProTabStore((s) => s.updateTabTitle);
const updateComposeDraft = useProTabStore((s) => s.updateComposeDraft); const updateComposeDraft = useProTabStore((s) => s.updateComposeDraft);
@@ -36,6 +39,14 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
const tabIdRef = useRef(tabId); const tabIdRef = useRef(tabId);
tabIdRef.current = tabId; tabIdRef.current = tabId;
const handleScheduledSendCreated = useCallback(async () => {
if (client) {
await refreshScheduledMetadata(client);
if (isScheduledView) await fetchScheduledEmails(client);
}
closeTab(tabIdRef.current);
}, [client, refreshScheduledMetadata, isScheduledView, fetchScheduledEmails, closeTab]);
const handleSend = useCallback(async (sendData: Parameters<NonNullable<React.ComponentProps<typeof EmailComposer>['onSend']>>[0]) => { const handleSend = useCallback(async (sendData: Parameters<NonNullable<React.ComponentProps<typeof EmailComposer>['onSend']>>[0]) => {
if (!client) return; if (!client) return;
try { try {
@@ -59,7 +70,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
); );
if (result.scheduled) { if (result.scheduled) {
closeTab(tabIdRef.current); await handleScheduledSendCreated();
return; return;
} }
@@ -87,7 +98,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
console.error('Failed to send email:', error); console.error('Failed to send email:', error);
toast.error(t('notifications.error_sending')); toast.error(t('notifications.error_sending'));
} }
}, [client, sendEmail, fetchEmails, selectedMailbox, closeTab, data.sourceEmailId, data.mode, t]); }, [client, sendEmail, fetchEmails, selectedMailbox, closeTab, data.sourceEmailId, data.mode, t, handleScheduledSendCreated]);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
closeTab(tabIdRef.current); closeTab(tabIdRef.current);
@@ -131,6 +142,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
initialDraftText={data.initialDraftText} initialDraftText={data.initialDraftText}
initialData={data.initialData} initialData={data.initialData}
onSend={handleSend} onSend={handleSend}
onScheduledSendCreated={handleScheduledSendCreated}
onClose={handleClose} onClose={handleClose}
onDiscardDraft={handleDiscardDraft} onDiscardDraft={handleDiscardDraft}
onSaveState={handleSaveState} onSaveState={handleSaveState}
+3 -2
View File
@@ -959,7 +959,7 @@ export class DemoJMAPClient implements IJMAPClient {
return delayedUntil ? { scheduled: true, emailId, emailSubmissionId, sendAt: delayedUntil, isSmime: true } : { scheduled: false, emailId, isSmime: true }; return delayedUntil ? { scheduled: true, emailId, emailSubmissionId, sendAt: delayedUntil, isSmime: true } : { scheduled: false, emailId, isSmime: true };
} }
async getScheduledEmails(limit = 50, position = 0): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number }> { async getScheduledEmails(limit = 50, position = 0): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; nextPosition: number }> {
const pending = Array.from(this.scheduledSubmissions.values()) const pending = Array.from(this.scheduledSubmissions.values())
.filter(s => s.undoStatus === 'pending') .filter(s => s.undoStatus === 'pending')
.sort((a, b) => new Date(a.sendAt).getTime() - new Date(b.sendAt).getTime()); .sort((a, b) => new Date(a.sendAt).getTime() - new Date(b.sendAt).getTime());
@@ -977,7 +977,8 @@ export class DemoJMAPClient implements IJMAPClient {
isSmimeScheduled: submission.isSmime, isSmimeScheduled: submission.isSmime,
} satisfies ScheduledEmail; } satisfies ScheduledEmail;
}).filter((email): email is ScheduledEmail => email !== null); }).filter((email): email is ScheduledEmail => email !== null);
return { emails, hasMore: position + emails.length < pending.length, total: pending.length }; const nextPosition = position + page.length;
return { emails, hasMore: nextPosition < pending.length, total: pending.length, nextPosition };
} }
async cancelEmailSubmission(submissionId: string): Promise<void> { async cancelEmailSubmission(submissionId: string): Promise<void> {
+1 -1
View File
@@ -153,7 +153,7 @@ export interface IJMAPClient {
): Promise<SendEmailResult>; ): Promise<SendEmailResult>;
sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise<SendEmailResult>; sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise<SendEmailResult>;
getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number }>; getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; nextPosition: number }>;
cancelEmailSubmission(submissionId: string): Promise<void>; cancelEmailSubmission(submissionId: string): Promise<void>;
rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise<SendEmailResult>; rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise<SendEmailResult>;
restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise<void>; restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise<void>;
+5 -4
View File
@@ -5617,9 +5617,9 @@ export class JMAPClient implements IJMAPClient {
: { scheduled: false, emailId, emailSubmissionId, isSmime: true }; : { scheduled: false, emailId, emailSubmissionId, isSmime: true };
} }
async getScheduledEmails(limit = 50, position = 0): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number }> { async getScheduledEmails(limit = 50, position = 0): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; nextPosition: number }> {
if (!this.hasDelayedSend()) { if (!this.hasDelayedSend()) {
return { emails: [], hasMore: false, total: 0 }; return { emails: [], hasMore: false, total: 0, nextPosition: position };
} }
const now = Date.now(); const now = Date.now();
@@ -5663,9 +5663,10 @@ export class JMAPClient implements IJMAPClient {
submissions.sort((a, b) => new Date(a.sendAt || '').getTime() - new Date(b.sendAt || '').getTime()); submissions.sort((a, b) => new Date(a.sendAt || '').getTime() - new Date(b.sendAt || '').getTime());
const total = submissions.length; const total = submissions.length;
const pageSubmissions = submissions.slice(position, position + limit); const pageSubmissions = submissions.slice(position, position + limit);
const nextPosition = position + pageSubmissions.length;
if (pageSubmissions.length === 0) { if (pageSubmissions.length === 0) {
return { emails: [], hasMore: false, total }; return { emails: [], hasMore: false, total, nextPosition };
} }
const emailResponse = await this.request([ const emailResponse = await this.request([
@@ -5704,7 +5705,7 @@ export class JMAPClient implements IJMAPClient {
.filter((email): email is ScheduledEmail => email !== null) .filter((email): email is ScheduledEmail => email !== null)
.sort((a, b) => new Date(a.scheduledSendAt).getTime() - new Date(b.scheduledSendAt).getTime()); .sort((a, b) => new Date(a.scheduledSendAt).getTime() - new Date(b.scheduledSendAt).getTime());
return { emails, hasMore: position + emails.length < total, total }; return { emails, hasMore: nextPosition < total, total, nextPosition };
} }
async cancelEmailSubmission(submissionId: string): Promise<void> { async cancelEmailSubmission(submissionId: string): Promise<void> {
+29 -9
View File
@@ -84,6 +84,7 @@ interface EmailStore {
scheduledSubmissionByEmailId: Map<string, ScheduledSubmissionMetadata>; scheduledSubmissionByEmailId: Map<string, ScheduledSubmissionMetadata>;
scheduledTotal: number; scheduledTotal: number;
scheduledHasMore: boolean; scheduledHasMore: boolean;
scheduledNextPosition: number;
isLoadingScheduled: boolean; isLoadingScheduled: boolean;
isScheduledView: boolean; isScheduledView: boolean;
pendingUndoSend: PendingUndoSend | null; pendingUndoSend: PendingUndoSend | null;
@@ -456,6 +457,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
scheduledSubmissionByEmailId: new Map(), scheduledSubmissionByEmailId: new Map(),
scheduledTotal: 0, scheduledTotal: 0,
scheduledHasMore: false, scheduledHasMore: false,
scheduledNextPosition: 0,
isLoadingScheduled: false, isLoadingScheduled: false,
isScheduledView: false, isScheduledView: false,
pendingUndoSend: null, pendingUndoSend: null,
@@ -2530,6 +2532,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
scheduledSubmissionByEmailId, scheduledSubmissionByEmailId,
scheduledTotal: result.total, scheduledTotal: result.total,
scheduledHasMore: result.hasMore, scheduledHasMore: result.hasMore,
scheduledNextPosition: result.nextPosition,
isLoadingScheduled: false, isLoadingScheduled: false,
pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, result.emails) ? null : pendingUndoSend, pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, result.emails) ? null : pendingUndoSend,
}); });
@@ -2542,18 +2545,19 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
scheduledSubmissionByEmailId: new Map(), scheduledSubmissionByEmailId: new Map(),
scheduledTotal: 0, scheduledTotal: 0,
scheduledHasMore: false, scheduledHasMore: false,
scheduledNextPosition: 0,
isLoadingScheduled: false, isLoadingScheduled: false,
}); });
} }
}, },
loadMoreScheduledEmails: async (client) => { loadMoreScheduledEmails: async (client) => {
const { isLoadingScheduled, scheduledHasMore, scheduledEmails } = get(); const { isLoadingScheduled, scheduledHasMore, scheduledEmails, scheduledNextPosition } = get();
if (isLoadingScheduled || !scheduledHasMore) return; if (isLoadingScheduled || !scheduledHasMore) return;
set({ isLoadingScheduled: true, error: null }); set({ isLoadingScheduled: true, error: null });
try { try {
const emailsPerPage = useSettingsStore.getState().emailsPerPage; const emailsPerPage = useSettingsStore.getState().emailsPerPage;
const result = await client.getScheduledEmails(emailsPerPage, scheduledEmails.length); const result = await client.getScheduledEmails(emailsPerPage, scheduledNextPosition);
const merged = [...scheduledEmails, ...result.emails.filter(email => !scheduledEmails.some(existing => existing.id === email.id))]; const merged = [...scheduledEmails, ...result.emails.filter(email => !scheduledEmails.some(existing => existing.id === email.id))];
const pendingUndoSend = get().pendingUndoSend; const pendingUndoSend = get().pendingUndoSend;
set({ set({
@@ -2567,6 +2571,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
}])), }])),
scheduledTotal: result.total, scheduledTotal: result.total,
scheduledHasMore: result.hasMore, scheduledHasMore: result.hasMore,
scheduledNextPosition: result.nextPosition,
isLoadingScheduled: false, isLoadingScheduled: false,
pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, merged) ? null : pendingUndoSend, pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, merged) ? null : pendingUndoSend,
}); });
@@ -2577,20 +2582,32 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
refreshScheduledMetadata: async (client) => { refreshScheduledMetadata: async (client) => {
try { try {
const result = await client.getScheduledEmails(useSettingsStore.getState().emailsPerPage, 0); const emailsPerPage = useSettingsStore.getState().emailsPerPage;
const allEmails: ScheduledEmail[] = [];
let position = 0;
let hasMore = true;
let total = 0;
while (hasMore) {
const page = await client.getScheduledEmails(emailsPerPage, position);
allEmails.push(...page.emails.filter(email => !allEmails.some(existing => existing.id === email.id)));
total = page.total;
hasMore = page.hasMore && page.nextPosition > position;
position = page.nextPosition;
}
const pendingUndoSend = get().pendingUndoSend; const pendingUndoSend = get().pendingUndoSend;
set({ set({
scheduledEmails: get().isScheduledView ? result.emails : get().scheduledEmails, scheduledEmails: get().isScheduledView ? allEmails : get().scheduledEmails,
scheduledEmailIds: new Set(result.emails.map(email => email.id)), scheduledEmailIds: new Set(allEmails.map(email => email.id)),
scheduledSubmissionByEmailId: new Map(result.emails.map(email => [email.id, { scheduledSubmissionByEmailId: new Map(allEmails.map(email => [email.id, {
submissionId: email.emailSubmissionId, submissionId: email.emailSubmissionId,
sendAt: email.scheduledSendAt, sendAt: email.scheduledSendAt,
identityId: email.scheduledIdentityId, identityId: email.scheduledIdentityId,
undoStatus: email.scheduledUndoStatus, undoStatus: email.scheduledUndoStatus,
}])), }])),
scheduledTotal: result.total, scheduledTotal: total,
scheduledHasMore: result.hasMore, scheduledHasMore: false,
pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, result.emails) ? null : pendingUndoSend, scheduledNextPosition: position,
pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, allEmails) ? null : pendingUndoSend,
}); });
} catch (error) { } catch (error) {
console.error('Failed to refresh scheduled metadata:', error); console.error('Failed to refresh scheduled metadata:', error);
@@ -2616,6 +2633,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
const submissionId = email.emailSubmissionId; const submissionId = email.emailSubmissionId;
if (!submissionId) return null; if (!submissionId) return null;
await client.cancelEmailSubmission(submissionId); await client.cancelEmailSubmission(submissionId);
if (get().pendingUndoSend?.submissionId === submissionId) {
set({ pendingUndoSend: null });
}
if (email.isSmimeScheduled) { if (email.isSmimeScheduled) {
await client.deleteEmail(email.id); await client.deleteEmail(email.id);
set(state => ({ set(state => ({