Redo time based on new requirements.

This commit is contained in:
picklesrus 2020-11-30 16:16:52 -05:00
parent bfc4202d11
commit b450d36a64
5 changed files with 65 additions and 216 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

View file

@ -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);
};

View file

@ -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&apos;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}

View file

@ -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: {

View file

@ -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');
});
});