Port cb2adaaf9d
to glitch-soc
Signed-off-by: Thibaut Girka <thib@sitedethib.com>
master
parent
c4e1b82caf
commit
a3ac322ded
@ -0,0 +1,62 @@ |
|||||||
|
// @ts-check
|
||||||
|
import React from 'react'; |
||||||
|
import { FormattedMessage } from 'react-intl'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns custom renderer for one of the common counter types |
||||||
|
* |
||||||
|
* @param {"statuses" | "following" | "followers"} counterType |
||||||
|
* Type of the counter |
||||||
|
* @param {boolean} isBold Whether display number must be displayed in bold |
||||||
|
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} |
||||||
|
* Renderer function |
||||||
|
* @throws If counterType is not covered by this function |
||||||
|
*/ |
||||||
|
export function counterRenderer(counterType, isBold = true) { |
||||||
|
/** |
||||||
|
* @type {(displayNumber: JSX.Element) => JSX.Element} |
||||||
|
*/ |
||||||
|
const renderCounter = isBold |
||||||
|
? (displayNumber) => <strong>{displayNumber}</strong> |
||||||
|
: (displayNumber) => displayNumber; |
||||||
|
|
||||||
|
switch (counterType) { |
||||||
|
case 'statuses': { |
||||||
|
return (displayNumber, pluralReady) => ( |
||||||
|
<FormattedMessage |
||||||
|
id='account.statuses_counter' |
||||||
|
defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}' |
||||||
|
values={{ |
||||||
|
count: pluralReady, |
||||||
|
counter: renderCounter(displayNumber), |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
case 'following': { |
||||||
|
return (displayNumber, pluralReady) => ( |
||||||
|
<FormattedMessage |
||||||
|
id='account.following_counter' |
||||||
|
defaultMessage='{count, plural, other {{counter} Following}}' |
||||||
|
values={{ |
||||||
|
count: pluralReady, |
||||||
|
counter: renderCounter(displayNumber), |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
case 'followers': { |
||||||
|
return (displayNumber, pluralReady) => ( |
||||||
|
<FormattedMessage |
||||||
|
id='account.followers_counter' |
||||||
|
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}' |
||||||
|
values={{ |
||||||
|
count: pluralReady, |
||||||
|
counter: renderCounter(displayNumber), |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../util/numbers'; |
||||||
|
import { FormattedMessage, FormattedNumber } from 'react-intl'; |
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** |
||||||
|
* @callback ShortNumberRenderer |
||||||
|
* @param {JSX.Element} displayNumber Number to display |
||||||
|
* @param {number} pluralReady Number used for pluralization |
||||||
|
* @returns {JSX.Element} Final render of number |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {object} ShortNumberProps |
||||||
|
* @property {number} value Number to display in short variant |
||||||
|
* @property {ShortNumberRenderer} [renderer] |
||||||
|
* Custom renderer for numbers, provided as a prop. If another renderer |
||||||
|
* passed as a child of this component, this prop won't be used. |
||||||
|
* @property {ShortNumberRenderer} [children] |
||||||
|
* Custom renderer for numbers, provided as a child. If another renderer |
||||||
|
* passed as a prop of this component, this one will be used instead. |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Component that renders short big number to a shorter version |
||||||
|
* |
||||||
|
* @param {ShortNumberProps} param0 Props for the component |
||||||
|
* @returns {JSX.Element} Rendered number |
||||||
|
*/ |
||||||
|
function ShortNumber({ value, renderer, children }) { |
||||||
|
const shortNumber = toShortNumber(value); |
||||||
|
const [, division] = shortNumber; |
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (children != null && renderer != null) { |
||||||
|
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); |
||||||
|
} |
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
const customRenderer = children != null ? children : renderer; |
||||||
|
|
||||||
|
const displayNumber = <ShortNumberCounter value={shortNumber} />; |
||||||
|
|
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
return customRenderer != null |
||||||
|
? customRenderer(displayNumber, pluralReady(value, division)) |
||||||
|
: displayNumber; |
||||||
|
} |
||||||
|
|
||||||
|
ShortNumber.propTypes = { |
||||||
|
value: PropTypes.number.isRequired, |
||||||
|
renderer: PropTypes.func, |
||||||
|
children: PropTypes.func, |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {object} ShortNumberCounterProps |
||||||
|
* @property {import('../util/number').ShortNumber} value Short number |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Renders short number into corresponding localizable react fragment |
||||||
|
* |
||||||
|
* @param {ShortNumberCounterProps} param0 Props for the component |
||||||
|
* @returns {JSX.Element} FormattedMessage ready to be embedded in code |
||||||
|
*/ |
||||||
|
function ShortNumberCounter({ value }) { |
||||||
|
const [rawNumber, unit, maxFractionDigits = 0] = value; |
||||||
|
|
||||||
|
const count = ( |
||||||
|
<FormattedNumber |
||||||
|
value={rawNumber} |
||||||
|
maximumFractionDigits={maxFractionDigits} |
||||||
|
/> |
||||||
|
); |
||||||
|
|
||||||
|
let values = { count, rawNumber }; |
||||||
|
|
||||||
|
switch (unit) { |
||||||
|
case DECIMAL_UNITS.THOUSAND: { |
||||||
|
return ( |
||||||
|
<FormattedMessage |
||||||
|
id='units.short.thousand' |
||||||
|
defaultMessage='{count}K' |
||||||
|
values={values} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
case DECIMAL_UNITS.MILLION: { |
||||||
|
return ( |
||||||
|
<FormattedMessage |
||||||
|
id='units.short.million' |
||||||
|
defaultMessage='{count}M' |
||||||
|
values={values} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
case DECIMAL_UNITS.BILLION: { |
||||||
|
return ( |
||||||
|
<FormattedMessage |
||||||
|
id='units.short.billion' |
||||||
|
defaultMessage='{count}B' |
||||||
|
values={values} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
// Not sure if we should go farther - @Sasha-Sorokin
|
||||||
|
default: return count; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ShortNumberCounter.propTypes = { |
||||||
|
value: PropTypes.arrayOf(PropTypes.number), |
||||||
|
}; |
||||||
|
|
||||||
|
export default React.memo(ShortNumber); |
@ -1,16 +1,71 @@ |
|||||||
import React, { Fragment } from 'react'; |
// @ts-check
|
||||||
import { FormattedNumber } from 'react-intl'; |
|
||||||
|
export const DECIMAL_UNITS = Object.freeze({ |
||||||
export const shortNumberFormat = number => { |
ONE: 1, |
||||||
if (number < 1000) { |
TEN: 10, |
||||||
return <FormattedNumber value={number} />; |
HUNDRED: Math.pow(10, 2), |
||||||
} else if (number < 10000) { |
THOUSAND: Math.pow(10, 3), |
||||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>; |
MILLION: Math.pow(10, 6), |
||||||
} else if (number < 1000000) { |
BILLION: Math.pow(10, 9), |
||||||
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={0} />K</Fragment>; |
TRILLION: Math.pow(10, 12), |
||||||
} else if (number < 10000000) { |
}); |
||||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>; |
|
||||||
} else { |
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10; |
||||||
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={0} />M</Fragment>; |
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10; |
||||||
|
|
||||||
|
/** |
||||||
|
* @typedef {[number, number, number]} ShortNumber |
||||||
|
* Array of: shorten number, unit of shorten number and maximum fraction digits |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {number} sourceNumber Number to convert to short number |
||||||
|
* @returns {ShortNumber} Calculated short number |
||||||
|
* @example |
||||||
|
* shortNumber(5936); |
||||||
|
* // => [5.936, 1000, 1]
|
||||||
|
*/ |
||||||
|
export function toShortNumber(sourceNumber) { |
||||||
|
if (sourceNumber < DECIMAL_UNITS.THOUSAND) { |
||||||
|
return [sourceNumber, DECIMAL_UNITS.ONE, 0]; |
||||||
|
} else if (sourceNumber < DECIMAL_UNITS.MILLION) { |
||||||
|
return [ |
||||||
|
sourceNumber / DECIMAL_UNITS.THOUSAND, |
||||||
|
DECIMAL_UNITS.THOUSAND, |
||||||
|
sourceNumber < TEN_THOUSAND ? 1 : 0, |
||||||
|
]; |
||||||
|
} else if (sourceNumber < DECIMAL_UNITS.BILLION) { |
||||||
|
return [ |
||||||
|
sourceNumber / DECIMAL_UNITS.MILLION, |
||||||
|
DECIMAL_UNITS.MILLION, |
||||||
|
sourceNumber < TEN_MILLIONS ? 1 : 0, |
||||||
|
]; |
||||||
|
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) { |
||||||
|
return [ |
||||||
|
sourceNumber / DECIMAL_UNITS.BILLION, |
||||||
|
DECIMAL_UNITS.BILLION, |
||||||
|
0, |
||||||
|
]; |
||||||
} |
} |
||||||
}; |
|
||||||
|
return [sourceNumber, DECIMAL_UNITS.ONE, 0]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {number} sourceNumber Original number that is shortened |
||||||
|
* @param {number} division The scale in which short number is displayed |
||||||
|
* @returns {number} Number that can be used for plurals when short form used |
||||||
|
* @example |
||||||
|
* pluralReady(1793, DECIMAL_UNITS.THOUSAND) |
||||||
|
* // => 1790
|
||||||
|
*/ |
||||||
|
export function pluralReady(sourceNumber, division) { |
||||||
|
// eslint-disable-next-line eqeqeq
|
||||||
|
if (division == null || division < DECIMAL_UNITS.HUNDRED) { |
||||||
|
return sourceNumber; |
||||||
|
} |
||||||
|
|
||||||
|
let closestScale = division / DECIMAL_UNITS.TEN; |
||||||
|
|
||||||
|
return Math.trunc(sourceNumber / closestScale) * closestScale; |
||||||
|
} |
||||||
|
Loading…
Reference in new issue