diff --git a/lib/__tests__/format-relative-ago.test.ts b/lib/__tests__/format-relative-ago.test.ts index 14d69202b..befcfba80 100644 --- a/lib/__tests__/format-relative-ago.test.ts +++ b/lib/__tests__/format-relative-ago.test.ts @@ -1,9 +1,15 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, afterEach } from 'vitest'; import { formatRelativeAgo } from '../utils'; +import { AUTO_TIME_ZONE } from '../timezone'; +import { useSettingsStore } from '@/stores/settings-store'; describe('formatRelativeAgo', () => { const now = new Date('2026-07-01T12:00:00Z'); + afterEach(() => { + useSettingsStore.setState({ timeZone: AUTO_TIME_ZONE }); + }); + it('says how long ago in the largest whole unit', () => { expect(formatRelativeAgo(new Date('2026-07-01T11:57:00Z'), now)).toBe('3 minutes ago'); expect(formatRelativeAgo(new Date('2026-07-01T08:00:00Z'), now)).toBe('4 hours ago'); @@ -14,4 +20,16 @@ describe('formatRelativeAgo', () => { expect(formatRelativeAgo(new Date('2026-05-01T12:00:00Z'), now)).toBe(''); expect(formatRelativeAgo('not a date', now)).toBe(''); }); + + it('counts calendar days in the effective time zone (#1259)', () => { + useSettingsStore.setState({ timeZone: 'America/Denver' }); + // 16 Oct, 10:00 in Denver. + const denverNow = new Date('2026-10-16T16:00:00Z'); + // 14 Oct, 20:51 in Denver: only 37 hours ago, but two days back. + expect(formatRelativeAgo(new Date('2026-10-15T02:51:30Z'), denverNow)).toBe('2 days ago'); + // 15 Oct, 08:00 in Denver: 26 hours ago, and yesterday. + expect(formatRelativeAgo(new Date('2026-10-15T14:00:00Z'), denverNow)).toBe('yesterday'); + // Under a day still reads in hours, even across midnight. + expect(formatRelativeAgo(new Date('2026-10-15T23:00:00Z'), denverNow)).toBe('17 hours ago'); + }); }); diff --git a/lib/utils.ts b/lib/utils.ts index 6d30d4514..1af71cfcd 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -258,6 +258,10 @@ export function formatDateTime( * How long ago `date` was, in words ("3 minutes ago", "2 days ago"), for the * parenthesis Gmail puts after a message's time. Empty once it is a month * old, where the date alone says enough. + * + * Days are calendar days in the user's effective time zone, so a mail from + * the day before yesterday is never called "yesterday" just because fewer + * than 48 hours have passed. (#1259) */ export function formatRelativeAgo(date: Date | string, now: Date = new Date()): string { const d = typeof date === 'string' ? new Date(date) : date; @@ -265,7 +269,12 @@ export function formatRelativeAgo(date: Date | string, now: Date = new Date()): const diff = Math.max(0, now.getTime() - d.getTime()); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); - const days = Math.floor(diff / 86400000); + const timeZone = getEffectiveTimeZone(); + const dayNumber = (x: 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)); if (days >= 30) return ''; const locale = getEffectiveLocale(); const rtf = new Intl.RelativeTimeFormat(locale === 'en' ? 'en-US' : locale, { numeric: 'auto' });