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}}1> 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?.();