mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-28 01:56:00 -05:00
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:
commit
c35e3be25b
5 changed files with 141 additions and 11 deletions
|
@ -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
45
src/lib/format-time.js
Normal 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);
|
||||
};
|
|
@ -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'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}
|
||||
|
|
|
@ -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: {
|
||||
|
|
77
test/unit/lib/format-time.test.js
Normal file
77
test/unit/lib/format-time.test.js
Normal 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');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue