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> </p>
</MuteStep> </MuteStep>
<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" sideImg="/svgs/commenting/mute_time.svg"
sideImgClass="side-img" sideImgClass="side-img"
> >
<p> <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>
<p> <p>
If you would like more information, you can read 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. 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. 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. @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 diff = timeStamp - Date.now();
const oneDayInMs = 1000 * 60 * 60 * 24;
const oneHourInMs = 1000 * 60 * 60; const oneHourInMs = 1000 * 60 * 60;
const oneMinuteInMs = 1000 * 60; const oneMinuteInMs = 1000 * 60;
let unit = 'minute'; let unit = 'minute';
let duration = diff / oneMinuteInMs; let duration = diff / oneMinuteInMs;
if (diff > oneDayInMs) { // We show minutes up to 2 hours, then switch to hours.
unit = 'day'; if (diff >= 2 * oneHourInMs) {
duration = diff / oneDayInMs;
} else if (diff > oneHourInMs) {
unit = 'hour'; unit = 'hour';
duration = diff / oneHourInMs; duration = diff / oneHourInMs;
} }
// Round to nearest hour or minute.
duration = Math.round(duration);
return { return {
unit: unit, unit: unit,
duration: duration 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. * 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. * The largest time unit is days, the smallest is minutes.
* @param {number} futureTime a timestamp in ms to build a phrase for. * @param {number} futureTime a timestamp in ms to build a phrase for.
* @param {string} lang Langauge to build the phrase in. * @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. * @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, { const formatter = new Intl.RelativeTimeFormat(lang, {
localeMatcher: 'best fit', localeMatcher: 'best fit',
numeric: 'always', numeric: 'always',
style: 'long' style: 'long'
}); });
const timeInfo = getTimeUnitAndDuration(futureTime);
const timeInfo = getTopLevelTimeUnit(futureTime); return formatter.format(timeInfo.duration, timeInfo.unit);
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;
}; };

View file

@ -165,11 +165,9 @@ class ComposeComment extends React.Component {
<FlexRow className="comment"> <FlexRow className="comment">
<CommentingStatus> <CommentingStatus>
<p>Scratch thinks your comment was disrespectful.</p> <p>Scratch thinks your comment was disrespectful.</p>
<p> <p> You will be able to comment
For the next {formatTime.formatTimeUntil(this.state.muteExpiresAt, window._locale)} you again {formatTime.formatRelativeTime(this.state.muteExpiresAt, window._locale)}.
won&apos;t be able to post comments. Your account has been paused from commenting until then.
Once {formatTime.formatTimeUntil(this.state.muteExpiresAt, window._locale)} have passed,
you will be able to comment again.
</p> </p>
<p className="bottom-text">For more information, <p className="bottom-text">For more information,
<a <a
@ -249,7 +247,7 @@ class ComposeComment extends React.Component {
useStandardSizes useStandardSizes
className="mod-mute" className="mod-mute"
shouldCloseOnOverlayClick={false} shouldCloseOnOverlayClick={false}
timeMuted={formatTime.formatTimeUntil(this.state.muteExpiresAt, window._locale)} timeMuted={formatTime.formatRelativeTime(this.state.muteExpiresAt, window._locale)}
onRequestClose={this.handleMuteClose} onRequestClose={this.handleMuteClose}
/> />
) : null} ) : null}

View file

@ -15,14 +15,14 @@ describe('Compose Comment test', () => {
let store; let store;
beforeEach(() => { beforeEach(() => {
const mockFormatToParts = { const mockFormat = {
formatToParts: jest.fn() format: jest.fn()
}; };
_mockFormat = Intl.RelativeTimeFormat = jest _mockFormat = Intl.RelativeTimeFormat = jest
.fn() .fn()
.mockImplementation(() => mockFormatToParts); .mockImplementation(() => mockFormat);
mockFormatToParts.formatToParts.mockReturnValue([]); mockFormat.format.mockReturnValue('');
store = mockStore({ store = mockStore({
session: { session: {
session: { session: {

View file

@ -4,7 +4,7 @@ describe('unit test lib/format-time.js', () => {
let realDateNow; let realDateNow;
let _mockFormat; let _mockFormat;
const mockFormatExpression = { const mockFormatExpression = {
formatToParts: jest.fn() format: jest.fn()
}; };
beforeEach(() =>{ beforeEach(() =>{
realDateNow = Date.now.bind(global.Date); 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', () => { test('test timestamp that is 2 minutes in the future', () => {
let response; let response;
const twoMin = 2 * 60 * 1000; const twoMin = 2 * 60 * 1000;
const formatToPartsResponse = [ mockFormatExpression.format.mockReturnValue('in 2 minutes');
{type: 'literal', value: 'in'}, response = format.formatRelativeTime(twoMin, 'en');
{type: 'integer', value: '2', unit: 'minute'}, expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'minute');
{type: 'decimal', value: '.', unit: 'minute'}, expect(response).toEqual('in 2 minutes');
{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');
}); });
test('test timestamp that is 2 hours in the future', () => { test('test rounding timestamp that is 4.4 minutes rounds to 4', () => {
let response; let response;
const twoHours = 2 * 60 * 60 * 1000; const twoMin = 4.4 * 60 * 1000;
mockFormatExpression.format.mockReturnValue('in 4 minutes');
const formatToPartsResponse = [ response = format.formatRelativeTime(twoMin, 'en');
{type: 'literal', value: 'in'}, expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'minute');
{type: 'integer', value: '2', unit: 'hours'}, expect(response).toEqual('in 4 minutes');
{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');
}); });
test('test timestamp that is exactly 2 days in the future', () => { test('test timestamp that is 95.25 minutes in the future', () => {
let response; let response;
const twoDays = 2 * 60 * 60 * 24 * 1000; const ninetyFiveMin = 95.25 * 60 * 1000;
const formatToPartsResponse = [ mockFormatExpression.format.mockReturnValue('in 95 minutes');
{type: 'literal', value: 'in'}, response = format.formatRelativeTime(ninetyFiveMin, 'en');
{type: 'integer', value: '2', unit: 'days'}, expect(mockFormatExpression.format).toHaveBeenCalledWith(95, 'minute');
{type: 'decimal', value: '.', unit: 'days'}, expect(response).toEqual('in 95 minutes');
{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');
}); });
test('test timestamp that is 48 hours in the future', () => {
test('test timestamp that is 2.5 days in the future', () => {
let response; let response;
const twoDays = 2.5 * 60 * 60 * 24 * 1000; const fortyEightHrs = 48 * 60 * 60 * 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);
response = format.formatTimeUntil(twoDays, 'en'); mockFormatExpression.format.mockReturnValue('in 48 hours');
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(2.5, 'day'); response = format.formatRelativeTime(fortyEightHrs, 'en');
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(12, 'hour'); expect(mockFormatExpression.format).toHaveBeenCalledWith(48, 'hour');
expect(response).toEqual('2 days 12 hours'); 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; let response;
const twoDays = 3.5 * 60 * 60 * 1000; const twoPlusHours = 2.6 * 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);
response = format.formatTimeUntil(twoDays, 'en'); mockFormatExpression.format.mockReturnValue('in 3 hours');
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(3.5, 'hour'); response = format.formatRelativeTime(twoPlusHours, 'en');
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(30, 'minute'); expect(mockFormatExpression.format).toHaveBeenCalledWith(3, 'hour');
expect(response).toEqual('3 hours 30 minutes'); 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; let response;
const aDayand10Min = 1.007 * 60 * 60 * 24 * 1000; const fourPlusHours = 4.2 * 60 * 60 * 1000;
const formatToPartsResponse = [ mockFormatExpression.format.mockReturnValue('in 4 hours');
{type: 'literal', value: 'in'}, response = format.formatRelativeTime(fourPlusHours, 'en');
{type: 'integer', value: '1', unit: 'days'}, expect(mockFormatExpression.format).toHaveBeenCalledWith(4, 'hour');
{type: 'decimal', value: '.', unit: 'days'}, expect(response).toEqual('in 4 hours');
{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');
}); });
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; let response;
const anHourAnd30Sec = 1.008 * 60 * 60 * 1000; const threeHours = 2 * 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'}
];
mockFormatExpression.formatToParts mockFormatExpression.format.mockReturnValue('in 2 hours');
.mockReturnValueOnce(formatToPartsResponse); response = format.formatRelativeTime(threeHours, 'en');
expect(mockFormatExpression.format).toHaveBeenCalledWith(2, 'hour');
response = format.formatTimeUntil(anHourAnd30Sec, 'en'); expect(response).toEqual('in 2 hours');
expect(mockFormatExpression.formatToParts).toHaveBeenCalledWith(1.008, 'hour');
expect(response).toEqual('1 hours');
}); });
}); });