timeago: migrate to TypeScript (#226)

This commit is contained in:
liamwhite 2024-04-16 08:02:12 -04:00 committed by GitHub
parent 3cba72ec4c
commit 33a713310a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 146 additions and 15 deletions

View file

@ -0,0 +1,114 @@
import { timeAgo, setupTimestamps } from '../timeago';
const epochRfc3339 = '1970-01-01T00:00:00.000Z';
describe('Timeago functionality', () => {
// TODO: is this robust? do we need e.g. timekeeper to freeze the time?
function timeAgoWithSecondOffset(offset: number) {
const utc = new Date(new Date().getTime() + offset * 1000).toISOString();
const timeEl = document.createElement('time');
timeEl.setAttribute('datetime', utc);
timeEl.textContent = utc;
timeAgo([timeEl]);
return timeEl.textContent;
}
/* eslint-disable no-implicit-coercion */
it('should parse a time as less than a minute', () => {
expect(timeAgoWithSecondOffset(-15)).toEqual('less than a minute ago');
expect(timeAgoWithSecondOffset(+15)).toEqual('less than a minute from now');
});
it('should parse a time as about a minute', () => {
expect(timeAgoWithSecondOffset(-75)).toEqual('about a minute ago');
expect(timeAgoWithSecondOffset(+75)).toEqual('about a minute from now');
});
it('should parse a time as 30 minutes', () => {
expect(timeAgoWithSecondOffset(-(60 * 30))).toEqual('30 minutes ago');
expect(timeAgoWithSecondOffset(+(60 * 30))).toEqual('30 minutes from now');
});
it('should parse a time as about an hour', () => {
expect(timeAgoWithSecondOffset(-(60 * 60))).toEqual('about an hour ago');
expect(timeAgoWithSecondOffset(+(60 * 60))).toEqual('about an hour from now');
});
it('should parse a time as about 6 hours', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 6))).toEqual('about 6 hours ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 6))).toEqual('about 6 hours from now');
});
it('should parse a time as a day', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 36))).toEqual('a day ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 36))).toEqual('a day from now');
});
it('should parse a time as 25 days', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 24 * 25))).toEqual('25 days ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 24 * 25))).toEqual('25 days from now');
});
it('should parse a time as about a month', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 24 * 35))).toEqual('about a month ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 24 * 35))).toEqual('about a month from now');
});
it('should parse a time as 3 months', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 24 * 30 * 3))).toEqual('3 months ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 24 * 30 * 3))).toEqual('3 months from now');
});
it('should parse a time as about a year', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 24 * 30 * 13))).toEqual('about a year ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 24 * 30 * 13))).toEqual('about a year from now');
});
it('should parse a time as 5 years', () => {
expect(timeAgoWithSecondOffset(-(60 * 60 * 24 * 30 * 12 * 5))).toEqual('5 years ago');
expect(timeAgoWithSecondOffset(+(60 * 60 * 24 * 30 * 12 * 5))).toEqual('5 years from now');
});
/* eslint-enable no-implicit-coercion */
it('should ignore time elements without a datetime attribute', () => {
const timeEl = document.createElement('time');
const value = Math.random().toString();
timeEl.textContent = value;
timeAgo([timeEl]);
expect(timeEl.textContent).toEqual(value);
});
it('should not reset title attribute if it already exists', () => {
const timeEl = document.createElement('time');
const value = Math.random().toString();
timeEl.setAttribute('datetime', epochRfc3339);
timeEl.setAttribute('title', value);
timeAgo([timeEl]);
expect(timeEl.getAttribute('title')).toEqual(value);
expect(timeEl.textContent).not.toEqual(epochRfc3339);
});
});
describe('Automatic timestamps', () => {
it('should process all timestamps in the document', () => {
for (let i = 0; i < 5; i += 1) {
const timeEl = document.createElement('time');
timeEl.setAttribute('datetime', epochRfc3339);
timeEl.textContent = epochRfc3339;
document.documentElement.insertAdjacentElement('beforeend', timeEl);
}
setupTimestamps();
for (const timeEl of document.getElementsByTagName('time')) {
expect(timeEl.textContent).not.toEqual(epochRfc3339);
}
});
});

View file

@ -6,6 +6,7 @@ import { $ } from './utils/dom';
import { showOwnedComments } from './communications/comment';
import { filterNode } from './imagesclientside';
import { fetchHtml } from './utils/requests';
import { timeAgo } from './timeago';
function handleError(response) {
@ -91,7 +92,7 @@ function insertParentPost(data, clickedLink, fullComment) {
fullComment.previousSibling.classList.add('fetched-comment');
// Execute timeago on the new comment - it was not present when first run
window.booru.timeAgo(fullComment.previousSibling.getElementsByTagName('time'));
timeAgo(fullComment.previousSibling.getElementsByTagName('time'));
// Add class active_reply_link to the clicked link
clickedLink.classList.add('active_reply_link');
@ -125,7 +126,7 @@ function displayComments(container, commentsHtml) {
container.innerHTML = commentsHtml;
// Execute timeago on comments
window.booru.timeAgo(document.getElementsByTagName('time'));
timeAgo(document.getElementsByTagName('time'));
// Filter images in the comments
filterNode(container);

View file

@ -2,7 +2,9 @@
* Frontend timestamps.
*/
const strings = {
import { assertNotNull } from './utils/assert';
const strings: Record<string, string> = {
seconds: 'less than a minute',
minute: 'about a minute',
minutes: '%d minutes',
@ -16,16 +18,21 @@ const strings = {
years: '%d years',
};
function distance(time) {
return new Date() - time;
function distance(time: Date) {
return new Date().getTime() - time.getTime();
}
function substitute(key, amount) {
return strings[key].replace('%d', Math.round(amount));
function substitute(key: string, amount: number) {
return strings[key].replace('%d', Math.round(amount).toString());
}
function setTimeAgo(el) {
const date = new Date(el.getAttribute('datetime'));
function setTimeAgo(el: HTMLTimeElement) {
const datetime = el.getAttribute('datetime');
if (!datetime) {
return;
}
const date = new Date(datetime);
const distMillis = distance(date);
const seconds = Math.abs(distMillis) / 1000,
@ -49,20 +56,20 @@ function setTimeAgo(el) {
substitute('years', years);
if (!el.getAttribute('title')) {
el.setAttribute('title', el.textContent);
el.setAttribute('title', assertNotNull(el.textContent));
}
el.textContent = words + (distMillis < 0 ? ' from now' : ' ago');
}
function timeAgo(args) {
[].forEach.call(args, el => setTimeAgo(el));
export function timeAgo(args: HTMLTimeElement[] | HTMLCollectionOf<HTMLTimeElement>) {
for (const el of args) {
setTimeAgo(el);
}
}
function setupTimestamps() {
export function setupTimestamps() {
timeAgo(document.getElementsByTagName('time'));
window.setTimeout(setupTimestamps, 60000);
}
export { setupTimestamps };
window.booru.timeAgo = timeAgo;

View file

@ -2,6 +2,8 @@ import '@testing-library/jest-dom';
import { matchNone } from '../js/query/boolean';
window.booru = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
timeAgo: () => {},
csrfToken: 'mockCsrfToken',
hiddenTag: '/mock-tagblocked.svg',
hiddenTagList: [],

View file

@ -13,6 +13,13 @@ interface Interaction {
}
interface BooruObject {
/**
* Automatic timestamp recalculation function for userscript use
*/
timeAgo: (args: HTMLTimeElement[]) => void;
/**
* Anti-forgery token sent by the server
*/
csrfToken: string;
/**
* One of the specified values, based on user setting