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.

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