You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
6.0 KiB

  1. import React from 'react';
  2. import { injectIntl, defineMessages } from 'react-intl';
  3. import PropTypes from 'prop-types';
  4. const messages = defineMessages({
  5. today: { id: 'relative_time.today', defaultMessage: 'today' },
  6. just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
  7. seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
  8. minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
  9. hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
  10. days: { id: 'relative_time.days', defaultMessage: '{number}d' },
  11. moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
  12. seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
  13. minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
  14. hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' },
  15. days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' },
  16. });
  17. const dateFormatOptions = {
  18. hour12: false,
  19. year: 'numeric',
  20. month: 'short',
  21. day: '2-digit',
  22. hour: '2-digit',
  23. minute: '2-digit',
  24. };
  25. const shortDateFormatOptions = {
  26. month: 'short',
  27. day: 'numeric',
  28. };
  29. const SECOND = 1000;
  30. const MINUTE = 1000 * 60;
  31. const HOUR = 1000 * 60 * 60;
  32. const DAY = 1000 * 60 * 60 * 24;
  33. const MAX_DELAY = 2147483647;
  34. const selectUnits = delta => {
  35. const absDelta = Math.abs(delta);
  36. if (absDelta < MINUTE) {
  37. return 'second';
  38. } else if (absDelta < HOUR) {
  39. return 'minute';
  40. } else if (absDelta < DAY) {
  41. return 'hour';
  42. }
  43. return 'day';
  44. };
  45. const getUnitDelay = units => {
  46. switch (units) {
  47. case 'second':
  48. return SECOND;
  49. case 'minute':
  50. return MINUTE;
  51. case 'hour':
  52. return HOUR;
  53. case 'day':
  54. return DAY;
  55. default:
  56. return MAX_DELAY;
  57. }
  58. };
  59. export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
  60. const delta = now - date.getTime();
  61. let relativeTime;
  62. if (delta < DAY && !timeGiven) {
  63. relativeTime = intl.formatMessage(messages.today);
  64. } else if (delta < 10 * SECOND) {
  65. relativeTime = intl.formatMessage(messages.just_now);
  66. } else if (delta < 7 * DAY) {
  67. if (delta < MINUTE) {
  68. relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
  69. } else if (delta < HOUR) {
  70. relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
  71. } else if (delta < DAY) {
  72. relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
  73. } else {
  74. relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
  75. }
  76. } else if (date.getFullYear() === year) {
  77. relativeTime = intl.formatDate(date, shortDateFormatOptions);
  78. } else {
  79. relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' });
  80. }
  81. return relativeTime;
  82. };
  83. const timeRemainingString = (intl, date, now, timeGiven = true) => {
  84. const delta = date.getTime() - now;
  85. let relativeTime;
  86. if (delta < DAY && !timeGiven) {
  87. relativeTime = intl.formatMessage(messages.today);
  88. } else if (delta < 10 * SECOND) {
  89. relativeTime = intl.formatMessage(messages.moments_remaining);
  90. } else if (delta < MINUTE) {
  91. relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) });
  92. } else if (delta < HOUR) {
  93. relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) });
  94. } else if (delta < DAY) {
  95. relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) });
  96. } else {
  97. relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) });
  98. }
  99. return relativeTime;
  100. };
  101. export default @injectIntl
  102. class RelativeTimestamp extends React.Component {
  103. static propTypes = {
  104. intl: PropTypes.object.isRequired,
  105. timestamp: PropTypes.string.isRequired,
  106. year: PropTypes.number.isRequired,
  107. futureDate: PropTypes.bool,
  108. };
  109. state = {
  110. now: this.props.intl.now(),
  111. };
  112. static defaultProps = {
  113. year: (new Date()).getFullYear(),
  114. };
  115. shouldComponentUpdate (nextProps, nextState) {
  116. // As of right now the locale doesn't change without a new page load,
  117. // but we might as well check in case that ever changes.
  118. return this.props.timestamp !== nextProps.timestamp ||
  119. this.props.intl.locale !== nextProps.intl.locale ||
  120. this.state.now !== nextState.now;
  121. }
  122. componentWillReceiveProps (nextProps) {
  123. if (this.props.timestamp !== nextProps.timestamp) {
  124. this.setState({ now: this.props.intl.now() });
  125. }
  126. }
  127. componentDidMount () {
  128. this._scheduleNextUpdate(this.props, this.state);
  129. }
  130. componentWillUpdate (nextProps, nextState) {
  131. this._scheduleNextUpdate(nextProps, nextState);
  132. }
  133. componentWillUnmount () {
  134. clearTimeout(this._timer);
  135. }
  136. _scheduleNextUpdate (props, state) {
  137. clearTimeout(this._timer);
  138. const { timestamp } = props;
  139. const delta = (new Date(timestamp)).getTime() - state.now;
  140. const unitDelay = getUnitDelay(selectUnits(delta));
  141. const unitRemainder = Math.abs(delta % unitDelay);
  142. const updateInterval = 1000 * 10;
  143. const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
  144. this._timer = setTimeout(() => {
  145. this.setState({ now: this.props.intl.now() });
  146. }, delay);
  147. }
  148. render () {
  149. const { timestamp, intl, year, futureDate } = this.props;
  150. const timeGiven = timestamp.includes('T');
  151. const date = new Date(timestamp);
  152. const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
  153. return (
  154. <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
  155. {relativeTime}
  156. </time>
  157. );
  158. }
  159. }