mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
fix: show Sending… and Sent in pop-out and quick compose windows straight after Send
The pop-out test for a message scheduled for later no longer expects no sending view at all: the window now says "Sending…" until it closes, so it checks that no Undo is offered there.
This commit is contained in:
@@ -614,9 +614,38 @@ describe('a send held for undo', () => {
|
||||
expect(sent?.notice.releasesAt).toBe(sendAt);
|
||||
});
|
||||
|
||||
// compose-24: the window sat with a greyed Send until the server answered.
|
||||
it('says "Sending…" as soon as Send goes through, before the server answers', async () => {
|
||||
let answer!: () => void;
|
||||
useEmailStore.setState({ sendEmail: vi.fn(() => new Promise((resolve) => { answer = () => resolve({}); })) } as never);
|
||||
sendAt = new Date(Date.now() + 5000).toISOString();
|
||||
window.history.replaceState(null, '', '/en/popout/compose?draft=d0&account=alice');
|
||||
render(<PopoutComposeApp />);
|
||||
await waitFor(() => expect(screen.getByTestId('composer').textContent).toBe('Before sending'));
|
||||
let done: Promise<unknown> = Promise.resolve();
|
||||
act(() => { done = (composer as unknown as Sending).onSend(DATA); });
|
||||
expect(screen.getByTestId('popout-sending').textContent).toMatch(/sending$/);
|
||||
await act(async () => { answer(); await done; });
|
||||
});
|
||||
|
||||
it('shows the composer again with its text when the send fails', async () => {
|
||||
useEmailStore.setState({ sendEmail: vi.fn(async () => { throw new Error('refused'); }) } as never);
|
||||
sendAt = new Date(Date.now() + 5000).toISOString();
|
||||
window.history.replaceState(null, '', '/en/popout/compose?draft=d0&account=alice');
|
||||
render(<PopoutComposeApp />);
|
||||
await waitFor(() => expect(screen.getByTestId('composer').textContent).toBe('Before sending'));
|
||||
await act(async () => {
|
||||
await expect((composer as unknown as Sending).onSend(DATA)).rejects.toThrow('refused');
|
||||
});
|
||||
expect(screen.queryByTestId('popout-sending')).toBeNull();
|
||||
expect(screen.getByTestId('composer').closest('.hidden')).toBeNull();
|
||||
});
|
||||
|
||||
it('closes once the undo time is over', async () => {
|
||||
await sendFromWindow(1.2);
|
||||
expect(closeSelf).not.toHaveBeenCalled();
|
||||
// As the main window's toast: "Sent" once Undo is over (compose-24).
|
||||
await waitFor(() => expect(screen.getByTestId('popout-sending').textContent).toMatch(/(^|\.)sent$/), { timeout: 3000 });
|
||||
await waitFor(() => expect(closeSelf).toHaveBeenCalled(), { timeout: 4000 });
|
||||
});
|
||||
|
||||
@@ -631,7 +660,8 @@ describe('a send held for undo', () => {
|
||||
|
||||
it('a message scheduled for later still goes to the main window, and the window closes', async () => {
|
||||
await sendFromWindow(3600);
|
||||
expect(screen.queryByTestId('popout-sending')).toBeNull();
|
||||
// "Sending…" may still show as the window goes, but no Undo here.
|
||||
expect(screen.queryByRole('button', { name: 'undo_send' })).toBeNull();
|
||||
expect(closeSelf).toHaveBeenCalled();
|
||||
const sent = heard.find((message) => message.type === 'sent') as Extract<PopoutMessage, { type: 'sent' }> | undefined;
|
||||
expect(sent?.notice.pendingUndoSend?.submissionId).toBe('sub1');
|
||||
|
||||
@@ -67,6 +67,17 @@ interface ReopenTarget {
|
||||
ownerAccountId?: string;
|
||||
}
|
||||
|
||||
/** "Sending…" or "Sent", where the composer was, with the send icon. */
|
||||
function SendStatus({ text, children }: { text: string; children?: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-4 p-6 text-center" data-testid="popout-sending">
|
||||
<Send className="h-6 w-6 text-muted-foreground" aria-hidden="true" />
|
||||
<p role="status" className="text-sm text-foreground">{text}</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A send held for undo, shown in the window it was sent from: that's where the
|
||||
* user is looking, and quick compose is used with the main window out of sight.
|
||||
@@ -79,6 +90,7 @@ function SendingWithUndo({ held, onUndone, onGone }: {
|
||||
onGone: () => void;
|
||||
}) {
|
||||
const tViewer = useTranslations("email_viewer");
|
||||
const tSend = useTranslations("compose_send");
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const [busy, setBusy] = useState(false);
|
||||
const sendAt = Date.parse(held.sendAt);
|
||||
@@ -122,17 +134,17 @@ function SendingWithUndo({ held, onUndone, onGone }: {
|
||||
onUndone(held.isSmime ? null : restored);
|
||||
};
|
||||
|
||||
const seconds = Math.max(0, Math.ceil((sendAt - now) / 1000));
|
||||
const seconds = Math.max(1, Math.ceil((sendAt - now) / 1000));
|
||||
// Once Undo is over, "Sent" for the moment before the window goes, as the
|
||||
// main window's toast says it.
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-4 p-6 text-center" data-testid="popout-sending">
|
||||
<Send className="h-6 w-6 text-muted-foreground" aria-hidden="true" />
|
||||
<p role="status" className="text-sm text-foreground">{tViewer("sending_in", { seconds })}</p>
|
||||
<SendStatus text={over && !busy ? tSend("sent") : tViewer("sending_in", { seconds })}>
|
||||
{!over && (
|
||||
<Button onClick={() => void undo()} disabled={busy} data-testid="popout-undo-send">
|
||||
{tViewer("undo_send")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SendStatus>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -320,7 +332,11 @@ function PopoutComposer({ payload, handoffId, onReopen }: {
|
||||
: t("email_composer.new_message"),
|
||||
);
|
||||
|
||||
const handleSend = useCallback(async (data: SendData) => {
|
||||
// "Sending…" from the moment Send goes through, as the main window says it
|
||||
// (compose-24); the composer waited greyed out for the server before. It
|
||||
// stays mounted behind, and comes back with the text if the send fails.
|
||||
const [sending, setSending] = useState(false);
|
||||
const sendFromWindow = useCallback(async (data: SendData) => {
|
||||
if (!client) return false;
|
||||
const sendClient = data.localAccountId
|
||||
? (useAuthStore.getState().getClientForAccount(data.localAccountId) ?? client)
|
||||
@@ -386,6 +402,16 @@ function PopoutComposer({ payload, handoffId, onReopen }: {
|
||||
await new Promise((resolve) => setTimeout(resolve, WARNING_READ_MS));
|
||||
}
|
||||
}, [client, mode, payload.sourceEmailId, payload.sourceEmailSource, t]);
|
||||
const handleSend = useCallback(async (data: SendData) => {
|
||||
if (!client) return false;
|
||||
if (!data.delayedUntil) setSending(true);
|
||||
try {
|
||||
return await sendFromWindow(data);
|
||||
} catch (error) {
|
||||
setSending(false);
|
||||
throw error;
|
||||
}
|
||||
}, [client, sendFromWindow]);
|
||||
|
||||
const handleDiscardDraft = useCallback(async (draftId: string, draft?: { accountId?: string; localAccountId?: string }) => {
|
||||
if (!client) return;
|
||||
@@ -507,6 +533,9 @@ function PopoutComposer({ payload, handoffId, onReopen }: {
|
||||
if (held) return <SendingWithUndo held={held} onUndone={heldUndone} onGone={heldGone} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{sending && <SendStatus text={t("compose_send.sending")} />}
|
||||
<div className={sending ? "hidden" : "contents"}>
|
||||
<ErrorBoundary fallback={ComposerErrorFallback}>
|
||||
<EmailComposer
|
||||
mode={mode}
|
||||
@@ -527,6 +556,8 @@ function PopoutComposer({ payload, handoffId, onReopen }: {
|
||||
className="min-h-0 flex-1"
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user