From 3e8989c0b0b3b0e2347f8fd428bf3e2d5f4d3302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 5 May 2024 14:46:22 +0200 Subject: [PATCH] Improve and enable animated number display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/animated-number.tsx | 54 ++++++++++++++----- src/components/status-action-button.tsx | 5 +- .../components/status-interaction-bar.tsx | 5 +- src/utils/numbers.tsx | 2 +- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/components/animated-number.tsx b/src/components/animated-number.tsx index ea5a82411..90672e65e 100644 --- a/src/components/animated-number.tsx +++ b/src/components/animated-number.tsx @@ -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 = ({ value, obfuscate }) => { +const AnimatedNumber: React.FC = ({ value, obfuscate, short }) => { + const intl = useIntl(); const { reduceMotion } = useSettings(); const [direction, setDirection] = useState(1); const [displayedValue, setDisplayedValue] = useState(value); + const [formattedValue, setFormattedValue] = useState(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 = ({ value, obfuscate }) => { const willLeave = () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }); if (reduceMotion) { - return obfuscate - ? <>{obfuscatedCount(displayedValue)} - : ; + 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 = ({ value, obfuscate }) => { key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }} > - {obfuscate - ? obfuscatedCount(data) - : } + {data} ))} diff --git a/src/components/status-action-button.tsx b/src/components/status-action-button.tsx index f6e32d3ea..e93fde172 100644 --- a/src/components/status-action-button.tsx +++ b/src/components/status-action-button.tsx @@ -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 = ({ count = 0 }): JSX return ( - {demetricator && count > 1 ? '1+' : shortNumberFormat(count)} + ); }; diff --git a/src/features/status/components/status-interaction-bar.tsx b/src/features/status/components/status-interaction-bar.tsx index 2e1809a22..0b14ee56b 100644 --- a/src/features/status/components/status-interaction-bar.tsx +++ b/src/features/status/components/status-interaction-bar.tsx @@ -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 = ({ count, children, on const body = ( - {shortNumberFormat(count)} + + {/* {shortNumberFormat(count)} */} diff --git a/src/utils/numbers.tsx b/src/utils/numbers.tsx index 4e3f0f371..72420dd2c 100644 --- a/src/utils/numbers.tsx +++ b/src/utils/numbers.tsx @@ -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); }