mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 09:35:56 -05:00
Redo time based on new requirements.
This commit is contained in:
parent
bfc4202d11
commit
b450d36a64
5 changed files with 65 additions and 216 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
|
||||
|
|
|
@ -1,114 +1,45 @@
|
|||
/**
|
||||
Given a timestamp in the future, calculate the largest, closest unit to show.
|
||||
On the high end we stop at days. e.g. 56 days is still counted in days not months or weeks.
|
||||
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, days) and how many. e.g. {unit: minutes, duration: 3}
|
||||
@returns {object} containing the unit (min, hours) and how many. e.g. {unit: minutes, duration: 3}
|
||||
*/
|
||||
const getTopLevelTimeUnit = timeStamp => {
|
||||
const getTimeUnitAndDuration = timeStamp => {
|
||||
const diff = timeStamp - Date.now();
|
||||
const oneDayInMs = 1000 * 60 * 60 * 24;
|
||||
const oneHourInMs = 1000 * 60 * 60;
|
||||
const oneMinuteInMs = 1000 * 60;
|
||||
|
||||
let unit = 'minute';
|
||||
let duration = diff / oneMinuteInMs;
|
||||
if (diff > oneDayInMs) {
|
||||
unit = 'day';
|
||||
duration = diff / oneDayInMs;
|
||||
} else if (diff > oneHourInMs) {
|
||||
// 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
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is used along with Intl.RelativeTimeFormat's formatToParts to extract
|
||||
* the value from a given key.
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/formatToParts
|
||||
* Also see examples in format-time.test.jsx.
|
||||
* @param {string} key The name of the key you want to look up
|
||||
* @param {Array} parts An array of parts from formatToParts.
|
||||
* @returns {string} The value associated with the given key.
|
||||
*/
|
||||
const getValueFromKey = (key, parts) => {
|
||||
for (const item of parts) {
|
||||
if (item.type === key) {
|
||||
return item.value;
|
||||
}
|
||||
}
|
||||
// Just in case key doesn't exist.
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a future timestamp and a langauge, constructs a phrase to describe that time relative to now.
|
||||
* e.g. 2 days 3 hours, 3 minutes, 7 hours 38 minutes, 2 horas 5 minutos.
|
||||
* 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.formatTimeUntil = (futureTime, lang) => {
|
||||
module.exports.formatRelativeTime = (futureTime, lang) => {
|
||||
const formatter = new Intl.RelativeTimeFormat(lang, {
|
||||
localeMatcher: 'best fit',
|
||||
numeric: 'always',
|
||||
style: 'long'
|
||||
});
|
||||
|
||||
const timeInfo = getTopLevelTimeUnit(futureTime);
|
||||
let str = '';
|
||||
const parts = formatter.formatToParts(timeInfo.duration,
|
||||
timeInfo.unit);
|
||||
// This shouldn't happen, but is here for extra safety.
|
||||
if (parts.length === 0) {
|
||||
return '';
|
||||
}
|
||||
// Extract the whole number from formatToParts list.
|
||||
const topLevelValue = getValueFromKey('integer', parts);
|
||||
const units = parts[parts.length - 1].value;
|
||||
|
||||
// Add value and unit to the string. e.g. 3 hours or 2 minutes
|
||||
str = `${topLevelValue} ${units}`;
|
||||
|
||||
// Convert the part after the decimal to a number so we can use it to calculate the next unit
|
||||
const remainder = parseFloat(`.${getValueFromKey('fraction', parts)}`);
|
||||
|
||||
// The smallest unit we show is minutes so we can stop if we're already there
|
||||
// or, our remainder is smaller than the next level down.
|
||||
// e.g. if it is 1 hour 30 sec, we just show 1 hour or
|
||||
// if it is 1 day 45 min, we only show 1 day.
|
||||
if (timeInfo.unit === 'minute' ||
|
||||
(timeInfo.unit === 'hour' && remainder < 1.0 / 60) ||
|
||||
(timeInfo.unit === 'day' && remainder < 1.0 / 24)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Now we need to go figure out what the second part of the string is
|
||||
// e.g. if we're 2 days 3 hours in the future, at this point we have "2 days"
|
||||
// and need to figure out how many hours.
|
||||
let remainingTime = 0;
|
||||
let unitsOfRemainingTime = '';
|
||||
|
||||
if (timeInfo.unit === 'hour') {
|
||||
remainingTime = remainder * 60;
|
||||
unitsOfRemainingTime = 'minute';
|
||||
} else if (timeInfo.unit === 'day') {
|
||||
remainingTime = remainder * 24;
|
||||
unitsOfRemainingTime = 'hour';
|
||||
}
|
||||
|
||||
const remainingParts = formatter.formatToParts(remainingTime, unitsOfRemainingTime);
|
||||
|
||||
// This shouldn't happen, but is here for extra safety.
|
||||
if (remainingParts.length === 0) {
|
||||
return str;
|
||||
}
|
||||
// Concatenate 2nd level value and unit. e.g. add "3 hours" to "1 day" to get "1 day 3 hours".
|
||||
str = `${str} ${getValueFromKey('integer', remainingParts)} ${remainingParts[remainingParts.length - 1].value}`;
|
||||
|
||||
return str;
|
||||
const timeInfo = getTimeUnitAndDuration(futureTime);
|
||||
return formatter.format(timeInfo.duration, timeInfo.unit);
|
||||
};
|
||||
|
|
|
@ -165,11 +165,9 @@ class ComposeComment extends React.Component {
|
|||
<FlexRow className="comment">
|
||||
<CommentingStatus>
|
||||
<p>Scratch thinks your comment was disrespectful.</p>
|
||||
<p>
|
||||
For the next {formatTime.formatTimeUntil(this.state.muteExpiresAt, window._locale)} you
|
||||
won't be able to post comments.
|
||||
Once {formatTime.formatTimeUntil(this.state.muteExpiresAt, window._locale)} have passed,
|
||||
you will be able to comment again.
|
||||
<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
|
||||
|
@ -249,7 +247,7 @@ class ComposeComment extends React.Component {
|
|||
useStandardSizes
|
||||
className="mod-mute"
|
||||
shouldCloseOnOverlayClick={false}
|
||||
timeMuted={formatTime.formatTimeUntil(this.state.muteExpiresAt, window._locale)}
|
||||
timeMuted={formatTime.formatRelativeTime(this.state.muteExpiresAt, window._locale)}
|
||||
onRequestClose={this.handleMuteClose}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -15,14 +15,14 @@ describe('Compose Comment test', () => {
|
|||
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
const mockFormatToParts = {
|
||||
formatToParts: jest.fn()
|
||||
const mockFormat = {
|
||||
format: jest.fn()
|
||||
};
|
||||
_mockFormat = Intl.RelativeTimeFormat = jest
|
||||
.fn()
|
||||
.mockImplementation(() => mockFormatToParts);
|
||||
mockFormatToParts.formatToParts.mockReturnValue([]);
|
||||
|
||||
.mockImplementation(() => mockFormat);
|
||||
mockFormat.format.mockReturnValue('');
|
||||
|
||||
store = mockStore({
|
||||
session: {
|
||||
session: {
|
||||
|
|
|
@ -4,7 +4,7 @@ describe('unit test lib/format-time.js', () => {
|
|||
let realDateNow;
|
||||
let _mockFormat;
|
||||
const mockFormatExpression = {
|
||||
formatToParts: jest.fn()
|
||||
format: jest.fn()
|
||||
};
|
||||
beforeEach(() =>{
|
||||
realDateNow = Date.now.bind(global.Date);
|
||||
|
@ -22,146 +22,66 @@ describe('unit test lib/format-time.js', () => {
|
|||
test('test timestamp that is 2 minutes in the future', () => {
|
||||
let response;
|
||||
const twoMin = 2 * 60 * 1000;
|
||||
const formatToPartsResponse = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '2', unit: 'minute'},
|
||||
{type: 'decimal', value: '.', unit: 'minute'},
|
||||
{type: 'fraction', value: '119', unit: 'minute'},
|
||||
{type: 'literal', value: 'minutes'}
|
||||
];
|
||||
mockFormatExpression.formatToParts.mockReturnValue(formatToPartsResponse);
|
||||
|
||||
response = format.formatTimeUntil(twoMin, 'en');
|
||||
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(2, 'minute');
|
||||
|
||||
expect(response).toEqual('2 minutes');
|
||||
mockFormatExpression.format.mockReturnValue('in 2 minutes');
|
||||
response = format.formatRelativeTime(twoMin, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'minute');
|
||||
expect(response).toEqual('in 2 minutes');
|
||||
});
|
||||
|
||||
test('test timestamp that is 2 hours in the future', () => {
|
||||
test('test rounding timestamp that is 4.4 minutes rounds to 4', () => {
|
||||
let response;
|
||||
const twoHours = 2 * 60 * 60 * 1000;
|
||||
|
||||
const formatToPartsResponse = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '2', unit: 'hours'},
|
||||
{type: 'decimal', value: '.', unit: 'hours'},
|
||||
{type: 'fraction', value: '0', unit: 'hours'},
|
||||
{type: 'literal', value: 'hours'}
|
||||
];
|
||||
mockFormatExpression.formatToParts.mockReturnValue(formatToPartsResponse);
|
||||
response = format.formatTimeUntil(twoHours, 'en');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(2, 'hour');
|
||||
expect(response).toEqual('2 hours');
|
||||
const twoMin = 4.4 * 60 * 1000;
|
||||
mockFormatExpression.format.mockReturnValue('in 4 minutes');
|
||||
response = format.formatRelativeTime(twoMin, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'minute');
|
||||
expect(response).toEqual('in 4 minutes');
|
||||
});
|
||||
|
||||
test('test timestamp that is exactly 2 days in the future', () => {
|
||||
test('test timestamp that is 95.25 minutes in the future', () => {
|
||||
let response;
|
||||
const twoDays = 2 * 60 * 60 * 24 * 1000;
|
||||
const formatToPartsResponse = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '2', unit: 'days'},
|
||||
{type: 'decimal', value: '.', unit: 'days'},
|
||||
{type: 'fraction', value: '0', unit: 'days'},
|
||||
{type: 'literal', value: 'days'}
|
||||
];
|
||||
mockFormatExpression.formatToParts.mockReturnValue(formatToPartsResponse);
|
||||
|
||||
response = format.formatTimeUntil(twoDays, 'en');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(2, 'day');
|
||||
expect(response).toEqual('2 days');
|
||||
const ninetyFiveMin = 95.25 * 60 * 1000;
|
||||
mockFormatExpression.format.mockReturnValue('in 95 minutes');
|
||||
response = format.formatRelativeTime(ninetyFiveMin, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(95, 'minute');
|
||||
expect(response).toEqual('in 95 minutes');
|
||||
});
|
||||
|
||||
|
||||
test('test timestamp that is 2.5 days in the future', () => {
|
||||
test('test timestamp that is 48 hours in the future', () => {
|
||||
let response;
|
||||
const twoDays = 2.5 * 60 * 60 * 24 * 1000;
|
||||
const formatToPartsResponseTwoAndAHalfDays = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '2', unit: 'days'},
|
||||
{type: 'decimal', value: '.', unit: 'days'},
|
||||
{type: 'fraction', value: '5', unit: 'days'},
|
||||
{type: 'literal', value: 'days'}
|
||||
];
|
||||
const formatToPartsResponseTwelveHours = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '12', unit: 'hours'},
|
||||
{type: 'decimal', value: '.', unit: 'hours'},
|
||||
{type: 'fraction', value: '0', unit: 'hours'},
|
||||
{type: 'literal', value: 'hours'}
|
||||
];
|
||||
mockFormatExpression.formatToParts
|
||||
.mockReturnValueOnce(formatToPartsResponseTwoAndAHalfDays)
|
||||
.mockReturnValueOnce(formatToPartsResponseTwelveHours);
|
||||
const fortyEightHrs = 48 * 60 * 60 * 1000;
|
||||
|
||||
response = format.formatTimeUntil(twoDays, 'en');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(2.5, 'day');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(12, 'hour');
|
||||
expect(response).toEqual('2 days 12 hours');
|
||||
mockFormatExpression.format.mockReturnValue('in 48 hours');
|
||||
response = format.formatRelativeTime(fortyEightHrs, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(48, 'hour');
|
||||
expect(response).toEqual('in 48 hours');
|
||||
});
|
||||
|
||||
test('test timestamp that is 3.5 hours in the future', () => {
|
||||
test('test timestamp that is 2.6 hours rounds to 3', () => {
|
||||
let response;
|
||||
const twoDays = 3.5 * 60 * 60 * 1000;
|
||||
const formatToPartsResponseOne = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '3', unit: 'hours'},
|
||||
{type: 'decimal', value: '.', unit: 'hours'},
|
||||
{type: 'fraction', value: '5', unit: 'hours'},
|
||||
{type: 'literal', value: 'hours'}
|
||||
];
|
||||
const formatToPartsResponseTwo = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '30', unit: 'minutes'},
|
||||
{type: 'decimal', value: '.', unit: 'minutes'},
|
||||
{type: 'fraction', value: '0', unit: 'minutes'},
|
||||
{type: 'literal', value: 'minutes'}
|
||||
];
|
||||
mockFormatExpression.formatToParts
|
||||
.mockReturnValueOnce(formatToPartsResponseOne)
|
||||
.mockReturnValueOnce(formatToPartsResponseTwo);
|
||||
const twoPlusHours = 2.6 * 60 * 60 * 1000;
|
||||
|
||||
response = format.formatTimeUntil(twoDays, 'en');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(3.5, 'hour');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(30, 'minute');
|
||||
expect(response).toEqual('3 hours 30 minutes');
|
||||
mockFormatExpression.format.mockReturnValue('in 3 hours');
|
||||
response = format.formatRelativeTime(twoPlusHours, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(3, 'hour');
|
||||
expect(response).toEqual('in 3 hours');
|
||||
});
|
||||
|
||||
test('test timestamp that is 1 day and less than an hour in the future', () => {
|
||||
test('test timestamp that is 4.2 hours in the future rounds to 4', () => {
|
||||
let response;
|
||||
const aDayand10Min = 1.007 * 60 * 60 * 24 * 1000;
|
||||
const formatToPartsResponse = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '1', unit: 'days'},
|
||||
{type: 'decimal', value: '.', unit: 'days'},
|
||||
{type: 'fraction', value: '0.007', unit: 'days'},
|
||||
{type: 'literal', value: 'days'}
|
||||
];
|
||||
|
||||
mockFormatExpression.formatToParts
|
||||
.mockReturnValueOnce(formatToPartsResponse);
|
||||
|
||||
response = format.formatTimeUntil(aDayand10Min, 'en');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(1.007, 'day');
|
||||
expect(response).toEqual('1 days');
|
||||
const fourPlusHours = 4.2 * 60 * 60 * 1000;
|
||||
mockFormatExpression.format.mockReturnValue('in 4 hours');
|
||||
response = format.formatRelativeTime(fourPlusHours, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'hour');
|
||||
expect(response).toEqual('in 4 hours');
|
||||
});
|
||||
|
||||
test('test timestamp that is hour and less than a minute in the future', () => {
|
||||
test('test timestamp that is 2 hours in the future is in hours', () => {
|
||||
let response;
|
||||
const anHourAnd30Sec = 1.008 * 60 * 60 * 1000;
|
||||
const formatToPartsResponse = [
|
||||
{type: 'literal', value: 'in'},
|
||||
{type: 'integer', value: '1', unit: 'hours'},
|
||||
{type: 'decimal', value: '.', unit: 'hours'},
|
||||
{type: 'fraction', value: '0.008', unit: 'hours'},
|
||||
{type: 'literal', value: 'hours'}
|
||||
];
|
||||
const threeHours = 2 * 60 * 60 * 1000;
|
||||
|
||||
mockFormatExpression.formatToParts
|
||||
.mockReturnValueOnce(formatToPartsResponse);
|
||||
|
||||
response = format.formatTimeUntil(anHourAnd30Sec, 'en');
|
||||
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(1.008, 'hour');
|
||||
expect(response).toEqual('1 hours');
|
||||
mockFormatExpression.format.mockReturnValue('in 2 hours');
|
||||
response = format.formatRelativeTime(threeHours, 'en');
|
||||
expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'hour');
|
||||
expect(response).toEqual('in 2 hours');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue