src/LiveTimestamp.js

import dayjs from 'dayjs';

import cd from './cd';
import settings from './settings';
import { removeFromArrayIfPresent } from './utils-general';
import { mixEventEmitterIntoObject } from './utils-oojs';
import { formatDate, relativeTimeThresholds } from './utils-timestamp';

/**
 * Class representing an element that has contains an automatically updated timestamp with relative
 * (dependent on the current date and time somehow) date and time.
 */
class LiveTimestamp {
  /**
   * Create a live timestamp.
   *
   * @param {Element} element Element that has the timestamp.
   * @param {Date} date Timestamp's date.
   * @param {boolean} addTimezone Whether to add a timezone to the timestamp.
   */
  constructor(element, date, addTimezone) {
    /**
     * Element that has the timestamp.
     *
     * @type {Element}
     * @private
     */
    this.element = element;

    /**
     * Timestamp's date.
     *
     * @type {Date}
     * @private
     */
    this.date = date;

    /**
     * Whether to add timezone to the timestamp.
     *
     * @type {boolean}
     * @private
     */
    this.addTimezone = addTimezone;

    this.format = settings.get('timestampFormat');
    this.useUiTime = settings.get('useUiTime');
  }

  /**
   * Initialize the timestamp (set the necessary timeouts for the timestamp to be updated when
   * needed).
   */
  init() {
    if (this.format === 'improved') {
      if (!this.constructor.improvedTimestampsInited) {
        // Timestamps of the "improved" format are updated all together, at the boundaries of days.
        // So, we only need to initiate the timeouts once.
        this.constructor.initImproved();
      }
      if (this.date.getTime() > this.constructor.yesterdayStart) {
        this.constructor.improvedTimestamps.push(this);
      }
    } else if (this.format === 'relative') {
      this.setUpdateTimeout();
    }
  }

  /**
   * Set a delay (timeout) until the next timestamp update.
   *
   * @param {boolean} update Whether to update the timestamp now.
   * @private
   */
  setUpdateTimeout(update = false) {
    if (update) {
      this.update();
    }

    const difference = Date.now() - this.date.getTime();
    const threshold = relativeTimeThresholds
      .find((threshold) => difference < threshold.interval * cd.g.msInMin);
    if (threshold) {
      const minSteps = Math.floor((difference / cd.g.msInMin) / threshold.step);
      for (
        let boundary = (threshold.start + (minSteps * threshold.step)) * cd.g.msInMin;
        boundary <= threshold.interval * cd.g.msInMin;
        boundary += threshold.step * cd.g.msInMin
      ) {
        if (difference < boundary) {
          removeFromArrayIfPresent(this.constructor.updateTimeouts, this.updateTimeout);
          this.updateTimeout = setTimeout(() => {
            this.setUpdateTimeout(true);
          }, boundary - difference);
          this.constructor.updateTimeouts.push(this.updateTimeout);
          break;
        }
      }
    }
  }

  /**
   * _For internal use._ Update the timestamp.
   */
  update() {
    this.element.textContent = formatDate(this.date, this.addTimezone);
  }

  static updateTimeouts = [];
  static improvedTimestampsInited = false;
  static improvedTimestamps = [];

  /**
   * Initialize the class (runs once).
   */
  static init() {
    mixEventEmitterIntoObject(this);
  }

  /**
   * _For internal use._ Initialize improved timestamps (when the timestamp format is set to
   * "improved").
   */
  static initImproved() {
    let date = dayjs();
    if (this.useUiTime && !['UTC', 0].includes(cd.g.uiTimezone)) {
      date = typeof cd.g.uiTimezone === 'number' ?
        date.utcOffset(cd.g.uiTimezone) :
        date.tz(cd.g.uiTimezone);
    } else {
      date = date.utc();
    }
    date = date.startOf('day');
    this.yesterdayStart = date.subtract(1, 'day').valueOf();
    const tomorrowStart = date.add(1, 'day').valueOf();
    const dayAfterTomorrowStart = date.add(2, 'day').valueOf();

    const tsDelay = tomorrowStart - Date.now();
    const tsTimeout = setTimeout(this.updateImproved.bind(this), tsDelay);
    const datsDelay = dayAfterTomorrowStart - Date.now();
    const datsTimeout = setTimeout(this.updateImproved.bind(this), datsDelay);
    this.updateTimeouts.push(tsTimeout, datsTimeout);

    this.improvedTimestampsInited = true;
  }

  /**
   * _For internal use._ Update the timestamps (when the timestamp format is set to "improved").
   */
  static updateImproved() {
    this.improvedTimestamps.forEach((timestamp) => {
      timestamp.update();
    });
    this.emit('updateimproved');
  }

  /**
   * Reset all the live timestamps on the page (this is run at every page load).
   */
  static reset() {
    this.updateTimeouts.forEach(clearTimeout);
    this.updateTimeouts = [];
    this.improvedTimestampsInited = false;
    this.improvedTimestamps = [];
  }
}

export default LiveTimestamp;