mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
feat(github): show relative dates in pull requests
Pull request lists, timelines, commits and replies show how long ago
something happened ("just now", "8 hours ago", "yesterday", "3 days ago")
and the date after 30 days ("on Oct 12"). Hovering a timeline date shows
the full date and time. With pretty formatting off, the plain date and time
are shown.
This commit is contained in:
1 parent
2bc5e8d0c8
commit
89475f4fed
8 files changed
+81
-51
No files matched your search
@@ -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 && <div className={styles.commitBody}>{description}</div>}
|
||||
{details && (
|
||||
<div className={styles.commitMeta}>
|
||||
{commit.author}, {commit.timeMs ? prettyDateTime(commit.timeMs) : ""}
|
||||
{commit.author}, {commit.timeMs ? prettyDate(commit.timeMs) : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import type { GHPullRequestCommit } from "../api/types";
|
||||
import { GHOST_USER } from "../data/model";
|
||||
import { prettyDateTime } from "../ui/prettyDate";
|
||||
import { prettyDate } from "../ui/prettyDate";
|
||||
import { commitsTitle, stepCommit } from "./model";
|
||||
import styles from "./Details.module.css";
|
||||
|
||||
@@ -34,7 +34,7 @@ function presentation(c: GHPullRequestCommit) {
|
||||
title: c.commit.messageHeadline,
|
||||
description: c.commit.messageBody,
|
||||
author: user.name || user.login,
|
||||
date: prettyDateTime(c.commit.committedDate),
|
||||
date: prettyDate(c.commit.committedDate),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { GHActor } from "../../api/types";
|
||||
import { GHOST_USER } from "../../data/model";
|
||||
import { openInBrowser } from "../../data/local";
|
||||
import { Avatar, AVATAR_SIZE } from "../../ui/Avatar";
|
||||
import { prettyDateTime } from "../../ui/prettyDate";
|
||||
import { fullDateTime, prettyDate } from "../../ui/prettyDate";
|
||||
import { presentableName } from "../model";
|
||||
import styles from "./comments.module.css";
|
||||
|
||||
@@ -78,7 +78,11 @@ export function ChatItem({
|
||||
{title ?? (
|
||||
<span>
|
||||
<AuthorLink actor={a} />
|
||||
{date && <span className={styles.date}> {prettyDateTime(date)}</span>}
|
||||
{date && (
|
||||
<span className={styles.date} title={fullDateTime(date)}>
|
||||
{prettyDate(date)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{tags}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "../../data/operations";
|
||||
import { AvatarStack } from "../../ui/Avatar";
|
||||
import { Markdown } from "../../ui/Markdown";
|
||||
import { prettyDateTime } from "../../ui/prettyDate";
|
||||
import { fullDateTime, prettyDate } from "../../ui/prettyDate";
|
||||
import { presentableName, threadDiffHunk, threadDiffWithAnchor } from "../model";
|
||||
import { ChatItem, Tag, type ChatItemVariant } from "./ChatItem";
|
||||
import { CommentEditor, submitShortcut } from "./CommentEditor";
|
||||
@@ -288,7 +288,11 @@ function FullThread({ number, thread }: Omit<ThreadViewProps, "variant">) {
|
||||
>
|
||||
{n === 0 ? t("Reply") : n === 1 ? tx`${n} reply` : tx`${n} replies`}
|
||||
</button>
|
||||
{last && <span className={styles.secondary}>{prettyDateTime(last.createdAt)}</span>}
|
||||
{last && (
|
||||
<span className={styles.secondary} title={fullDateTime(last.createdAt)}>
|
||||
{prettyDate(last.createdAt)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{resolve.canChange && (
|
||||
|
||||
@@ -21,7 +21,7 @@ import { sameRepository, useGhRepository } from "../data/repository";
|
||||
import { openPullRequest } from "../toolwindow/open";
|
||||
import { BranchChip, LabelChip } from "../ui/chips";
|
||||
import { Markdown } from "../ui/Markdown";
|
||||
import { prettyDateTime } from "../ui/prettyDate";
|
||||
import { fullDateTime, prettyDate } from "../ui/prettyDate";
|
||||
import { ChatItem, CommentItem, ThreadView } from "./comments";
|
||||
import { joined, rich } from "./comments/ChatItem";
|
||||
import {
|
||||
@@ -293,7 +293,11 @@ function CommitsItem({ commits, prAuthor }: { commits: TimelineCommit[]; prAutho
|
||||
{actor && (
|
||||
<p className={styles.paragraph}>
|
||||
{actorLink(actor)}
|
||||
{author?.date && <> {prettyDateTime(author.date)}</>}
|
||||
{author?.date && (
|
||||
<>
|
||||
<span title={fullDateTime(author.date)}>{prettyDate(author.date)}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user