From d1b2206aa0acb0a597d5f3c260ebc4eb3142a8db Mon Sep 17 00:00:00 2001 From: Lucas Gaitzsch Date: Tue, 26 May 2026 18:43:01 +0200 Subject: [PATCH] fixes --- components/pro/pro-compose-tab-body.tsx | 16 +++++++++-- lib/demo/demo-client.ts | 5 ++-- lib/jmap/client-interface.ts | 2 +- lib/jmap/client.ts | 9 +++--- stores/email-store.ts | 38 +++++++++++++++++++------ 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/components/pro/pro-compose-tab-body.tsx b/components/pro/pro-compose-tab-body.tsx index 35686e5d..ecbca751 100644 --- a/components/pro/pro-compose-tab-body.tsx +++ b/components/pro/pro-compose-tab-body.tsx @@ -26,7 +26,10 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) { const client = useAuthStore((s) => s.client); const sendEmail = useEmailStore((s) => s.sendEmail); 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 isScheduledView = useEmailStore((s) => s.isScheduledView); const closeTab = useProTabStore((s) => s.closeTab); const updateTabTitle = useProTabStore((s) => s.updateTabTitle); const updateComposeDraft = useProTabStore((s) => s.updateComposeDraft); @@ -36,6 +39,14 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) { const tabIdRef = useRef(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['onSend']>>[0]) => { if (!client) return; try { @@ -59,7 +70,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) { ); if (result.scheduled) { - closeTab(tabIdRef.current); + await handleScheduledSendCreated(); return; } @@ -87,7 +98,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) { console.error('Failed to send email:', error); 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(() => { closeTab(tabIdRef.current); @@ -131,6 +142,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) { initialDraftText={data.initialDraftText} initialData={data.initialData} onSend={handleSend} + onScheduledSendCreated={handleScheduledSendCreated} onClose={handleClose} onDiscardDraft={handleDiscardDraft} onSaveState={handleSaveState} diff --git a/lib/demo/demo-client.ts b/lib/demo/demo-client.ts index 6f68b739..c8569bef 100644 --- a/lib/demo/demo-client.ts +++ b/lib/demo/demo-client.ts @@ -959,7 +959,7 @@ export class DemoJMAPClient implements IJMAPClient { 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()) .filter(s => s.undoStatus === 'pending') .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, } satisfies ScheduledEmail; }).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 { diff --git a/lib/jmap/client-interface.ts b/lib/jmap/client-interface.ts index ccac56ec..6ab51f30 100644 --- a/lib/jmap/client-interface.ts +++ b/lib/jmap/client-interface.ts @@ -153,7 +153,7 @@ export interface IJMAPClient { ): Promise; sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise; - 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; rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise; restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise; diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index d06b7f41..e7d17868 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -5617,9 +5617,9 @@ export class JMAPClient implements IJMAPClient { : { 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()) { - return { emails: [], hasMore: false, total: 0 }; + return { emails: [], hasMore: false, total: 0, nextPosition: position }; } 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()); const total = submissions.length; const pageSubmissions = submissions.slice(position, position + limit); + const nextPosition = position + pageSubmissions.length; if (pageSubmissions.length === 0) { - return { emails: [], hasMore: false, total }; + return { emails: [], hasMore: false, total, nextPosition }; } const emailResponse = await this.request([ @@ -5704,7 +5705,7 @@ export class JMAPClient implements IJMAPClient { .filter((email): email is ScheduledEmail => email !== null) .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 { diff --git a/stores/email-store.ts b/stores/email-store.ts index c0a28280..7c7ce7c0 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -84,6 +84,7 @@ interface EmailStore { scheduledSubmissionByEmailId: Map; scheduledTotal: number; scheduledHasMore: boolean; + scheduledNextPosition: number; isLoadingScheduled: boolean; isScheduledView: boolean; pendingUndoSend: PendingUndoSend | null; @@ -456,6 +457,7 @@ export const useEmailStore = create((set, get) => ({ scheduledSubmissionByEmailId: new Map(), scheduledTotal: 0, scheduledHasMore: false, + scheduledNextPosition: 0, isLoadingScheduled: false, isScheduledView: false, pendingUndoSend: null, @@ -2530,6 +2532,7 @@ export const useEmailStore = create((set, get) => ({ scheduledSubmissionByEmailId, scheduledTotal: result.total, scheduledHasMore: result.hasMore, + scheduledNextPosition: result.nextPosition, isLoadingScheduled: false, pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, result.emails) ? null : pendingUndoSend, }); @@ -2542,18 +2545,19 @@ export const useEmailStore = create((set, get) => ({ scheduledSubmissionByEmailId: new Map(), scheduledTotal: 0, scheduledHasMore: false, + scheduledNextPosition: 0, isLoadingScheduled: false, }); } }, loadMoreScheduledEmails: async (client) => { - const { isLoadingScheduled, scheduledHasMore, scheduledEmails } = get(); + const { isLoadingScheduled, scheduledHasMore, scheduledEmails, scheduledNextPosition } = get(); if (isLoadingScheduled || !scheduledHasMore) return; set({ isLoadingScheduled: true, error: null }); try { 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 pendingUndoSend = get().pendingUndoSend; set({ @@ -2567,6 +2571,7 @@ export const useEmailStore = create((set, get) => ({ }])), scheduledTotal: result.total, scheduledHasMore: result.hasMore, + scheduledNextPosition: result.nextPosition, isLoadingScheduled: false, pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, merged) ? null : pendingUndoSend, }); @@ -2577,20 +2582,32 @@ export const useEmailStore = create((set, get) => ({ refreshScheduledMetadata: async (client) => { 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; set({ - scheduledEmails: get().isScheduledView ? result.emails : get().scheduledEmails, - scheduledEmailIds: new Set(result.emails.map(email => email.id)), - scheduledSubmissionByEmailId: new Map(result.emails.map(email => [email.id, { + scheduledEmails: get().isScheduledView ? allEmails : get().scheduledEmails, + scheduledEmailIds: new Set(allEmails.map(email => email.id)), + scheduledSubmissionByEmailId: new Map(allEmails.map(email => [email.id, { submissionId: email.emailSubmissionId, sendAt: email.scheduledSendAt, identityId: email.scheduledIdentityId, undoStatus: email.scheduledUndoStatus, }])), - scheduledTotal: result.total, - scheduledHasMore: result.hasMore, - pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, result.emails) ? null : pendingUndoSend, + scheduledTotal: total, + scheduledHasMore: false, + scheduledNextPosition: position, + pendingUndoSend: shouldClearPendingUndoSend(pendingUndoSend, allEmails) ? null : pendingUndoSend, }); } catch (error) { console.error('Failed to refresh scheduled metadata:', error); @@ -2616,6 +2633,9 @@ export const useEmailStore = create((set, get) => ({ const submissionId = email.emailSubmissionId; if (!submissionId) return null; await client.cancelEmailSubmission(submissionId); + if (get().pendingUndoSend?.submissionId === submissionId) { + set({ pendingUndoSend: null }); + } if (email.isSmimeScheduled) { await client.deleteEmail(email.id); set(state => ({