fix: close the details panel on the next message and show no delivery time for mail received before it was sent

This commit is contained in:
Linus Rath
2026-11-04 12:09:16 +01:00
parent e93e0419b8
commit a2f5bad572
2 changed files with 76 additions and 2 deletions
@@ -0,0 +1,66 @@
import { render, screen, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import type { Email, Mailbox } from '@/lib/jmap/types';
// mail-7: "Show details" stayed open for every message opened after it.
// mail-29: a mail received before it was sent showed a delivery time.
vi.mock('@/hooks/use-media-query', () => ({
useDeviceDetection: () => ({ isMobile: false, isTablet: false, isDesktop: true }),
useMediaQuery: () => false,
}));
vi.mock('@/components/tour/tour-provider', () => ({
useTour: () => ({ activeTour: null, startTour: vi.fn(), endTour: vi.fn(), registerStep: vi.fn() }),
}));
class NoopResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
vi.stubGlobal('ResizeObserver', NoopResizeObserver);
const { EmailViewer } = await import('../email-viewer');
const mailboxes = [
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
] as unknown as Mailbox[];
const make = (id: string, sentAt: string, receivedAt: string) => ({
id, blobId: `b-${id}`, threadId: `t-${id}`, mailboxIds: { inbox: true }, keywords: { $seen: true },
from: [{ name: 'Sam', email: 'sam@example.com' }], to: [{ name: 'Bob', email: 'bob@example.com' }],
subject: `Message ${id}`, sentAt, receivedAt, preview: 'Hello',
hasAttachment: false, size: 100, textBody: [], htmlBody: [], attachments: [], bodyValues: {},
}) as unknown as Email;
const viewer = (email: Email) => (
<EmailViewer email={email} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" isLoading={false} />
);
describe('message details panel', () => {
it('closes when another message opens', () => {
const first = make('e1', '2026-11-03T10:00:00Z', '2026-11-03T10:00:05Z');
const second = make('e2', '2026-11-03T11:00:00Z', '2026-11-03T11:00:05Z');
const { rerender } = render(viewer(first));
act(() => { fireEvent.click(screen.getAllByText('show_details')[0]); });
expect(screen.queryByText('details.recipients_routing')).not.toBeNull();
rerender(viewer(second));
expect(screen.queryByText('details.recipients_routing')).toBeNull();
// And back on the first one it is closed too: it was opened for that view.
rerender(viewer(first));
expect(screen.queryByText('details.recipients_routing')).toBeNull();
});
it('shows no delivery time for a mail received before it was sent', () => {
render(viewer(make('e3', '2026-11-03T21:40:00Z', '2026-11-03T20:40:00Z')));
act(() => { fireEvent.click(screen.getAllByText('show_details')[0]); });
expect(screen.queryByText(/details\.delivery_time/i)).toBeNull();
expect(document.body.textContent).not.toMatch(/delivery_time/);
});
it('still shows a real delivery time', () => {
render(viewer(make('e4', '2026-11-03T20:40:00Z', '2026-11-03T21:40:00Z')));
act(() => { fireEvent.click(screen.getAllByText('show_details')[0]); });
expect(document.body.textContent).toMatch(/delivery_time/);
});
});
+10 -2
View File
@@ -1670,7 +1670,13 @@ export function EmailViewer({
}, [onRescheduleScheduled, t, tComposer, scheduledSendAt, timeFormat]);
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const { startTour } = useTour();
const [showFullHeaders, setShowFullHeaders] = useState(false);
// The details panel belongs to the message it was opened on: the next
// message opens without it (mail-7).
const shownEmailId = email?.id ?? null;
const [details, setDetails] = useState<{ emailId: string | null; open: boolean }>({ emailId: shownEmailId, open: false });
if (details.emailId !== shownEmailId) setDetails({ emailId: shownEmailId, open: false });
const showFullHeaders = details.open && details.emailId === shownEmailId;
const setShowFullHeaders = (open: boolean) => setDetails({ emailId: shownEmailId, open });
const [isDownloadingAll, setIsDownloadingAll] = useState(false);
const [imageThumbUrls, setImageThumbUrls] = useState<Record<string, string>>({});
const [allowExternalContent, setAllowExternalContent] = useState(false);
@@ -5516,8 +5522,10 @@ ${printWatermark ? printWatermark.html : ''}
};
const replyToDifferent = !!email.replyTo?.length &&
(!email.from || email.replyTo[0].email !== email.from[0]?.email);
// Received before it was sent means a wrong clock somewhere, not a
// delivery time (mail-29).
const deliveryDeltaMs = email.sentAt && email.receivedAt
? Math.abs(new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime())
? new Date(email.receivedAt).getTime() - new Date(email.sentAt).getTime()
: 0;
const formatDelta = (diff: number) => {
const minutes = Math.floor(diff / 60000);