Skip to content

Commit

Permalink
timeago: migrate to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
liamwhite committed Apr 16, 2024
1 parent 394c238 commit 0618056
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 15 deletions.
114 changes: 114 additions & 0 deletions assets/js/__tests__/timeago.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
5 changes: 3 additions & 2 deletions assets/js/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 20 additions & 13 deletions assets/js/timeago.js → assets/js/timeago.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand All @@ -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;
7 changes: 7 additions & 0 deletions assets/types/booru-object.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 0618056

Please sign in to comment.