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.

160 lines
4.2 KiB

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