diff --git a/src/features/github/create/ChangesPanel.tsx b/src/features/github/create/ChangesPanel.tsx index 758b3470a..5d03a01cc 100644 --- a/src/features/github/create/ChangesPanel.tsx +++ b/src/features/github/create/ChangesPanel.tsx @@ -12,7 +12,7 @@ import type { Commit } from "@/platform/ipc"; import { tx } from "@/platform/i18n"; import { Icon, IconButton, Spinner, T, Tree, fileIconName, showContextMenu, type MenuItem } from "@/ui"; import { ChooserPopup } from "../ui/ChooserPopup"; -import { prettyDateTime } from "../ui/prettyDate"; +import { prettyDate } from "../ui/prettyDate"; import { commitDescription } from "./model"; import { selectCommit, useCreatePr, type CreateChange } from "./store"; import styles from "./Create.module.css"; @@ -119,7 +119,7 @@ function CommitChooser({ commits, selected }: { commits: Commit[]; selected: Com c ? { text: c.summary, - detail: `${c.author}, ${c.timeMs ? prettyDateTime(c.timeMs) : ""}`, + detail: `${c.author}, ${c.timeMs ? prettyDate(c.timeMs) : ""}`, icon: selected?.id === c.id ? "check" : "commit", } : { text: tx`All commits (${commits.length})`, icon: selected ? "commit" : "check" } @@ -150,7 +150,7 @@ function CommitDetails({ commit }: { commit: Commit }) { {details && description &&
{actorLink(actor)} - {author?.date && <> {prettyDateTime(author.date)}>} + {author?.date && ( + <> + {prettyDate(author.date)} + > + )}
)} diff --git a/src/features/github/ui/prettyDate.ts b/src/features/github/ui/prettyDate.ts index 1eb8d284f..3941c5a22 100644 --- a/src/features/github/ui/prettyDate.ts +++ b/src/features/github/ui/prettyDate.ts @@ -1,46 +1,59 @@ /** - * IntelliJ's `DateFormatUtil.formatPrettyDate` / `formatPrettyDateTime`, honouring - * Settings › Date Formats: "Moments ago", "N minutes ago" within the hour, "Today" / - * "Yesterday" (with the time for `prettyDateTime`), else the date. With pretty - * formatting off, the plain date (and time). + * GitHub-style relative dates: "just now", "N minutes ago", "N hours ago", + * "yesterday", "N days ago", then "on Oct 12" (with the year when it differs). + * With Settings › Date Formats › pretty formatting off, the plain date and time. */ -import { dateFormatOptions, formatClock, formatDay, type DateFormatOptions } from "@/platform/dateFormat"; +import { + dateFormatOptions, + formatClock, + formatDay, + formatPattern, + type DateFormatOptions, +} from "@/platform/dateFormat"; import { tx } from "@/platform/i18n"; const MINUTE = 60_000; const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; -function pretty(ms: number, withTime: boolean, now: number, o: DateFormatOptions): string { - const d = new Date(ms); - const time = withTime ? ` ${formatClock(d, o)}` : ""; - if (o.pretty) { - const delta = now - ms; - const today = new Date(now); - if (d.toDateString() === today.toDateString()) { - if (delta >= 0 && delta < MINUTE) return tx("Moments ago"); - if (delta >= 0 && delta <= HOUR) { - const n = Math.floor(delta / MINUTE); - return n === 1 ? tx("1 minute ago") : tx`${n} minutes ago`; - } - return withTime ? tx`Today ${formatClock(d, o)}` : tx("Today"); - } - if (d.toDateString() === new Date(now - 86_400_000).toDateString()) - return withTime ? tx`Yesterday ${formatClock(d, o)}` : tx("Yesterday"); - } - return `${formatDay(d, o)}${time}`; +function onDay(d: Date, now: number, o: DateFormatOptions): string { + if (o.overrideSystem) return tx`on ${formatPattern(d, o.pattern)}`; + const sameYear = d.getFullYear() === new Date(now).getFullYear(); + const day = d.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + ...(sameYear ? {} : { year: "numeric" }), + }); + return tx`on ${day}`; } const toMs = (date: string | number | Date) => typeof date === "number" ? date : date instanceof Date ? date.getTime() : Date.parse(date); -/** `formatPrettyDate`: list rows ("created {date}"). Empty for an invalid date. */ +/** List rows, timeline headers, commits and replies. Empty for an invalid date. */ export function prettyDate(date: string | number | Date, now = Date.now(), o = dateFormatOptions()): string { const ms = toMs(date); - return Number.isNaN(ms) ? "" : pretty(ms, false, now, o); + if (Number.isNaN(ms)) return ""; + const d = new Date(ms); + if (!o.pretty) return `${formatDay(d, o)} ${formatClock(d, o)}`; + const delta = Math.max(0, now - ms); + if (delta < MINUTE) return tx("just now"); + if (delta < HOUR) { + const n = Math.floor(delta / MINUTE); + return n === 1 ? tx("1 minute ago") : tx`${n} minutes ago`; + } + if (delta < DAY) { + const n = Math.floor(delta / HOUR); + return n === 1 ? tx("1 hour ago") : tx`${n} hours ago`; + } + const days = Math.floor(delta / DAY); + if (days === 1) return tx("yesterday"); + if (days < 30) return tx`${days} days ago`; + return onDay(d, now, o); } -/** `formatPrettyDateTime`: timeline headers, commits, replies. */ -export function prettyDateTime(date: string | number | Date, now = Date.now(), o = dateFormatOptions()): string { +/** The full date and time, for a tooltip on a relative date. */ +export function fullDateTime(date: string | number | Date, o = dateFormatOptions()): string { const ms = toMs(date); - return Number.isNaN(ms) ? "" : pretty(ms, true, now, o); + return Number.isNaN(ms) ? "" : `${formatDay(new Date(ms), o)} ${formatClock(new Date(ms), o)}`; } diff --git a/src/features/github/ui/ui.test.tsx b/src/features/github/ui/ui.test.tsx index 9e55131b3..747cf4a6d 100644 --- a/src/features/github/ui/ui.test.tsx +++ b/src/features/github/ui/ui.test.tsx @@ -6,27 +6,28 @@ import { Avatar, AvatarStack, avatarSrc, reviewerRingColor } from "./Avatar"; import { BranchChip, LabelChip, StateTag, labelForeground } from "./chips"; import { ChooserPopup, chooserItems } from "./ChooserPopup"; import { Markdown } from "./Markdown"; -import { prettyDate, prettyDateTime } from "./prettyDate"; +import { fullDateTime, prettyDate } from "./prettyDate"; describe("prettyDate", () => { const o = { ...DEFAULT_DATE_FORMAT, overrideSystem: true, pattern: "dd.MM.yyyy", use24Hour: true }; - const now = new Date(2026, 9, 30, 15, 0).getTime(); + const now = new Date(2026, 9, 31, 9, 2).getTime(); - it("formats like DateFormatUtil.formatPrettyDate", () => { - expect(prettyDate(now - 20_000, now, o)).toBe("Moments ago"); + it("formats relative dates like GitHub", () => { + expect(prettyDate(now - 20_000, now, o)).toBe("just now"); + expect(prettyDate(now + 20_000, now, o)).toBe("just now"); expect(prettyDate(now - 60_000, now, o)).toBe("1 minute ago"); expect(prettyDate(now - 42 * 60_000, now, o)).toBe("42 minutes ago"); - expect(prettyDate(now - 3 * 3_600_000, now, o)).toBe("Today"); - expect(prettyDate(new Date(2026, 9, 29, 9, 5).getTime(), now, o)).toBe("Yesterday"); - expect(prettyDate(new Date(2026, 8, 2, 9, 5).getTime(), now, o)).toBe("02.09.2026"); + expect(prettyDate(now - 3_600_000, now, o)).toBe("1 hour ago"); + expect(prettyDate(new Date(2026, 9, 31, 0, 27).getTime(), now, o)).toBe("8 hours ago"); + expect(prettyDate(new Date(2026, 9, 30, 8, 0).getTime(), now, o)).toBe("yesterday"); + expect(prettyDate(new Date(2026, 9, 21, 9, 5).getTime(), now, o)).toBe("9 days ago"); + expect(prettyDate(new Date(2026, 8, 2, 9, 5).getTime(), now, o)).toBe("on 02.09.2026"); expect(prettyDate("not a date", now, o)).toBe(""); }); - it("adds the time with formatPrettyDateTime, and honours pretty formatting off", () => { - expect(prettyDateTime(new Date(2026, 9, 30, 9, 5).getTime(), now, o)).toBe("Today 09:05"); - expect(prettyDateTime(new Date(2026, 9, 29, 9, 5).getTime(), now, o)).toBe("Yesterday 09:05"); - expect(prettyDateTime(new Date(2026, 8, 2, 9, 5).getTime(), now, o)).toBe("02.09.2026 09:05"); - expect(prettyDate(now - 20_000, now, { ...o, pretty: false })).toBe("30.10.2026"); + it("shows the plain date and time with pretty formatting off", () => { + expect(prettyDate(now - 20_000, now, { ...o, pretty: false })).toBe("31.10.2026 09:01"); + expect(fullDateTime(new Date(2026, 9, 31, 0, 27).getTime(), o)).toBe("31.10.2026 00:27"); }); }); diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index ad96e2166..7f3471076 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -4976,6 +4976,10 @@ "Merge commit message:": "合并提交消息:", "Merge commit message": "合并提交消息", "1 minute ago": "1 分钟前", + "1 hour ago": "1 小时前", + "{n} hours ago": "{n} 小时前", + "{n} days ago": "{n} 天前", + "yesterday": "昨天", "Today": "今天", "Yesterday": "昨天", "Select pull request base and head to view changes": "选择拉取请求的基础分支和头分支以查看更改",