diff --git a/config/jest.config.js b/config/jest.config.js
index ebba9100..84f4d8b9 100644
--- a/config/jest.config.js
+++ b/config/jest.config.js
@@ -1,3 +1,5 @@
+process.env.TZ = 'UTC';
+
module.exports = {
rootDir: '..',
moduleNameMapper: {
diff --git a/package.json b/package.json
index 0d3083dc..de97cf2c 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"i18next": "^19.3.2",
"immutable": "^4.0.0-rc.12",
"lodash": "^4.17.15",
+ "mockdate": "^3.0.2",
"prop-types": "^15.7.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",
diff --git a/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap b/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap
index 0fa34941..74fa4e91 100644
--- a/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap
+++ b/source/components/GlobalNavigation/components/Notifications/__snapshots__/Card.spec.js.snap
@@ -156,6 +156,11 @@ exports[`Card renders correctly with default props 1`] = `
>
@@ -332,6 +337,11 @@ exports[`Card renders correctly with isUnread set to true 1`] = `
>
@@ -513,6 +523,11 @@ exports[`Card renders correctly with no title 1`] = `
>
@@ -672,6 +687,11 @@ exports[`Card renders correctly with no title and announcement type 1`] = `
>
@@ -848,6 +868,11 @@ exports[`Card renders correctly with two actors 1`] = `
>
diff --git a/source/components/Timeago/__snapshots__/index.spec.js.snap b/source/components/Timeago/__snapshots__/index.spec.js.snap
index a0630b40..9f22efe2 100644
--- a/source/components/Timeago/__snapshots__/index.spec.js.snap
+++ b/source/components/Timeago/__snapshots__/index.spec.js.snap
@@ -1,9 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Renders Timeago component 1`] = `
-
- 2/20/2020
-
+ customMessage
+
+`;
+
+exports[`Timeago now should render custom string 2`] = `
+
+`;
+
+exports[`Timeago now should render default string 1`] = `
+
+`;
+
+exports[`Timeago now should render default string 2`] = `
+
`;
diff --git a/source/components/Timeago/index.js b/source/components/Timeago/index.js
index 533150d8..cb451d57 100644
--- a/source/components/Timeago/index.js
+++ b/source/components/Timeago/index.js
@@ -1,70 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
-/* istanbul ignore next */
-function getTimeDistanceString(datetime) {
- const date = new Date(parseInt(datetime, 10));
- const now = Date.now();
- const diffInSeconds = (now - date) / 1000;
+export const SECOND_MILLISECONDS = 1000;
+export const MINUTE_SECONDS = 60;
+export const HOUR_SECONDS = 60 * MINUTE_SECONDS;
+export const DAY_SECONDS = 24 * HOUR_SECONDS;
+export const FIVE_DAYS_SECONDS = 5 * DAY_SECONDS;
- if (diffInSeconds > 432000) {
- // more than 5 days ago - show date
- return date.toLocaleDateString();
- }
-
- if (diffInSeconds > 86400) {
- // more than a day ago
- return `${Math.round(diffInSeconds / 60 / 60 / 24)}d`;
- }
-
- if (diffInSeconds > 3600) {
- // more than an hour ago
- return `${Math.round(diffInSeconds / 60 / 60)}h`;
- }
-
- if (diffInSeconds < 60) {
- // less than a minute ago
- return 'now';
- }
-
- return `${Math.round(diffInSeconds / 60)}m`;
-}
+export const TIMEAGO_SHOW_DATE_THRESHOLD = FIVE_DAYS_SECONDS;
+export const TIMEAGO_NOW_THRESHOLD = MINUTE_SECONDS;
/**
* The Timeago component is a small component that
* shows the number of seconds/minutes/days from given datetime.
- *
- * It all happens after the component is mounted so it's safe to use this
- * component on the Back-End without messing up the hydration.
*/
-export default class Timeago extends React.Component {
- state = {
- display: this.props.datetime,
- };
+export default function Timeago({
+ datetime, renderNow, renderDate, renderDays, renderHours, renderMinutes,
+}) {
+ const date = new Date(datetime);
- static propTypes = {
- datetime: PropTypes.oneOfType([
- PropTypes.instanceOf(Date),
- PropTypes.string,
- ]).isRequired,
- };
+ const withCustomRender = (renderFunction, value, defaultElement = value) => (typeof (renderFunction) === 'function' ? renderFunction(value) : defaultElement);
+
+ const renderTime = () => {
+ const now = Date.now();
+ const diffInSeconds = (now - date.getTime()) / SECOND_MILLISECONDS;
- componentDidMount() {
- const { datetime } = this.props;
+ if (diffInSeconds > TIMEAGO_SHOW_DATE_THRESHOLD) {
+ const dateString = date.toLocaleDateString();
- this.setState({
- display: getTimeDistanceString(datetime),
- });
- }
+ return withCustomRender(renderDate, dateString);
+ }
- render() {
- const { datetime } = this.props;
- const { display } = this.state;
+ if (diffInSeconds > DAY_SECONDS) {
+ // more than a day ago
+ const days = Math.round(diffInSeconds / DAY_SECONDS);
+ return withCustomRender(renderDays, days, `${days}d`);
+ }
- return (
-
- {display}
-
- );
- }
+ if (diffInSeconds > HOUR_SECONDS) {
+ // more than an hour ago
+ const hours = Math.round(diffInSeconds / HOUR_SECONDS);
+ return withCustomRender(renderHours, hours, `${hours}h`);
+ }
+
+ if (diffInSeconds > TIMEAGO_NOW_THRESHOLD) {
+ const minutes = Math.round(diffInSeconds / MINUTE_SECONDS);
+ return withCustomRender(renderMinutes, minutes, `${minutes}m`);
+ }
+
+ // less than a minute ago
+ return typeof (renderNow) === 'function' ? renderNow() : 'now';
+ };
+
+
+ return (
+
+ );
}
+
+Timeago.propTypes = {
+ datetime: PropTypes.oneOfType([
+ PropTypes.instanceOf(Date),
+ PropTypes.string,
+ ]).isRequired,
+ renderDate: PropTypes.func,
+ renderDays: PropTypes.func,
+ renderHours: PropTypes.func,
+ renderMinutes: PropTypes.func,
+ renderNow: PropTypes.func,
+};
+
+Timeago.defaultProps = {
+ renderDays: null,
+ renderHours: null,
+ renderMinutes: null,
+ renderDate: null,
+ renderNow: null,
+};
diff --git a/source/components/Timeago/index.spec.js b/source/components/Timeago/index.spec.js
index b40e6a97..fe2862f8 100644
--- a/source/components/Timeago/index.spec.js
+++ b/source/components/Timeago/index.spec.js
@@ -1,10 +1,165 @@
import React from 'react';
import { shallow } from 'enzyme';
+import MockDate from 'mockdate';
import Timeago from './index';
-test('Renders Timeago component', () => {
- const component = shallow();
+const getNowDate = () => new Date('Tue Sep 17 2018 12:58:43');
- expect(component).toMatchSnapshot();
+const RealDate = Date;
+
+beforeAll(() => {
+ MockDate.set(getNowDate());
+ Date.prototype = Object.assign(Date.prototype, {
+ toLocaleString() {
+ return this.toISOString();
+ },
+ });
+});
+
+afterAll(() => {
+ MockDate.reset();
+ global.Date = RealDate;
+});
+
+describe('Timeago', () => {
+ describe('now', () => {
+ const date = getNowDate();
+ const dataSet = [
+ date,
+ `${date}`,
+ ];
+
+ test('should render default string', () => {
+ dataSet.forEach(datetime => {
+ const component = shallow();
+
+ expect(component.text()).toEqual('now');
+ expect(component).toMatchSnapshot();
+ });
+ });
+
+ dataSet.forEach(datetime => {
+ test('should render custom string', () => {
+ const customMessage = 'customMessage';
+ const renderNow = () => customMessage;
+ const component = shallow();
+
+ expect(component.text()).toEqual(customMessage);
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('minutes ago', () => {
+ const date = getNowDate();
+ date.setMinutes(date.getMinutes() - 5);
+
+ const dataSet = [
+ date,
+ `${date}`,
+ ];
+
+ dataSet.forEach(datetime => {
+ test('should render default string', () => {
+ const component = shallow();
+
+ expect(component.text()).toEqual('5m');
+ });
+ });
+
+ dataSet.forEach(datetime => {
+ test('should render custom string', () => {
+ const renderMinutes = (minutes) => `${minutes} minutes ago`;
+
+ const component = shallow();
+
+ expect(component.text()).toEqual('5 minutes ago');
+ });
+ });
+ });
+
+ describe('hours ago', () => {
+ const date = getNowDate();
+ date.setHours(date.getHours() - 8);
+
+ const dataSet = [
+ date,
+ `${date}`,
+ ];
+
+ dataSet.forEach(datetime => {
+ test('should render default string', () => {
+ const component = shallow();
+
+ expect(component.text()).toEqual('8h');
+ });
+ });
+
+ dataSet.forEach(datetime => {
+ test('should render custom string', () => {
+ const renderHours = (hours) => `${hours} hours ago`;
+
+ const component = shallow();
+
+ expect(component.text()).toEqual('8 hours ago');
+ });
+ });
+ });
+
+ describe('days ago', () => {
+ const date = getNowDate();
+ date.setDate(date.getDate() - 2);
+
+ const dataSet = [
+ date,
+ `${date}`,
+ ];
+
+ dataSet.forEach(datetime => {
+ test('should render default string', () => {
+ const component = shallow();
+
+ expect(component.text()).toEqual('2d');
+ });
+ });
+
+ dataSet.forEach(datetime => {
+ test('should render custom string', () => {
+ const renderDays = (days) => `${days} days ago`;
+
+ const component = shallow();
+
+ expect(component.text()).toEqual('2 days ago');
+ });
+ });
+ });
+
+ describe('date', () => {
+ const date = getNowDate();
+ date.setMonth(date.getMonth() - 3);
+
+ const dataSet = [
+ date,
+ `${date}`,
+ ];
+
+ dataSet.forEach(datetime => {
+ test('should render default string', () => {
+ const component = shallow();
+
+ expect(component.text()).toEqual(date.toLocaleDateString());
+ });
+ });
+
+ dataSet.forEach(datetime => {
+ test('should render custom string', () => {
+ const renderDate = (dateString) => `created on ${dateString}`;
+
+ const component = shallow();
+
+ expect(component.text()).toEqual(`created on ${date.toLocaleDateString()}`);
+ });
+ });
+ });
});
diff --git a/source/utils/timezone.spec.js b/source/utils/timezone.spec.js
new file mode 100644
index 00000000..9692b909
--- /dev/null
+++ b/source/utils/timezone.spec.js
@@ -0,0 +1,5 @@
+describe('Timezone', () => {
+ test('should be UTC', () => {
+ expect(new Date().getTimezoneOffset()).toBe(0);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index d4c0c536..935d9f4c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10058,6 +10058,11 @@ mktemp@~0.4.0:
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws=
+mockdate@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.2.tgz#a5a7bb5820da617747af424d7a4dcb22c6c03d79"
+ integrity sha512-ldfYSUW1ocqSHGTK6rrODUiqAFPGAg0xaHqYJ5tvj1hQyFsjuHpuWgWFTZWwDVlzougN/s2/mhDr8r5nY5xDpA==
+
moo@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"