@theia/ai-chat-ui 1.77.0-next.37 → 1.77.0-next.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/chat-date-utils.d.ts +5 -7
- package/lib/browser/chat-date-utils.d.ts.map +1 -1
- package/lib/browser/chat-date-utils.js +47 -13
- package/lib/browser/chat-date-utils.js.map +1 -1
- package/lib/browser/chat-date-utils.spec.d.ts +2 -0
- package/lib/browser/chat-date-utils.spec.d.ts.map +1 -0
- package/lib/browser/chat-date-utils.spec.js +61 -0
- package/lib/browser/chat-date-utils.spec.js.map +1 -0
- package/package.json +11 -12
- package/src/browser/chat-date-utils.spec.ts +65 -0
- package/src/browser/chat-date-utils.ts +48 -12
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import * as locales from 'date-fns/locale';
|
|
2
|
-
/**
|
|
3
|
-
* Returns the date-fns locale matching the current Theia locale.
|
|
4
|
-
*/
|
|
5
|
-
export declare function getDateFnsLocale(): locales.Locale;
|
|
6
1
|
/**
|
|
7
2
|
* Formats a timestamp as a human-readable relative time string (e.g., "2 hours ago").
|
|
3
|
+
*
|
|
4
|
+
* The largest unit that fits into the elapsed time is used and the value is rounded
|
|
5
|
+
* down so that it never crosses into the next larger unit. Timestamps less than a
|
|
6
|
+
* second away render as "0 seconds ago", future ones as "in ...".
|
|
8
7
|
* @param timestamp - The timestamp in milliseconds
|
|
9
|
-
* @param addSuffix - Whether to add "ago" suffix (default: true)
|
|
10
8
|
*/
|
|
11
|
-
export declare function formatTimeAgo(timestamp: number
|
|
9
|
+
export declare function formatTimeAgo(timestamp: number): string;
|
|
12
10
|
//# sourceMappingURL=chat-date-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-date-utils.d.ts","sourceRoot":"","sources":["../../src/browser/chat-date-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chat-date-utils.d.ts","sourceRoot":"","sources":["../../src/browser/chat-date-utils.ts"],"names":[],"mappings":"AAoDA;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAcvD"}
|
|
@@ -15,27 +15,61 @@
|
|
|
15
15
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
16
|
// *****************************************************************************
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.getDateFnsLocale = getDateFnsLocale;
|
|
19
18
|
exports.formatTimeAgo = formatTimeAgo;
|
|
20
19
|
const core_1 = require("@theia/core");
|
|
21
|
-
const date_fns_1 = require("date-fns");
|
|
22
|
-
const locales = require("date-fns/locale");
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
21
|
+
* The relative time units supported by `formatTimeAgo` with their approximate
|
|
22
|
+
* lengths in milliseconds, from largest to smallest. The first unit that fits
|
|
23
|
+
* into the elapsed time is used.
|
|
25
24
|
*/
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
25
|
+
const RELATIVE_TIME_UNITS = [
|
|
26
|
+
['year', 31_557_600_000], // 365.25 days
|
|
27
|
+
['month', 2_629_800_000], // 30.44 days
|
|
28
|
+
['week', 604_800_000],
|
|
29
|
+
['day', 86_400_000],
|
|
30
|
+
['hour', 3_600_000],
|
|
31
|
+
['minute', 60_000],
|
|
32
|
+
['second', 1_000]
|
|
33
|
+
];
|
|
34
|
+
let cachedRelativeTimeFormat;
|
|
35
|
+
/**
|
|
36
|
+
* Returns a cached `Intl.RelativeTimeFormat` for the current Theia locale,
|
|
37
|
+
* falling back to English if the locale is not supported.
|
|
38
|
+
*/
|
|
39
|
+
function getRelativeTimeFormat() {
|
|
40
|
+
// Theia locale ids may use '_' as region separator while `Intl` expects BCP-47 ('-').
|
|
41
|
+
const locale = core_1.nls.locale?.replace('_', '-');
|
|
42
|
+
if (cachedRelativeTimeFormat === undefined || cachedRelativeTimeFormat.locale !== locale) {
|
|
43
|
+
try {
|
|
44
|
+
cachedRelativeTimeFormat = { locale, format: new Intl.RelativeTimeFormat(locale, { numeric: 'always' }) };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
cachedRelativeTimeFormat = { locale, format: new Intl.RelativeTimeFormat('en', { numeric: 'always' }) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return cachedRelativeTimeFormat.format;
|
|
29
51
|
}
|
|
30
52
|
/**
|
|
31
53
|
* Formats a timestamp as a human-readable relative time string (e.g., "2 hours ago").
|
|
54
|
+
*
|
|
55
|
+
* The largest unit that fits into the elapsed time is used and the value is rounded
|
|
56
|
+
* down so that it never crosses into the next larger unit. Timestamps less than a
|
|
57
|
+
* second away render as "0 seconds ago", future ones as "in ...".
|
|
32
58
|
* @param timestamp - The timestamp in milliseconds
|
|
33
|
-
* @param addSuffix - Whether to add "ago" suffix (default: true)
|
|
34
59
|
*/
|
|
35
|
-
function formatTimeAgo(timestamp
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
60
|
+
function formatTimeAgo(timestamp) {
|
|
61
|
+
if (typeof Intl === 'undefined' || typeof Intl.RelativeTimeFormat === 'undefined') {
|
|
62
|
+
// Environments without `Intl.RelativeTimeFormat` get an absolute date instead.
|
|
63
|
+
return new Date(timestamp).toLocaleString();
|
|
64
|
+
}
|
|
65
|
+
const elapsed = Date.now() - timestamp;
|
|
66
|
+
const direction = elapsed >= 0 ? -1 : 1; // `Intl` expects negative values for the past
|
|
67
|
+
const duration = Math.abs(elapsed);
|
|
68
|
+
for (const [unit, unitLength] of RELATIVE_TIME_UNITS) {
|
|
69
|
+
if (duration >= unitLength) {
|
|
70
|
+
return getRelativeTimeFormat().format(direction * Math.floor(duration / unitLength), unit);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return getRelativeTimeFormat().format(-0, 'second');
|
|
40
74
|
}
|
|
41
75
|
//# sourceMappingURL=chat-date-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-date-utils.js","sourceRoot":"","sources":["../../src/browser/chat-date-utils.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;
|
|
1
|
+
{"version":3,"file":"chat-date-utils.js","sourceRoot":"","sources":["../../src/browser/chat-date-utils.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;AA8ChF,sCAcC;AA1DD,sCAAkC;AAElC;;;;GAIG;AACH,MAAM,mBAAmB,GAAkE;IACvF,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,cAAc;IACxC,CAAC,OAAO,EAAE,aAAa,CAAC,EAAE,aAAa;IACvC,CAAC,MAAM,EAAE,WAAW,CAAC;IACrB,CAAC,KAAK,EAAE,UAAU,CAAC;IACnB,CAAC,MAAM,EAAE,SAAS,CAAC;IACnB,CAAC,QAAQ,EAAE,MAAM,CAAC;IAClB,CAAC,QAAQ,EAAE,KAAK,CAAC;CACpB,CAAC;AAEF,IAAI,wBAAqG,CAAC;AAE1G;;;GAGG;AACH,SAAS,qBAAqB;IAC1B,sFAAsF;IACtF,MAAM,MAAM,GAAG,UAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC7C,IAAI,wBAAwB,KAAK,SAAS,IAAI,wBAAwB,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QACvF,IAAI,CAAC;YACD,wBAAwB,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAC9G,CAAC;QAAC,MAAM,CAAC;YACL,wBAAwB,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAC5G,CAAC;IACL,CAAC;IACD,OAAO,wBAAwB,CAAC,MAAM,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,aAAa,CAAC,SAAiB;IAC3C,IAAI,OAAO,IAAI,KAAK,WAAW,IAAI,OAAO,IAAI,CAAC,kBAAkB,KAAK,WAAW,EAAE,CAAC;QAChF,+EAA+E;QAC/E,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,CAAC;IAChD,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACvC,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,8CAA8C;IACvF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,mBAAmB,EAAE,CAAC;QACnD,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;YACzB,OAAO,qBAAqB,EAAE,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,UAAU,CAAC,EAAE,IAAI,CAAC,CAAC;QAC/F,CAAC;IACL,CAAC;IACD,OAAO,qBAAqB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-date-utils.spec.d.ts","sourceRoot":"","sources":["../../src/browser/chat-date-utils.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const chai_1 = require("chai");
|
|
19
|
+
const chat_date_utils_1 = require("./chat-date-utils");
|
|
20
|
+
describe('formatTimeAgo', () => {
|
|
21
|
+
// `formatTimeAgo` uses the runtime's default locale when no Theia locale is
|
|
22
|
+
// set (as in tests), so expected strings must be computed with a reference
|
|
23
|
+
// formatter using the same locale instead of being hard-coded.
|
|
24
|
+
const reference = new Intl.RelativeTimeFormat(undefined, { numeric: 'always' });
|
|
25
|
+
it('uses the largest unit that fits into the elapsed time', () => {
|
|
26
|
+
const cases = [
|
|
27
|
+
[2_000, 'second'],
|
|
28
|
+
[2 * 60_000, 'minute'],
|
|
29
|
+
[2 * 3_600_000, 'hour'],
|
|
30
|
+
[2 * 86_400_000, 'day'],
|
|
31
|
+
[2 * 604_800_000, 'week'],
|
|
32
|
+
[2 * 2_629_800_000, 'month'],
|
|
33
|
+
[2 * 31_557_600_000, 'year']
|
|
34
|
+
];
|
|
35
|
+
for (const [elapsed, unit] of cases) {
|
|
36
|
+
const timestamp = Date.now() - elapsed;
|
|
37
|
+
(0, chai_1.expect)((0, chat_date_utils_1.formatTimeAgo)(timestamp)).to.equal(reference.format(-2, unit));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
it('rounds down so the value never crosses into the next larger unit', () => {
|
|
41
|
+
const cases = [
|
|
42
|
+
[90_000, 'minute', 1], // 1.5 minutes floor to 1
|
|
43
|
+
[59.5 * 60_000, 'minute', 59],
|
|
44
|
+
[23.6 * 3_600_000, 'hour', 23],
|
|
45
|
+
[6.6 * 86_400_000, 'day', 6],
|
|
46
|
+
[11.6 * 2_629_800_000, 'month', 11]
|
|
47
|
+
];
|
|
48
|
+
for (const [elapsed, unit, value] of cases) {
|
|
49
|
+
const timestamp = Date.now() - elapsed;
|
|
50
|
+
(0, chai_1.expect)((0, chat_date_utils_1.formatTimeAgo)(timestamp)).to.equal(reference.format(-value, unit));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it('renders the present timestamp as 0 seconds ago', () => {
|
|
54
|
+
(0, chai_1.expect)((0, chat_date_utils_1.formatTimeAgo)(Date.now())).to.equal(reference.format(-0, 'second'));
|
|
55
|
+
});
|
|
56
|
+
it('renders future timestamps as "in ..."', () => {
|
|
57
|
+
const timestamp = Date.now() + 2.5 * 3_600_000; // 2.5 hours floor to 2
|
|
58
|
+
(0, chai_1.expect)((0, chat_date_utils_1.formatTimeAgo)(timestamp)).to.equal(reference.format(2, 'hour'));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=chat-date-utils.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-date-utils.spec.js","sourceRoot":"","sources":["../../src/browser/chat-date-utils.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/ai-chat-ui",
|
|
3
|
-
"version": "1.77.0-next.
|
|
3
|
+
"version": "1.77.0-next.43+5dbb84a9f",
|
|
4
4
|
"description": "Theia - AI Chat UI Extension",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@theia/ai-chat": "1.77.0-next.
|
|
7
|
-
"@theia/ai-core": "1.77.0-next.
|
|
8
|
-
"@theia/core": "1.77.0-next.
|
|
9
|
-
"@theia/editor": "1.77.0-next.
|
|
10
|
-
"@theia/editor-preview": "1.77.0-next.
|
|
11
|
-
"@theia/filesystem": "1.77.0-next.
|
|
12
|
-
"@theia/monaco": "1.77.0-next.
|
|
6
|
+
"@theia/ai-chat": "1.77.0-next.43+5dbb84a9f",
|
|
7
|
+
"@theia/ai-core": "1.77.0-next.43+5dbb84a9f",
|
|
8
|
+
"@theia/core": "1.77.0-next.43+5dbb84a9f",
|
|
9
|
+
"@theia/editor": "1.77.0-next.43+5dbb84a9f",
|
|
10
|
+
"@theia/editor-preview": "1.77.0-next.43+5dbb84a9f",
|
|
11
|
+
"@theia/filesystem": "1.77.0-next.43+5dbb84a9f",
|
|
12
|
+
"@theia/monaco": "1.77.0-next.43+5dbb84a9f",
|
|
13
13
|
"@theia/monaco-editor-core": "1.108.201",
|
|
14
|
-
"@theia/preferences": "1.77.0-next.
|
|
15
|
-
"@theia/workspace": "1.77.0-next.
|
|
16
|
-
"date-fns": "^4.4.0",
|
|
14
|
+
"@theia/preferences": "1.77.0-next.43+5dbb84a9f",
|
|
15
|
+
"@theia/workspace": "1.77.0-next.43+5dbb84a9f",
|
|
17
16
|
"mermaid": "^11.17.2",
|
|
18
17
|
"tslib": "^2.8.1"
|
|
19
18
|
},
|
|
@@ -56,5 +55,5 @@
|
|
|
56
55
|
"nyc": {
|
|
57
56
|
"extends": "../../configs/nyc.json"
|
|
58
57
|
},
|
|
59
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "5dbb84a9f43e9287fe3d6f0f52ee1dd2c3c4a20f"
|
|
60
59
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { formatTimeAgo } from './chat-date-utils';
|
|
19
|
+
|
|
20
|
+
describe('formatTimeAgo', () => {
|
|
21
|
+
|
|
22
|
+
// `formatTimeAgo` uses the runtime's default locale when no Theia locale is
|
|
23
|
+
// set (as in tests), so expected strings must be computed with a reference
|
|
24
|
+
// formatter using the same locale instead of being hard-coded.
|
|
25
|
+
const reference = new Intl.RelativeTimeFormat(undefined, { numeric: 'always' });
|
|
26
|
+
|
|
27
|
+
it('uses the largest unit that fits into the elapsed time', () => {
|
|
28
|
+
const cases: ReadonlyArray<readonly [number, Intl.RelativeTimeFormatUnit]> = [
|
|
29
|
+
[2_000, 'second'],
|
|
30
|
+
[2 * 60_000, 'minute'],
|
|
31
|
+
[2 * 3_600_000, 'hour'],
|
|
32
|
+
[2 * 86_400_000, 'day'],
|
|
33
|
+
[2 * 604_800_000, 'week'],
|
|
34
|
+
[2 * 2_629_800_000, 'month'],
|
|
35
|
+
[2 * 31_557_600_000, 'year']
|
|
36
|
+
];
|
|
37
|
+
for (const [elapsed, unit] of cases) {
|
|
38
|
+
const timestamp = Date.now() - elapsed;
|
|
39
|
+
expect(formatTimeAgo(timestamp)).to.equal(reference.format(-2, unit));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('rounds down so the value never crosses into the next larger unit', () => {
|
|
44
|
+
const cases: ReadonlyArray<readonly [number, Intl.RelativeTimeFormatUnit, number]> = [
|
|
45
|
+
[90_000, 'minute', 1], // 1.5 minutes floor to 1
|
|
46
|
+
[59.5 * 60_000, 'minute', 59],
|
|
47
|
+
[23.6 * 3_600_000, 'hour', 23],
|
|
48
|
+
[6.6 * 86_400_000, 'day', 6],
|
|
49
|
+
[11.6 * 2_629_800_000, 'month', 11]
|
|
50
|
+
];
|
|
51
|
+
for (const [elapsed, unit, value] of cases) {
|
|
52
|
+
const timestamp = Date.now() - elapsed;
|
|
53
|
+
expect(formatTimeAgo(timestamp)).to.equal(reference.format(-value, unit));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders the present timestamp as 0 seconds ago', () => {
|
|
58
|
+
expect(formatTimeAgo(Date.now())).to.equal(reference.format(-0, 'second'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders future timestamps as "in ..."', () => {
|
|
62
|
+
const timestamp = Date.now() + 2.5 * 3_600_000; // 2.5 hours floor to 2
|
|
63
|
+
expect(formatTimeAgo(timestamp)).to.equal(reference.format(2, 'hour'));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -15,25 +15,61 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { nls } from '@theia/core';
|
|
18
|
-
import { formatDistance } from 'date-fns';
|
|
19
|
-
import * as locales from 'date-fns/locale';
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
|
-
*
|
|
20
|
+
* The relative time units supported by `formatTimeAgo` with their approximate
|
|
21
|
+
* lengths in milliseconds, from largest to smallest. The first unit that fits
|
|
22
|
+
* into the elapsed time is used.
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
24
|
+
const RELATIVE_TIME_UNITS: ReadonlyArray<readonly [Intl.RelativeTimeFormatUnit, number]> = [
|
|
25
|
+
['year', 31_557_600_000], // 365.25 days
|
|
26
|
+
['month', 2_629_800_000], // 30.44 days
|
|
27
|
+
['week', 604_800_000],
|
|
28
|
+
['day', 86_400_000],
|
|
29
|
+
['hour', 3_600_000],
|
|
30
|
+
['minute', 60_000],
|
|
31
|
+
['second', 1_000]
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
let cachedRelativeTimeFormat: { locale: string | undefined; format: Intl.RelativeTimeFormat } | undefined;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns a cached `Intl.RelativeTimeFormat` for the current Theia locale,
|
|
38
|
+
* falling back to English if the locale is not supported.
|
|
39
|
+
*/
|
|
40
|
+
function getRelativeTimeFormat(): Intl.RelativeTimeFormat {
|
|
41
|
+
// Theia locale ids may use '_' as region separator while `Intl` expects BCP-47 ('-').
|
|
42
|
+
const locale = nls.locale?.replace('_', '-');
|
|
43
|
+
if (cachedRelativeTimeFormat === undefined || cachedRelativeTimeFormat.locale !== locale) {
|
|
44
|
+
try {
|
|
45
|
+
cachedRelativeTimeFormat = { locale, format: new Intl.RelativeTimeFormat(locale, { numeric: 'always' }) };
|
|
46
|
+
} catch {
|
|
47
|
+
cachedRelativeTimeFormat = { locale, format: new Intl.RelativeTimeFormat('en', { numeric: 'always' }) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return cachedRelativeTimeFormat.format;
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
/**
|
|
30
54
|
* Formats a timestamp as a human-readable relative time string (e.g., "2 hours ago").
|
|
55
|
+
*
|
|
56
|
+
* The largest unit that fits into the elapsed time is used and the value is rounded
|
|
57
|
+
* down so that it never crosses into the next larger unit. Timestamps less than a
|
|
58
|
+
* second away render as "0 seconds ago", future ones as "in ...".
|
|
31
59
|
* @param timestamp - The timestamp in milliseconds
|
|
32
|
-
* @param addSuffix - Whether to add "ago" suffix (default: true)
|
|
33
60
|
*/
|
|
34
|
-
export function formatTimeAgo(timestamp: number
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
61
|
+
export function formatTimeAgo(timestamp: number): string {
|
|
62
|
+
if (typeof Intl === 'undefined' || typeof Intl.RelativeTimeFormat === 'undefined') {
|
|
63
|
+
// Environments without `Intl.RelativeTimeFormat` get an absolute date instead.
|
|
64
|
+
return new Date(timestamp).toLocaleString();
|
|
65
|
+
}
|
|
66
|
+
const elapsed = Date.now() - timestamp;
|
|
67
|
+
const direction = elapsed >= 0 ? -1 : 1; // `Intl` expects negative values for the past
|
|
68
|
+
const duration = Math.abs(elapsed);
|
|
69
|
+
for (const [unit, unitLength] of RELATIVE_TIME_UNITS) {
|
|
70
|
+
if (duration >= unitLength) {
|
|
71
|
+
return getRelativeTimeFormat().format(direction * Math.floor(duration / unitLength), unit);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return getRelativeTimeFormat().format(-0, 'second');
|
|
39
75
|
}
|