mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: tell today's mail by the time zone setting in the sender line #1259
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
|
||||
// #1259: the Mountain View sender line shows only the time for today's mail
|
||||
// and the date as well before that. "Today" is the day in the time zone
|
||||
// setting, the zone the time and the "(… ago)" after it are shown in, not the
|
||||
// browser's.
|
||||
|
||||
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() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({
|
||||
PluginSlot: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-plugin-slot-offers', () => ({
|
||||
usePluginSlotOffers: () => [],
|
||||
}));
|
||||
|
||||
class NoopResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', NoopResizeObserver);
|
||||
|
||||
const { EmailViewer } = await import('../email-viewer');
|
||||
|
||||
const mailboxes: Mailbox[] = [
|
||||
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
|
||||
] as unknown as Mailbox[];
|
||||
|
||||
function mail(n: number, receivedAt: string, overrides: Partial<Email> = {}): Email {
|
||||
return {
|
||||
id: `m${n}`,
|
||||
blobId: `b${n}`,
|
||||
threadId: 't1',
|
||||
mailboxIds: { inbox: true },
|
||||
keywords: { $seen: true },
|
||||
from: [{ name: `Sender ${n}`, email: `s${n}@example.com` }],
|
||||
to: [{ name: 'Me', email: 'me@example.com' }],
|
||||
subject: 'Subject',
|
||||
receivedAt,
|
||||
preview: `Preview ${n}`,
|
||||
hasAttachment: false,
|
||||
size: 100,
|
||||
...overrides,
|
||||
} as unknown as Email;
|
||||
}
|
||||
|
||||
let originalTZ: string | undefined;
|
||||
|
||||
describe('Mountain View sender date (#1259)', () => {
|
||||
beforeAll(() => {
|
||||
// The browser's zone, a day ahead of Denver in the evening.
|
||||
originalTZ = process.env.TZ;
|
||||
process.env.TZ = 'UTC';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.TZ = originalTZ;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
useSettingsStore.setState({ timeZone: 'auto' });
|
||||
});
|
||||
|
||||
it("shows the date for yesterday's mail in the time zone setting", () => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
// 16 Oct, 10:00 in Denver.
|
||||
vi.setSystemTime(new Date('2026-10-16T16:00:00Z'));
|
||||
useSettingsStore.setState({
|
||||
interfaceLayout: 'mountain-view', markAsReadDelay: 0, timeZone: 'America/Denver', timeFormat: '24h',
|
||||
});
|
||||
// 15 Oct, 21:00 in Denver, which is already 16 Oct in UTC.
|
||||
const yesterday = mail(1, '2026-10-16T03:00:00Z');
|
||||
const open = {
|
||||
...mail(2, '2026-10-16T15:00:00Z'),
|
||||
textBody: [{ partId: '1', type: 'text/plain' }],
|
||||
htmlBody: [],
|
||||
attachments: [],
|
||||
bodyValues: { '1': { value: 'Open body', isTruncated: false } },
|
||||
} as unknown as Email;
|
||||
const { container } = render(
|
||||
<EmailViewer
|
||||
email={open}
|
||||
mailboxes={mailboxes}
|
||||
selectedMailbox="inbox"
|
||||
currentMailboxRole="inbox"
|
||||
mvConversation={[yesterday, open]}
|
||||
/>,
|
||||
);
|
||||
const card = container.querySelector('[data-mv-conversation-card][data-email-id="m1"]');
|
||||
expect(card?.textContent).toContain('21:00');
|
||||
expect(card?.textContent).toContain('2026');
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { SubMenuPanel } from "@/components/ui/context-menu";
|
||||
import { formatFileSize, cn, MailboxNode, formatDate, formatDateTime, formatRelativeAgo, generateUUID, cleanPreview } from "@/lib/utils";
|
||||
import { getEffectiveTimeZone, getWallClock } from "@/lib/timezone";
|
||||
import { itemsToHide } from "@/lib/toolbar-overflow";
|
||||
import { formatScheduledSendAt } from "@/lib/scheduled-send-label";
|
||||
import { buildMoveTargets, resolveMoveOwnerAccountId } from "@/lib/move-targets";
|
||||
@@ -689,7 +690,12 @@ function DraggableAttachmentChip({ attachment, client, accountId, enabled, downl
|
||||
*/
|
||||
function mvSenderDate(date: Date | string, timeFormat: '12h' | '24h'): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
const today = new Date().toDateString() === d.toDateString();
|
||||
// "Today" in the time zone setting, the zone the time and the relative
|
||||
// part are shown in, not the browser's (#1259).
|
||||
const zone = getEffectiveTimeZone();
|
||||
const day = Number.isNaN(d.getTime()) ? null : getWallClock(d, zone);
|
||||
const now = getWallClock(new Date(), zone);
|
||||
const today = !!day && day.year === now.year && day.month === now.month && day.day === now.day;
|
||||
const when = today
|
||||
? formatDateTime(d, timeFormat)
|
||||
: formatDateTime(d, timeFormat, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
|
||||
@@ -32,4 +32,10 @@ describe('formatRelativeAgo', () => {
|
||||
// Under a day still reads in hours, even across midnight.
|
||||
expect(formatRelativeAgo(new Date('2026-10-15T23:00:00Z'), denverNow)).toBe('17 hours ago');
|
||||
});
|
||||
|
||||
it('does not call the same day yesterday on the 25-hour day the clocks go back', () => {
|
||||
useSettingsStore.setState({ timeZone: 'America/Denver' });
|
||||
// 1 Nov 2026 in Denver: 00:10 MDT, and 24.5 hours later 23:40 MST.
|
||||
expect(formatRelativeAgo(new Date('2026-11-01T06:10:00Z'), new Date('2026-11-02T06:40:00Z'))).toBe('24 hours ago');
|
||||
});
|
||||
});
|
||||
|
||||
+3
-2
@@ -274,13 +274,14 @@ export function formatRelativeAgo(date: Date | string, now: Date = new Date()):
|
||||
const w = getWallClock(x, timeZone);
|
||||
return Date.UTC(w.year, w.month - 1, w.day) / 86400000;
|
||||
};
|
||||
const days = Math.max(1, dayNumber(now) - dayNumber(d));
|
||||
const days = dayNumber(now) - dayNumber(d);
|
||||
if (days >= 30) return '';
|
||||
const locale = getEffectiveLocale();
|
||||
const rtf = new Intl.RelativeTimeFormat(locale === 'en' ? 'en-US' : locale, { numeric: 'auto' });
|
||||
if (minutes < 1) return rtf.format(0, 'second');
|
||||
if (minutes < 60) return rtf.format(-minutes, 'minute');
|
||||
if (hours < 24) return rtf.format(-hours, 'hour');
|
||||
// The day the clocks go back has 25 hours: 24 of them can still be today.
|
||||
if (hours < 24 || days < 1) return rtf.format(-hours, 'hour');
|
||||
return rtf.format(-days, 'day');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user