mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
timeago: migrate to TypeScript (#226)
This commit is contained in:
parent
3cba72ec4c
commit
33a713310a
5 changed files with 146 additions and 15 deletions
114
assets/js/__tests__/timeago.spec.ts
Normal file
114
assets/js/__tests__/timeago.spec.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import { $ } from './utils/dom';
|
||||||
import { showOwnedComments } from './communications/comment';
|
import { showOwnedComments } from './communications/comment';
|
||||||
import { filterNode } from './imagesclientside';
|
import { filterNode } from './imagesclientside';
|
||||||
import { fetchHtml } from './utils/requests';
|
import { fetchHtml } from './utils/requests';
|
||||||
|
import { timeAgo } from './timeago';
|
||||||
|
|
||||||
function handleError(response) {
|
function handleError(response) {
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ function insertParentPost(data, clickedLink, fullComment) {
|
||||||
fullComment.previousSibling.classList.add('fetched-comment');
|
fullComment.previousSibling.classList.add('fetched-comment');
|
||||||
|
|
||||||
// Execute timeago on the new comment - it was not present when first run
|
// 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
|
// Add class active_reply_link to the clicked link
|
||||||
clickedLink.classList.add('active_reply_link');
|
clickedLink.classList.add('active_reply_link');
|
||||||
|
@ -125,7 +126,7 @@ function displayComments(container, commentsHtml) {
|
||||||
container.innerHTML = commentsHtml;
|
container.innerHTML = commentsHtml;
|
||||||
|
|
||||||
// Execute timeago on comments
|
// Execute timeago on comments
|
||||||
window.booru.timeAgo(document.getElementsByTagName('time'));
|
timeAgo(document.getElementsByTagName('time'));
|
||||||
|
|
||||||
// Filter images in the comments
|
// Filter images in the comments
|
||||||
filterNode(container);
|
filterNode(container);
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
* Frontend timestamps.
|
* Frontend timestamps.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const strings = {
|
import { assertNotNull } from './utils/assert';
|
||||||
|
|
||||||
|
const strings: Record<string, string> = {
|
||||||
seconds: 'less than a minute',
|
seconds: 'less than a minute',
|
||||||
minute: 'about a minute',
|
minute: 'about a minute',
|
||||||
minutes: '%d minutes',
|
minutes: '%d minutes',
|
||||||
|
@ -16,16 +18,21 @@ const strings = {
|
||||||
years: '%d years',
|
years: '%d years',
|
||||||
};
|
};
|
||||||
|
|
||||||
function distance(time) {
|
function distance(time: Date) {
|
||||||
return new Date() - time;
|
return new Date().getTime() - time.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
function substitute(key, amount) {
|
function substitute(key: string, amount: number) {
|
||||||
return strings[key].replace('%d', Math.round(amount));
|
return strings[key].replace('%d', Math.round(amount).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTimeAgo(el) {
|
function setTimeAgo(el: HTMLTimeElement) {
|
||||||
const date = new Date(el.getAttribute('datetime'));
|
const datetime = el.getAttribute('datetime');
|
||||||
|
if (!datetime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(datetime);
|
||||||
const distMillis = distance(date);
|
const distMillis = distance(date);
|
||||||
|
|
||||||
const seconds = Math.abs(distMillis) / 1000,
|
const seconds = Math.abs(distMillis) / 1000,
|
||||||
|
@ -49,20 +56,20 @@ function setTimeAgo(el) {
|
||||||
substitute('years', years);
|
substitute('years', years);
|
||||||
|
|
||||||
if (!el.getAttribute('title')) {
|
if (!el.getAttribute('title')) {
|
||||||
el.setAttribute('title', el.textContent);
|
el.setAttribute('title', assertNotNull(el.textContent));
|
||||||
}
|
}
|
||||||
el.textContent = words + (distMillis < 0 ? ' from now' : ' ago');
|
el.textContent = words + (distMillis < 0 ? ' from now' : ' ago');
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(args) {
|
export function timeAgo(args: HTMLTimeElement[] | HTMLCollectionOf<HTMLTimeElement>) {
|
||||||
[].forEach.call(args, el => setTimeAgo(el));
|
for (const el of args) {
|
||||||
|
setTimeAgo(el);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTimestamps() {
|
export function setupTimestamps() {
|
||||||
timeAgo(document.getElementsByTagName('time'));
|
timeAgo(document.getElementsByTagName('time'));
|
||||||
window.setTimeout(setupTimestamps, 60000);
|
window.setTimeout(setupTimestamps, 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupTimestamps };
|
|
||||||
|
|
||||||
window.booru.timeAgo = timeAgo;
|
window.booru.timeAgo = timeAgo;
|
|
@ -2,6 +2,8 @@ import '@testing-library/jest-dom';
|
||||||
import { matchNone } from '../js/query/boolean';
|
import { matchNone } from '../js/query/boolean';
|
||||||
|
|
||||||
window.booru = {
|
window.booru = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
timeAgo: () => {},
|
||||||
csrfToken: 'mockCsrfToken',
|
csrfToken: 'mockCsrfToken',
|
||||||
hiddenTag: '/mock-tagblocked.svg',
|
hiddenTag: '/mock-tagblocked.svg',
|
||||||
hiddenTagList: [],
|
hiddenTagList: [],
|
||||||
|
|
7
assets/types/booru-object.d.ts
vendored
7
assets/types/booru-object.d.ts
vendored
|
@ -13,6 +13,13 @@ interface Interaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BooruObject {
|
interface BooruObject {
|
||||||
|
/**
|
||||||
|
* Automatic timestamp recalculation function for userscript use
|
||||||
|
*/
|
||||||
|
timeAgo: (args: HTMLTimeElement[]) => void;
|
||||||
|
/**
|
||||||
|
* Anti-forgery token sent by the server
|
||||||
|
*/
|
||||||
csrfToken: string;
|
csrfToken: string;
|
||||||
/**
|
/**
|
||||||
* One of the specified values, based on user setting
|
* One of the specified values, based on user setting
|
||||||
|
|
Loading…
Reference in a new issue