diff --git a/addons/core/addon/components/relative-datetime-live/index.hbs b/addons/core/addon/components/relative-datetime-live/index.hbs new file mode 100644 index 000000000..ffa6fc83c --- /dev/null +++ b/addons/core/addon/components/relative-datetime-live/index.hbs @@ -0,0 +1 @@ +{{this.formatted}} diff --git a/addons/core/addon/components/relative-datetime-live/index.js b/addons/core/addon/components/relative-datetime-live/index.js new file mode 100644 index 000000000..7dcdade2f --- /dev/null +++ b/addons/core/addon/components/relative-datetime-live/index.js @@ -0,0 +1,100 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +const MILLISECOND = 1; +const SECOND = MILLISECOND * 1000; +const MINUTE = SECOND * 60; +const HOUR = MINUTE * 60; +const DAY = HOUR * 24; +const MONTH = DAY * 31; +const YEAR = MONTH * 12; + +// common time intervals in milliseconds +const unitMappings = [ + { + unit: 'second', + value: SECOND, + }, + { + unit: 'minute', + value: MINUTE, + }, + { + unit: 'hour', + value: HOUR, + }, + { + unit: 'day', + value: DAY, + }, + { + unit: 'month', + value: MONTH, + }, + { + unit: 'year', + value: YEAR, + }, +]; + +export default class RelativeDatetimeLiveComponent extends Component { + // =services + + @service clockTick; + @service intl; + + // =properties + + /** + * The difference in milliseconds between the date argument and the + * current time. + * @type {number} + */ + get delta() { + return this.args.date.valueOf() - this.clockTick.now; + } + + /** + * The greatest meaningful unit is the unit which delta is equal to or + * greater than. For example, the unit of a delta equivalent of 59 seconds is + * 'second' while 60 seconds is 'minute', 36 hours is 'day', etc. + * @type {object} + */ + get greatestMeaningfulUnit() { + return unitMappings.reduce((currentValue, previousValue) => { + return Math.abs(this.delta) >= previousValue.value + ? previousValue + : currentValue; + }, unitMappings[0]); + } + + /** + * The delta value scaled to the greatest meaningful unit. For example, if + * the delta is 1000 ms, the scaled value is 1, which corresponds to + * a GMU of 'second'. A delta of 60000 ms is also 1, corresponding to a GMU + * of 'minute'. + * @type {number} + */ + get scaledDelta() { + if (this.delta < 0) { + return Math.ceil(this.delta / this.greatestMeaningfulUnit.value); + } + return Math.floor(this.delta / this.greatestMeaningfulUnit.value); + } + + /** + * A string representation of the greatest meaningful unit. + * @type {string} + */ + get unit() { + return this.greatestMeaningfulUnit.unit; + } + + /** + * A localizd relative datetime. E.g. "30 seconds ago". + * @type {string} + */ + get formatted() { + return this.intl.formatRelative(this.scaledDelta, { unit: this.unit }); + } +} diff --git a/addons/core/addon/services/clock-tick.js b/addons/core/addon/services/clock-tick.js new file mode 100644 index 000000000..6c64e9b05 --- /dev/null +++ b/addons/core/addon/services/clock-tick.js @@ -0,0 +1,78 @@ +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { getOwner } from '@ember/application'; +import { next } from '@ember/runloop'; + +export default class ClockTickService extends Service { + // =attributes + + /** + * The current datetime. This is updated once every `interval` while the + * service is running (1s by default). + * @type {number} + */ + @tracked now = Date.now(); + + /** + * The frequency to update `now`, in milliseconds. + * @type {number} + */ + frequency = 1000; + + /** + * The timer interval ID + */ + #timer; + + /** + * This service is enabled only in non-test environments. + * @type {boolean} + */ + get enabled() { + const config = getOwner(this).resolveRegistration('config:environment'); + return config.environment !== 'test'; + } + + // =methods + + /** + * Starts the timer, if enabled. + */ + constructor() { + super(...arguments); + if (this.enabled) this._start(); + } + + /** + * Stops the timer. + */ + willDestroy() { + super.willDestroy(...arguments); + this._stop(); + } + + /** + * Sets up the timer interval, calling `_tick()` every + * `frequency` milliseconds. + */ + _start() { + const self = this; + this.#timer = setInterval(function () { + self._tick(); + }, this.frequency); + } + + /** + * Clears the timer interval. + */ + _stop() { + clearInterval(this.#timer); + } + + /** + * Updates the value of `now` within the next runloop. + */ + _tick() { + next(() => (this.now = Date.now())); + } +} diff --git a/addons/core/app/components/relative-datetime-live.js b/addons/core/app/components/relative-datetime-live.js new file mode 100644 index 000000000..c338a513a --- /dev/null +++ b/addons/core/app/components/relative-datetime-live.js @@ -0,0 +1 @@ +export { default } from 'core/components/relative-datetime-live'; diff --git a/addons/core/app/services/clock-tick.js b/addons/core/app/services/clock-tick.js new file mode 100644 index 000000000..a09c9bc53 --- /dev/null +++ b/addons/core/app/services/clock-tick.js @@ -0,0 +1 @@ +export { default } from 'core/services/clock-tick'; diff --git a/addons/core/tests/dummy/app/controllers/application.js b/addons/core/tests/dummy/app/controllers/application.js new file mode 100644 index 000000000..9cecc9ac3 --- /dev/null +++ b/addons/core/tests/dummy/app/controllers/application.js @@ -0,0 +1,12 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ApplicationController extends Controller { + // =services + + @service clockTick; + + // =properties + + now = new Date(); +} diff --git a/addons/core/tests/dummy/app/templates/application.hbs b/addons/core/tests/dummy/app/templates/application.hbs index 9c8163e59..0b957c048 100644 --- a/addons/core/tests/dummy/app/templates/application.hbs +++ b/addons/core/tests/dummy/app/templates/application.hbs @@ -1,3 +1,3 @@