Improve and enable animated number display
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
5a8a13a136
commit
3e8989c0b0
4 changed files with 48 additions and 18 deletions
|
@ -1,36 +1,68 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedNumber } from 'react-intl';
|
||||
import { useIntl, type IntlShape } from 'react-intl';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
import { isNumber, roundDown } from 'soapbox/utils/numbers';
|
||||
|
||||
const obfuscatedCount = (count: number) => {
|
||||
const obfuscatedCount = (count: number): string => {
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
return '0';
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
return count.toString();
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
};
|
||||
|
||||
const shortNumberFormat = (number: any, intl: IntlShape) => {
|
||||
if (!isNumber(number)) return '•';
|
||||
|
||||
let value = number;
|
||||
let factor: string = '';
|
||||
if (number >= 1000 && number < 1000000) {
|
||||
factor = 'k';
|
||||
value = roundDown(value / 1000);
|
||||
} else if (number >= 1000000) {
|
||||
factor = 'M';
|
||||
value = roundDown(value / 1000000);
|
||||
}
|
||||
|
||||
return intl.formatNumber(value, {
|
||||
maximumFractionDigits: 0,
|
||||
minimumFractionDigits: 0,
|
||||
maximumSignificantDigits: 3,
|
||||
numberingSystem: 'latn',
|
||||
style: 'decimal',
|
||||
}) + factor;
|
||||
};
|
||||
|
||||
interface IAnimatedNumber {
|
||||
value: number;
|
||||
obfuscate?: boolean;
|
||||
short?: boolean;
|
||||
}
|
||||
|
||||
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
||||
const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate, short }) => {
|
||||
const intl = useIntl();
|
||||
const { reduceMotion } = useSettings();
|
||||
|
||||
const [direction, setDirection] = useState(1);
|
||||
const [displayedValue, setDisplayedValue] = useState<number>(value);
|
||||
const [formattedValue, setFormattedValue] = useState<string>(intl.formatNumber(value, { numberingSystem: 'latn' }));
|
||||
|
||||
useEffect(() => {
|
||||
if (displayedValue !== undefined) {
|
||||
if (value > displayedValue) setDirection(1);
|
||||
else if (value < displayedValue) setDirection(-1);
|
||||
}
|
||||
|
||||
setDisplayedValue(value);
|
||||
setFormattedValue(obfuscate
|
||||
? obfuscatedCount(value)
|
||||
: short
|
||||
? shortNumberFormat(value, intl)
|
||||
: intl.formatNumber(value, { numberingSystem: 'latn' }));
|
||||
}, [value]);
|
||||
|
||||
const willEnter = () => ({ y: -1 * direction });
|
||||
|
@ -38,14 +70,12 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
|||
const willLeave = () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) });
|
||||
|
||||
if (reduceMotion) {
|
||||
return obfuscate
|
||||
? <>{obfuscatedCount(displayedValue)}</>
|
||||
: <FormattedNumber value={displayedValue} numberingSystem='latn' />;
|
||||
return <>{formattedValue}</>;
|
||||
}
|
||||
|
||||
const styles = [{
|
||||
key: `${displayedValue}`,
|
||||
data: displayedValue,
|
||||
key: `${formattedValue}`,
|
||||
data: formattedValue,
|
||||
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
|
||||
}];
|
||||
|
||||
|
@ -58,9 +88,7 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
|||
key={key}
|
||||
style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}
|
||||
>
|
||||
{obfuscate
|
||||
? obfuscatedCount(data)
|
||||
: <FormattedNumber value={data} numberingSystem='latn' />}
|
||||
{data}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
|
|
|
@ -3,7 +3,8 @@ import React from 'react';
|
|||
|
||||
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
import AnimatedNumber from './animated-number';
|
||||
|
||||
import type { EmojiReaction } from 'soapbox/schemas';
|
||||
|
||||
|
@ -24,7 +25,7 @@ const StatusActionCounter: React.FC<IStatusActionCounter> = ({ count = 0 }): JSX
|
|||
|
||||
return (
|
||||
<Text size='xs' weight='semibold' theme='inherit'>
|
||||
{demetricator && count > 1 ? '1+' : shortNumberFormat(count)}
|
||||
<AnimatedNumber value={count} obfuscate={demetricator} short />
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,10 +3,10 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||
import { HStack, Text, Emoji } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useSoapboxConfig, useFeatures, useAppDispatch } from 'soapbox/hooks';
|
||||
import { reduceEmoji } from 'soapbox/utils/emoji-reacts';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -218,7 +218,8 @@ const InteractionCounter: React.FC<IInteractionCounter> = ({ count, children, on
|
|||
const body = (
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text weight='bold'>
|
||||
{shortNumberFormat(count)}
|
||||
<AnimatedNumber value={count} short />
|
||||
{/* {shortNumberFormat(count)} */}
|
||||
</Text>
|
||||
|
||||
<Text tag='div' theme='muted'>
|
||||
|
|
|
@ -10,7 +10,7 @@ export const realNumberSchema = z.coerce.number().refine(n => !isNaN(n));
|
|||
|
||||
export const secondsToDays = (seconds: number) => Math.floor(seconds / (3600 * 24));
|
||||
|
||||
const roundDown = (num: number) => {
|
||||
export const roundDown = (num: number) => {
|
||||
if (num >= 100 && num < 1000) {
|
||||
num = Math.floor(num);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue