mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
🚸(frontend) improve sending experience
First when the sending reach the timeout, instead to display a toast error with a message that lets believe the message cannot be sent, we display a warning message mentionning that sending takes more time than expected. Then, once a message is sent, we optimistically update thread cache to hide immediately the sent draft and show instead the message in the thread view.
This commit is contained in:
@@ -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 <strong>{{mailboxAddress}}</strong> 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.",
|
||||
|
||||
@@ -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 <strong>{{mailboxAddress}}</strong> has been created successfully.": "L'adresse personnelle <strong>{{mailboxAddress}}</strong> 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.",
|
||||
|
||||
@@ -106,7 +106,7 @@ export const MessageForm = ({
|
||||
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(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?.();
|
||||
}
|
||||
|
||||
+6
-3
@@ -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}
|
||||
/>
|
||||
|
||||
+1
-2
@@ -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 {
|
||||
|
||||
+6
-1
@@ -321,7 +321,12 @@ export const ThreadMessage = forwardRef<HTMLSpanElement, ThreadMessageProps>(
|
||||
icon={<Icon name="update" type={IconType.OUTLINED} />}
|
||||
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,
|
||||
|
||||
@@ -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: <ToasterItem type="error"><span>{t('The message could not be sent. Please try again later.')}</span></ToasterItem>,
|
||||
render: (
|
||||
<ToasterItem type="warning">
|
||||
<span className="material-icons">schedule_send</span>
|
||||
<span>{t('Sending is taking longer than expected. You can track your message in the Outbox.')}</span>
|
||||
</ToasterItem>
|
||||
),
|
||||
autoClose: QUEUED_MESSAGE_CLOSE_DELAY * 2,
|
||||
});
|
||||
onSettled?.();
|
||||
|
||||
Reference in New Issue
Block a user