Merge pull request #4635 from picklesrus/readable-time

Add a time formatting utility that formats a relative time in the future.
This commit is contained in:
picklesrus 2020-12-07 16:48:50 -05:00 committed by GitHub
commit c35e3be25b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 11 deletions

View file

@ -57,12 +57,12 @@ class MuteModal extends React.Component {
</p>
</MuteStep>
<MuteStep
header={`For the next ${this.props.timeMuted} you won't be able to post comments.`}
header={`You will be able to comment again ${this.props.timeMuted}.`}
sideImg="/svgs/commenting/mute_time.svg"
sideImgClass="side-img"
>
<p>
Once {this.props.timeMuted} have passed, you will be able to comment again.
Your account has been paused from commenting until then.
</p>
<p>
If you would like more information, you can read

45
src/lib/format-time.js Normal file
View file

@ -0,0 +1,45 @@
/**
Given a timestamp in the future, calculate the largest, closest unit to show.
On the high end we stop at hours. e.g. 15 days is still counted in hours not days or weeks.
On the low end we stop at minutes.
This rounds duration to the nearest integer. e.g. 5.7 minutes => will return 6 as duration.
@param {number} timeStamp A future time stamp in ms.
@returns {object} containing the unit (min, hours) and how many. e.g. {unit: minutes, duration: 3}
*/
const getTimeUnitAndDuration = timeStamp => {
const diff = timeStamp - Date.now();
const oneHourInMs = 1000 * 60 * 60;
const oneMinuteInMs = 1000 * 60;
let unit = 'minute';
let duration = diff / oneMinuteInMs;
// We show minutes up to 2 hours, then switch to hours.
if (diff >= 2 * oneHourInMs) {
unit = 'hour';
duration = diff / oneHourInMs;
}
// Round to nearest hour or minute.
duration = Math.round(duration);
return {
unit: unit,
duration: duration
};
};
/**
* Given a future timestamp and a langauge, constructs a phrase to describe that time relative to now.
* e.g. in 2 days, in 3 minutes, en 2 horas.
* The largest time unit is days, the smallest is minutes.
* @param {number} futureTime a timestamp in ms to build a phrase for.
* @param {string} lang Langauge to build the phrase in.
* @returns {string} A phrase representing the relative time in the future. e.g. 3 days 5 hours.
*/
module.exports.formatRelativeTime = (futureTime, lang) => {
const formatter = new Intl.RelativeTimeFormat([lang].concat(window.navigator.languages), {
localeMatcher: 'best fit',
numeric: 'always',
style: 'long'
});
const timeInfo = getTimeUnitAndDuration(futureTime);
return formatter.format(timeInfo.duration, timeInfo.unit);
};

View file

@ -12,6 +12,7 @@ const InplaceInput = require('../../../components/forms/inplace-input.jsx');
const Button = require('../../../components/forms/button.jsx');
const CommentingStatus = require('../../../components/commenting-status/commenting-status.jsx');
const MuteModal = require('../../../components/modal/mute/modal.jsx');
const formatTime = require('../../../lib/format-time');
const connect = require('react-redux').connect;
@ -79,7 +80,7 @@ class ComposeComment extends React.Component {
let muteExpiresAt = 0;
let rejectedStatus = ComposeStatus.REJECTED;
if (body.status && body.status.mute_status) {
muteExpiresAt = body.status.mute_status.muteExpiresAt;
muteExpiresAt = body.status.mute_status.muteExpiresAt * 1000; // convert to ms
rejectedStatus = ComposeStatus.REJECTED_MUTE;
if (this.shouldShowMuteModal(body.status.mute_status.offenses)) {
muteOpen = true;
@ -170,12 +171,11 @@ class ComposeComment extends React.Component {
{this.isMuted() ? (
<FlexRow className="comment">
<CommentingStatus>
<p>Scratch thinks your most recent comment was disrespectful.</p>
<p>
For the next {this.convertToMinutesFromNow(this.state.muteExpiresAt)} minutes you
won&apos;t be able to post comments.
Once {this.convertToMinutesFromNow(this.state.muteExpiresAt)} minutes have passed,
you will be able to comment again.
<p>Scratch thinks your comment was disrespectful.</p>
<p> You will be able to comment
again {formatTime.formatRelativeTime(this.state.muteExpiresAt, window._locale)}.
Your account has been paused from commenting until then.
</p>
<p className="bottom-text">For more information,
<a
@ -259,7 +259,7 @@ class ComposeComment extends React.Component {
useStandardSizes
className="mod-mute"
shouldCloseOnOverlayClick={false}
timeMuted={`${this.convertToMinutesFromNow(this.state.muteExpiresAt)} minutes`}
timeMuted={formatTime.formatRelativeTime(this.state.muteExpiresAt, window._locale)}
onRequestClose={this.handleMuteClose}
/>
) : null}

View file

@ -3,9 +3,9 @@ const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const ComposeComment = require('../../../src/views/preview/comment/compose-comment.jsx');
import configureStore from 'redux-mock-store';
describe('Compose Comment test', () => {
const mockStore = configureStore();
let _mockFormat;
const defaultProps = () =>({
user: {
thumbnailUrl: 'scratch.mit.edu',
@ -15,6 +15,14 @@ describe('Compose Comment test', () => {
let store;
beforeEach(() => {
const mockFormat = {
format: jest.fn()
};
_mockFormat = Intl.RelativeTimeFormat = jest
.fn()
.mockImplementation(() => mockFormat);
mockFormat.format.mockReturnValue('');
store = mockStore({
session: {
session: {

View file

@ -0,0 +1,77 @@
const format = require('../../../src/lib/format-time');
describe('unit test lib/format-time.js', () => {
let realDateNow;
let _mockFormat;
const mockFormatExpression = {
format: jest.fn()
};
beforeEach(() =>{
realDateNow = Date.now.bind(global.Date);
global.Date.now = () => 0;
_mockFormat = Intl.RelativeTimeFormat = jest
.fn()
.mockImplementation(() => mockFormatExpression);
});
afterEach(()=>{
global.Date.now = realDateNow;
jest.resetAllMocks();
});
test('test timestamp that is 2 minutes in the future', () => {
const twoMin = 2 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 2 minutes');
format.formatRelativeTime(twoMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'minute');
});
test('test rounding timestamp that is 4.4 minutes rounds to 4', () => {
const fourPlusMin = 4.4 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 4 minutes');
format.formatRelativeTime(fourPlusMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'minute');
});
test('test timestamp that is 95.25 minutes in the future', () => {
const ninetyFiveMin = 95.25 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 95 minutes');
format.formatRelativeTime(ninetyFiveMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(95, 'minute');
});
test('test timestamp that is 119 minutes in the future', () => {
const ninetyFiveMin = 119 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 199 minutes');
format.formatRelativeTime(ninetyFiveMin, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(119, 'minute');
});
test('test timestamp that is 48 hours in the future', () => {
const fortyEightHrs = 48 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 48 hours');
format.formatRelativeTime(fortyEightHrs, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(48, 'hour');
});
test('test timestamp that is 2.6 hours rounds to 3', () => {
const twoPlusHours = 2.6 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 3 hours');
format.formatRelativeTime(twoPlusHours, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(3, 'hour');
});
test('test timestamp that is 4.2 hours in the future rounds to 4', () => {
const fourPlusHours = 4.2 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 4 hours');
format.formatRelativeTime(fourPlusHours, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'hour');
});
test('test timestamp that is 2 hours in the future is in hours', () => {
const twoHours = 2 * 60 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 2 hours');
format.formatRelativeTime(twoHours, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'hour');
});
});