diff --git a/src/frontend/public/locales/common/en-US.json b/src/frontend/public/locales/common/en-US.json index 1327bc87..19a57672 100755 --- a/src/frontend/public/locales/common/en-US.json +++ b/src/frontend/public/locales/common/en-US.json @@ -634,6 +634,7 @@ "Send and archive": "Send and archive", "Send and receive your messages in an instant.": "Send and receive your messages in an instant.", "Send Feedback": "Send Feedback", + "Sending is taking longer than expected. You can track your message in the Outbox.": "Sending is taking longer than expected. You can track your message in the Outbox.", "Sending message...": "Sending message...", "Sent": "Sent", "Sent by {{name}}": "Sent by {{name}}", @@ -709,7 +710,6 @@ "The forced signature will be the only one usable for new messages.": "The forced signature will be the only one usable for new messages.", "The mailbox \"{{mailbox}}\" currently has read-only access on this thread. To assign {{user}} to it, edit permissions must be granted to this mailbox.": "The mailbox \"{{mailbox}}\" currently has read-only access on this thread. To assign {{user}} to it, edit permissions must be granted to this mailbox.", "The message could not be sent.": "The message could not be sent.", - "The message could not be sent. Please try again later.": "The message could not be sent. Please try again later.", "The organizer marked this event as tentative.": "The organizer marked this event as tentative.", "The personal mailbox {{mailboxAddress}} has been created successfully.": "The personal mailbox <1>{{mailboxAddress}} has been created successfully.", "The PST archive is unreadable: the file is corrupt or its internal structure is incomplete. Retrying will not help — please try to re-generate the archive.": "The PST archive is unreadable: the file is corrupt or its internal structure is incomplete. Retrying will not help — please try to re-generate the archive.", diff --git a/src/frontend/public/locales/common/fr-FR.json b/src/frontend/public/locales/common/fr-FR.json index 32427e66..3640a043 100755 --- a/src/frontend/public/locales/common/fr-FR.json +++ b/src/frontend/public/locales/common/fr-FR.json @@ -710,6 +710,7 @@ "Send and archive": "Envoyer et archiver", "Send and receive your messages in an instant.": "Envoyez et recevez vos messages en un instant.", "Send Feedback": "Envoyer le message", + "Sending is taking longer than expected. You can track your message in the Outbox.": "L'envoi prend plus de temps que prévu. Vous pouvez suivre votre message dans la boîte d'envoi.", "Sending message...": "Envoi du message en cours...", "Sent": "Envoyés", "Sent by {{name}}": "Envoyé par {{name}}", @@ -788,7 +789,6 @@ "The forced signature will be the only one usable for new messages.": "La signature forcée sera la seule utilisable pour les nouveaux messages.", "The mailbox \"{{mailbox}}\" currently has read-only access on this thread. To assign {{user}} to it, edit permissions must be granted to this mailbox.": "La boîte « {{mailbox}} » n'a actuellement que les droits en lecture sur cette conversation. Pour y assigner {{user}}, les droits en édition doivent être accordés à cette boîte.", "The message could not be sent.": "Le message n'a pas pu être envoyé.", - "The message could not be sent. Please try again later.": "Le message n'a pas pu être envoyé. Veuillez réessayer plus tard.", "The organizer marked this event as tentative.": "L'organisateur a marqué cet événement comme provisoire.", "The personal mailbox {{mailboxAddress}} has been created successfully.": "L'adresse personnelle {{mailboxAddress}} a été créée avec succès.", "The PST archive is unreadable: the file is corrupt or its internal structure is incomplete. Retrying will not help — please try to re-generate the archive.": "L'archive PST est illisible : le fichier est corrompu ou sa structure interne est incomplète. Réessayer ne servira à rien — veuillez essayer de régénérer l'archive.", diff --git a/src/frontend/src/features/forms/components/message-form/index.tsx b/src/frontend/src/features/forms/components/message-form/index.tsx index 1ab639f9..f1570008 100644 --- a/src/frontend/src/features/forms/components/message-form/index.tsx +++ b/src/frontend/src/features/forms/components/message-form/index.tsx @@ -106,7 +106,7 @@ export const MessageForm = ({ const autoSaveTimerRef = useRef(null); const saveDraftRef = useRef<() => void>(() => {}); const quoteType: QuoteType | undefined = mode !== "new" ? (mode === "forward" ? "forward" : "reply") : undefined; - const { selectedMailbox, selectedThread, mailboxes, removeMessages, invalidateMailbox, invalidateThreadsStats, unselectThread, unpinThreads, pinThreads } = useMailboxContext(); + const { selectedMailbox, selectedThread, mailboxes, removeMessages, patchMessages, invalidateMailbox, invalidateThreadsStats, unselectThread, unpinThreads, pinThreads } = useMailboxContext(); const hideSubjectField = Boolean(draftMessage?.parent_id ?? parentMessage); // For replies/forwards, only allow sending from a mailbox that has access to the thread. const availableMailboxes = useMemo(() => { @@ -287,8 +287,20 @@ export const MessageForm = ({ form.clearErrors(); toast.dismiss(DRAFT_TOAST_ID); }, - onSuccess: async (response) => { + onSuccess: async (response, variables) => { const data = (response as sendCreateResponse200).data; + // The backend un-drafts the message synchronously before queuing + // the SMTP task. Reflect that optimistically so the thread renders + // it as a sending message right away instead of keeping the draft + // form open until the background refetch lands. + const sentThreadId = draft?.thread_id ?? selectedThread?.id; + if (sentThreadId) { + patchMessages(sentThreadId, (message) => + message.id === variables.data.messageId + ? { ...message, is_draft: false } + : message + ); + } addQueuedMessage(data.task_id); onSuccess?.(); } diff --git a/src/frontend/src/features/layouts/components/thread-view/components/message-reply-form/index.tsx b/src/frontend/src/features/layouts/components/thread-view/components/message-reply-form/index.tsx index 8d14b39a..67095cba 100644 --- a/src/frontend/src/features/layouts/components/thread-view/components/message-reply-form/index.tsx +++ b/src/frontend/src/features/layouts/components/thread-view/components/message-reply-form/index.tsx @@ -17,10 +17,13 @@ const MessageReplyForm = ({ handleClose, message, mode }: MessageReplyFormProps) draftMessage={message.is_draft ? message : undefined} parentMessage={message.is_draft ? undefined : message} mode={mode} - onSuccess={async () => { - // Force refetch the messages query to avoid showing the draft message in the thread view - await queryClient.refetchQueries({ queryKey: ["messages", message.thread_id] }); + onSuccess={() => { + // Close right away: MessageForm has optimistically un-drafted + // the message, so the thread already shows it as sending. handleClose(); + // Reconcile with the server state (delivery status, etc.) in + // the background without blocking the form close. + void queryClient.refetchQueries({ queryKey: ["messages", message.thread_id] }); }} onClose={handleClose} /> diff --git a/src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss b/src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss index eb670ecf..e10f2e09 100644 --- a/src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss +++ b/src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss @@ -224,8 +224,7 @@ display: flex; justify-content: center; align-items: center; - height: 100%; - margin-block: var(--c--globals--spacings--base); + margin-bottom: var(--c--globals--spacings--base); } .thread-message__reply-form { diff --git a/src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx b/src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx index f8504c0d..f29afbe9 100644 --- a/src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx +++ b/src/frontend/src/features/layouts/components/thread-view/components/thread-message/index.tsx @@ -321,7 +321,12 @@ export const ThreadMessage = forwardRef( icon={} type="warning" fullWidth - actions={canUpdateDeliveryStatus ? [ + // Only offer cancellation when there are recipients the + // backend can actually transition (retry). A message still + // being sent has pending recipients (delivery_status null), + // which the banner reports but which cannot be cancelled — + // showing the button there would POST an empty payload. + actions={canUpdateDeliveryStatus && retryRecipients.length > 0 ? [ { label: t('Cancel those sendings'), onClick: handleCancelRetries, diff --git a/src/frontend/src/features/providers/sent-box/queued-message.tsx b/src/frontend/src/features/providers/sent-box/queued-message.tsx index 070daea7..e2be18c3 100644 --- a/src/frontend/src/features/providers/sent-box/queued-message.tsx +++ b/src/frontend/src/features/providers/sent-box/queued-message.tsx @@ -95,8 +95,17 @@ export const QueueMessage = ({ taskId, onSettled }: QueueMessageProps) => { useEffect(() => { if (hasTimedOut) { + // The send didn't fail: the backend already un-drafted the message and + // the SMTP task is still running. Reassure the user and point them to + // the Outbox rather than showing a misleading error. onSettled refreshes + // the stats so the Outbox folder reflects the pending message. toast.update(toastId, { - render: {t('The message could not be sent. Please try again later.')}, + render: ( + + schedule_send + {t('Sending is taking longer than expected. You can track your message in the Outbox.')} + + ), autoClose: QUEUED_MESSAGE_CLOSE_DELAY * 2, }); onSettled?.();