diff options
-rw-r--r-- | src/lib/custom-animations/CountWheel.tsx | 177 | ||||
-rw-r--r-- | src/lib/custom-animations/CountWheel.web.tsx | 121 | ||||
-rw-r--r-- | src/lib/custom-animations/LikeIcon.tsx | 139 | ||||
-rw-r--r-- | src/lib/custom-animations/LikeIcon.web.tsx | 115 | ||||
-rw-r--r-- | src/lib/custom-animations/util.ts | 21 | ||||
-rw-r--r-- | src/view/com/util/post-ctrls/PostCtrls.tsx | 254 |
6 files changed, 580 insertions, 247 deletions
diff --git a/src/lib/custom-animations/CountWheel.tsx b/src/lib/custom-animations/CountWheel.tsx new file mode 100644 index 000000000..dfa697911 --- /dev/null +++ b/src/lib/custom-animations/CountWheel.tsx @@ -0,0 +1,177 @@ +import React from 'react' +import {View} from 'react-native' +import Animated, { + Easing, + LayoutAnimationConfig, + useReducedMotion, + withTiming, +} from 'react-native-reanimated' +import {i18n} from '@lingui/core' + +import {decideShouldRoll} from 'lib/custom-animations/util' +import {s} from 'lib/styles' +import {formatCount} from 'view/com/util/numeric/format' +import {Text} from 'view/com/util/text/Text' +import {atoms as a, useTheme} from '#/alf' + +const animationConfig = { + duration: 400, + easing: Easing.out(Easing.cubic), +} + +function EnteringUp() { + 'worklet' + const animations = { + opacity: withTiming(1, animationConfig), + transform: [{translateY: withTiming(0, animationConfig)}], + } + const initialValues = { + opacity: 0, + transform: [{translateY: 18}], + } + return { + animations, + initialValues, + } +} + +function EnteringDown() { + 'worklet' + const animations = { + opacity: withTiming(1, animationConfig), + transform: [{translateY: withTiming(0, animationConfig)}], + } + const initialValues = { + opacity: 0, + transform: [{translateY: -18}], + } + return { + animations, + initialValues, + } +} + +function ExitingUp() { + 'worklet' + const animations = { + opacity: withTiming(0, animationConfig), + transform: [ + { + translateY: withTiming(-18, animationConfig), + }, + ], + } + const initialValues = { + opacity: 1, + transform: [{translateY: 0}], + } + return { + animations, + initialValues, + } +} + +function ExitingDown() { + 'worklet' + const animations = { + opacity: withTiming(0, animationConfig), + transform: [{translateY: withTiming(18, animationConfig)}], + } + const initialValues = { + opacity: 1, + transform: [{translateY: 0}], + } + return { + animations, + initialValues, + } +} + +export function CountWheel({ + likeCount, + big, + isLiked, +}: { + likeCount: number + big?: boolean + isLiked: boolean +}) { + const t = useTheme() + const shouldAnimate = !useReducedMotion() + const shouldRoll = decideShouldRoll(isLiked, likeCount) + + // Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting + // animation + // The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would + // be unnecessary + const [key, setKey] = React.useState(0) + const [prevCount, setPrevCount] = React.useState(likeCount) + const prevIsLiked = React.useRef(isLiked) + const formattedCount = formatCount(i18n, likeCount) + const formattedPrevCount = formatCount(i18n, prevCount) + + React.useEffect(() => { + if (isLiked === prevIsLiked.current) { + return + } + + const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1 + setKey(prev => prev + 1) + setPrevCount(newPrevCount) + prevIsLiked.current = isLiked + }, [isLiked, likeCount]) + + const enteringAnimation = + shouldAnimate && shouldRoll + ? isLiked + ? EnteringUp + : EnteringDown + : undefined + const exitingAnimation = + shouldAnimate && shouldRoll + ? isLiked + ? ExitingUp + : ExitingDown + : undefined + + return ( + <LayoutAnimationConfig skipEntering skipExiting> + {likeCount > 0 ? ( + <View style={[a.justify_center]}> + <Animated.View entering={enteringAnimation} key={key}> + <Text + testID="likeCount" + style={[ + big ? a.text_md : {fontSize: 15}, + a.user_select_none, + isLiked + ? [a.font_bold, s.likeColor] + : {color: t.palette.contrast_500}, + ]}> + {formattedCount} + </Text> + </Animated.View> + {shouldAnimate ? ( + <Animated.View + entering={exitingAnimation} + // Add 2 to the key so there are never duplicates + key={key + 2} + style={[a.absolute, {width: 50}]} + aria-disabled={true}> + <Text + style={[ + big ? a.text_md : {fontSize: 15}, + a.user_select_none, + isLiked + ? [a.font_bold, s.likeColor] + : {color: t.palette.contrast_500}, + ]}> + {formattedPrevCount} + </Text> + </Animated.View> + ) : null} + </View> + ) : null} + </LayoutAnimationConfig> + ) +} diff --git a/src/lib/custom-animations/CountWheel.web.tsx b/src/lib/custom-animations/CountWheel.web.tsx new file mode 100644 index 000000000..618dcb1a5 --- /dev/null +++ b/src/lib/custom-animations/CountWheel.web.tsx @@ -0,0 +1,121 @@ +import React from 'react' +import {View} from 'react-native' +import {useReducedMotion} from 'react-native-reanimated' +import {i18n} from '@lingui/core' + +import {decideShouldRoll} from 'lib/custom-animations/util' +import {s} from 'lib/styles' +import {formatCount} from 'view/com/util/numeric/format' +import {Text} from 'view/com/util/text/Text' +import {atoms as a, useTheme} from '#/alf' + +const animationConfig = { + duration: 400, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + fill: 'forwards' as FillMode, +} + +const enteringUpKeyframe = [ + {opacity: 0, transform: 'translateY(18px)'}, + {opacity: 1, transform: 'translateY(0)'}, +] + +const enteringDownKeyframe = [ + {opacity: 0, transform: 'translateY(-18px)'}, + {opacity: 1, transform: 'translateY(0)'}, +] + +const exitingUpKeyframe = [ + {opacity: 1, transform: 'translateY(0)'}, + {opacity: 0, transform: 'translateY(-18px)'}, +] + +const exitingDownKeyframe = [ + {opacity: 1, transform: 'translateY(0)'}, + {opacity: 0, transform: 'translateY(18px)'}, +] + +export function CountWheel({ + likeCount, + big, + isLiked, +}: { + likeCount: number + big?: boolean + isLiked: boolean +}) { + const t = useTheme() + const shouldAnimate = !useReducedMotion() + const shouldRoll = decideShouldRoll(isLiked, likeCount) + + const countView = React.useRef<HTMLDivElement>(null) + const prevCountView = React.useRef<HTMLDivElement>(null) + + const [prevCount, setPrevCount] = React.useState(likeCount) + const prevIsLiked = React.useRef(isLiked) + const formattedCount = formatCount(i18n, likeCount) + const formattedPrevCount = formatCount(i18n, prevCount) + + React.useEffect(() => { + if (isLiked === prevIsLiked.current) { + return + } + + const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1 + if (shouldAnimate && shouldRoll) { + countView.current?.animate?.( + isLiked ? enteringUpKeyframe : enteringDownKeyframe, + animationConfig, + ) + prevCountView.current?.animate?.( + isLiked ? exitingUpKeyframe : exitingDownKeyframe, + animationConfig, + ) + setPrevCount(newPrevCount) + } + prevIsLiked.current = isLiked + }, [isLiked, likeCount, shouldAnimate, shouldRoll]) + + if (likeCount < 1) { + return null + } + + return ( + <View> + <View + aria-disabled={true} + // @ts-expect-error is div + ref={countView}> + <Text + testID="likeCount" + style={[ + big ? a.text_md : {fontSize: 15}, + a.user_select_none, + isLiked + ? [a.font_bold, s.likeColor] + : {color: t.palette.contrast_500}, + ]}> + {formattedCount} + </Text> + </View> + {shouldAnimate ? ( + <View + style={{position: 'absolute'}} + aria-disabled={true} + // @ts-expect-error is div + ref={prevCountView}> + <Text + style={[ + big ? a.text_md : {fontSize: 15}, + a.user_select_none, + isLiked + ? [a.font_bold, s.likeColor] + : {color: t.palette.contrast_500}, + ]}> + {formattedPrevCount} + </Text> + </View> + ) : null} + </View> + ) +} diff --git a/src/lib/custom-animations/LikeIcon.tsx b/src/lib/custom-animations/LikeIcon.tsx new file mode 100644 index 000000000..06d5c2850 --- /dev/null +++ b/src/lib/custom-animations/LikeIcon.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import {View} from 'react-native' +import Animated, { + Keyframe, + LayoutAnimationConfig, + useReducedMotion, +} from 'react-native-reanimated' + +import {s} from 'lib/styles' +import {useTheme} from '#/alf' +import { + Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, + Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, +} from '#/components/icons/Heart2' + +const keyframe = new Keyframe({ + 0: { + transform: [{scale: 1}], + }, + 10: { + transform: [{scale: 0.7}], + }, + 40: { + transform: [{scale: 1.2}], + }, + 100: { + transform: [{scale: 1}], + }, +}) + +const circle1Keyframe = new Keyframe({ + 0: { + opacity: 0, + transform: [{scale: 0}], + }, + 10: { + opacity: 0.4, + }, + 40: { + transform: [{scale: 1.5}], + }, + 95: { + opacity: 0.4, + }, + 100: { + opacity: 0, + transform: [{scale: 1.5}], + }, +}) + +const circle2Keyframe = new Keyframe({ + 0: { + opacity: 0, + transform: [{scale: 0}], + }, + 10: { + opacity: 1, + }, + 40: { + transform: [{scale: 0}], + }, + 95: { + opacity: 1, + }, + 100: { + opacity: 0, + transform: [{scale: 1.5}], + }, +}) + +export function AnimatedLikeIcon({ + isLiked, + big, +}: { + isLiked: boolean + big?: boolean +}) { + const t = useTheme() + const size = big ? 22 : 18 + const shouldAnimate = !useReducedMotion() + + return ( + <View> + <LayoutAnimationConfig skipEntering> + {isLiked ? ( + <Animated.View + entering={shouldAnimate ? keyframe.duration(300) : undefined}> + <HeartIconFilled style={s.likeColor} width={size} /> + </Animated.View> + ) : ( + <HeartIconOutline + style={[{color: t.palette.contrast_500}, {pointerEvents: 'none'}]} + width={size} + /> + )} + {isLiked ? ( + <> + <Animated.View + entering={ + shouldAnimate ? circle1Keyframe.duration(300) : undefined + } + style={[ + { + position: 'absolute', + backgroundColor: s.likeColor.color, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + }, + ]} + /> + <Animated.View + entering={ + shouldAnimate ? circle2Keyframe.duration(300) : undefined + } + style={[ + { + position: 'absolute', + backgroundColor: t.atoms.bg.backgroundColor, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + }, + ]} + /> + </> + ) : null} + </LayoutAnimationConfig> + </View> + ) +} diff --git a/src/lib/custom-animations/LikeIcon.web.tsx b/src/lib/custom-animations/LikeIcon.web.tsx new file mode 100644 index 000000000..c131dcf67 --- /dev/null +++ b/src/lib/custom-animations/LikeIcon.web.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import {View} from 'react-native' +import {useReducedMotion} from 'react-native-reanimated' + +import {s} from 'lib/styles' +import {useTheme} from '#/alf' +import { + Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, + Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, +} from '#/components/icons/Heart2' + +const animationConfig = { + duration: 400, + easing: 'cubic-bezier(0.4, 0, 0.2, 1)', + fill: 'forwards' as FillMode, +} + +const keyframe = [ + {transform: 'scale(1)'}, + {transform: 'scale(0.7)'}, + {transform: 'scale(1.2)'}, + {transform: 'scale(1)'}, +] + +const circle1Keyframe = [ + {opacity: 0, transform: 'scale(0)'}, + {opacity: 0.4}, + {transform: 'scale(1.5)'}, + {opacity: 0.4}, + {opacity: 0, transform: 'scale(1.5)'}, +] + +const circle2Keyframe = [ + {opacity: 0, transform: 'scale(0)'}, + {opacity: 1}, + {transform: 'scale(0)'}, + {opacity: 1}, + {opacity: 0, transform: 'scale(1.5)'}, +] + +export function AnimatedLikeIcon({ + isLiked, + big, +}: { + isLiked: boolean + big?: boolean +}) { + const t = useTheme() + const size = big ? 22 : 18 + const shouldAnimate = !useReducedMotion() + const prevIsLiked = React.useRef(isLiked) + + const likeIconRef = React.useRef<HTMLDivElement>(null) + const circle1Ref = React.useRef<HTMLDivElement>(null) + const circle2Ref = React.useRef<HTMLDivElement>(null) + + React.useEffect(() => { + if (prevIsLiked.current === isLiked) { + return + } + + if (shouldAnimate && isLiked) { + likeIconRef.current?.animate?.(keyframe, animationConfig) + circle1Ref.current?.animate?.(circle1Keyframe, animationConfig) + circle2Ref.current?.animate?.(circle2Keyframe, animationConfig) + } + prevIsLiked.current = isLiked + }, [shouldAnimate, isLiked]) + + return ( + <View> + {isLiked ? ( + // @ts-expect-error is div + <View ref={likeIconRef}> + <HeartIconFilled style={s.likeColor} width={size} /> + </View> + ) : ( + <HeartIconOutline + style={[{color: t.palette.contrast_500}, {pointerEvents: 'none'}]} + width={size} + /> + )} + <View + // @ts-expect-error is div + ref={circle1Ref} + style={{ + position: 'absolute', + backgroundColor: s.likeColor.color, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + }} + /> + <View + // @ts-expect-error is div + ref={circle2Ref} + style={{ + position: 'absolute', + backgroundColor: t.atoms.bg.backgroundColor, + top: 0, + left: 0, + width: size, + height: size, + zIndex: -1, + pointerEvents: 'none', + borderRadius: size / 2, + }} + /> + </View> + ) +} diff --git a/src/lib/custom-animations/util.ts b/src/lib/custom-animations/util.ts new file mode 100644 index 000000000..0aebab57b --- /dev/null +++ b/src/lib/custom-animations/util.ts @@ -0,0 +1,21 @@ +// It should roll when: +// - We're going from 1 to 0 (roll backwards) +// - The count is anywhere between 1 and 999 +// - The count is going up and is a multiple of 100 +// - The count is going down and is 1 less than a multiple of 100 +export function decideShouldRoll(isSet: boolean, count: number) { + let shouldRoll = false + if (!isSet && count === 0) { + shouldRoll = true + } else if (count > 0 && count < 1000) { + shouldRoll = true + } else if (count > 0) { + const mod = count % 100 + if (isSet && mod === 0) { + shouldRoll = true + } else if (!isSet && mod === 99) { + shouldRoll = true + } + } + return shouldRoll +} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 49c9229a6..6a58a5624 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -6,14 +6,6 @@ import { View, type ViewStyle, } from 'react-native' -import Animated, { - Easing, - interpolate, - SharedValue, - useAnimatedStyle, - useSharedValue, - withTiming, -} from 'react-native-reanimated' import * as Clipboard from 'expo-clipboard' import { AppBskyFeedDefs, @@ -31,8 +23,6 @@ import {makeProfileLink} from '#/lib/routes/links' import {shareUrl} from '#/lib/sharing' import {useGate} from '#/lib/statsig/statsig' import {toShareUrl} from '#/lib/strings/url-helpers' -import {s} from '#/lib/styles' -import {isWeb} from '#/platform/detection' import {Shadow} from '#/state/cache/types' import {useFeedFeedbackContext} from '#/state/feed-feedback' import { @@ -45,16 +35,13 @@ import { ProgressGuideAction, useProgressGuideControls, } from '#/state/shell/progress-guide' +import {CountWheel} from 'lib/custom-animations/CountWheel' +import {AnimatedLikeIcon} from 'lib/custom-animations/LikeIcon' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' -import { - Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, - Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, -} from '#/components/icons/Heart2' import * as Prompt from '#/components/Prompt' -import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' import {PostDropdownBtn} from '../forms/PostDropdownBtn' import {formatCount} from '../numeric/format' import {Text} from '../text/Text' @@ -120,17 +107,7 @@ let PostCtrls = ({ ) as StyleProp<ViewStyle> const likeValue = post.viewer?.like ? 1 : 0 - const likeIconAnimValue = useSharedValue(likeValue) - const likeTextAnimValue = useSharedValue(likeValue) const nextExpectedLikeValue = React.useRef(likeValue) - React.useEffect(() => { - // Catch nonlocal changes (e.g. shadow update) and always reflect them. - if (likeValue !== nextExpectedLikeValue.current) { - nextExpectedLikeValue.current = likeValue - likeIconAnimValue.value = likeValue - likeTextAnimValue.value = likeValue - } - }, [likeValue, likeIconAnimValue, likeTextAnimValue]) const onPressToggleLike = React.useCallback(async () => { if (isBlocked) { @@ -144,19 +121,6 @@ let PostCtrls = ({ try { if (!post.viewer?.like) { nextExpectedLikeValue.current = 1 - if (PlatformInfo.getIsReducedMotionEnabled()) { - likeIconAnimValue.value = 1 - likeTextAnimValue.value = 1 - } else { - likeIconAnimValue.value = withTiming(1, { - duration: 400, - easing: Easing.out(Easing.cubic), - }) - likeTextAnimValue.value = withTiming(1, { - duration: 400, - easing: Easing.out(Easing.cubic), - }) - } playHaptic() sendInteraction({ item: post.uri, @@ -167,15 +131,6 @@ let PostCtrls = ({ await queueLike() } else { nextExpectedLikeValue.current = 0 - likeIconAnimValue.value = 0 // Intentionally not animated - if (PlatformInfo.getIsReducedMotionEnabled()) { - likeTextAnimValue.value = 0 - } else { - likeTextAnimValue.value = withTiming(0, { - duration: 400, - easing: Easing.out(Easing.cubic), - }) - } await queueUnlike() } } catch (e: any) { @@ -185,8 +140,6 @@ let PostCtrls = ({ } }, [ _, - likeIconAnimValue, - likeTextAnimValue, playHaptic, post.uri, post.viewer?.like, @@ -291,8 +244,8 @@ let PostCtrls = ({ a.gap_xs, a.rounded_full, a.flex_row, - a.align_center, a.justify_center, + a.align_center, {padding: 5}, (pressed || hovered) && t.atoms.bg_contrast_25, ], @@ -364,13 +317,11 @@ let PostCtrls = ({ } accessibilityHint="" hitSlop={POST_CTRL_HITSLOP}> - <AnimatedLikeIcon - big={big ?? false} - likeIconAnimValue={likeIconAnimValue} - likeTextAnimValue={likeTextAnimValue} - defaultCtrlColor={defaultCtrlColor} - isLiked={Boolean(post.viewer?.like)} + <AnimatedLikeIcon isLiked={Boolean(post.viewer?.like)} big={big} /> + <CountWheel likeCount={post.likeCount ?? 0} + big={big} + isLiked={Boolean(post.viewer?.like)} /> </Pressable> </View> @@ -450,194 +401,3 @@ let PostCtrls = ({ } PostCtrls = memo(PostCtrls) export {PostCtrls} - -function AnimatedLikeIcon({ - big, - likeIconAnimValue, - likeTextAnimValue, - defaultCtrlColor, - isLiked, - likeCount, -}: { - big: boolean - likeIconAnimValue: SharedValue<number> - likeTextAnimValue: SharedValue<number> - defaultCtrlColor: StyleProp<ViewStyle> - isLiked: boolean - likeCount: number -}) { - const t = useTheme() - const {i18n} = useLingui() - const likeStyle = useAnimatedStyle(() => ({ - transform: [ - { - scale: interpolate( - likeIconAnimValue.value, - [0, 0.1, 0.4, 1], - [1, 0.7, 1.2, 1], - 'clamp', - ), - }, - ], - })) - const circle1Style = useAnimatedStyle(() => ({ - opacity: interpolate( - likeIconAnimValue.value, - [0, 0.1, 0.95, 1], - [0, 0.4, 0.4, 0], - 'clamp', - ), - transform: [ - { - scale: interpolate( - likeIconAnimValue.value, - [0, 0.4, 1], - [0, 1.5, 1.5], - 'clamp', - ), - }, - ], - })) - const circle2Style = useAnimatedStyle(() => ({ - opacity: interpolate( - likeIconAnimValue.value, - [0, 0.1, 0.95, 1], - [0, 1, 1, 0], - 'clamp', - ), - transform: [ - { - scale: interpolate( - likeIconAnimValue.value, - [0, 0.4, 1], - [0, 0, 1.5], - 'clamp', - ), - }, - ], - })) - const countStyle = useAnimatedStyle(() => ({ - transform: [ - { - translateY: interpolate( - likeTextAnimValue.value, - [0, 1], - [0, big ? -22 : -18], - 'clamp', - ), - }, - ], - })) - - const prevFormattedCount = formatCount( - i18n, - isLiked ? likeCount - 1 : likeCount, - ) - const nextFormattedCount = formatCount( - i18n, - isLiked ? likeCount : likeCount + 1, - ) - const shouldRollLike = - prevFormattedCount !== nextFormattedCount && prevFormattedCount !== '0' - - return ( - <> - <View> - <Animated.View - style={[ - { - position: 'absolute', - backgroundColor: s.likeColor.color, - top: 0, - left: 0, - width: big ? 22 : 18, - height: big ? 22 : 18, - zIndex: -1, - pointerEvents: 'none', - borderRadius: (big ? 22 : 18) / 2, - }, - circle1Style, - ]} - /> - <Animated.View - style={[ - { - position: 'absolute', - backgroundColor: isWeb - ? t.atoms.bg_contrast_25.backgroundColor - : t.atoms.bg.backgroundColor, - top: 0, - left: 0, - width: big ? 22 : 18, - height: big ? 22 : 18, - zIndex: -1, - pointerEvents: 'none', - borderRadius: (big ? 22 : 18) / 2, - }, - circle2Style, - ]} - /> - <Animated.View style={likeStyle}> - {isLiked ? ( - <HeartIconFilled style={s.likeColor} width={big ? 22 : 18} /> - ) : ( - <HeartIconOutline - style={[defaultCtrlColor, {pointerEvents: 'none'}]} - width={big ? 22 : 18} - /> - )} - </Animated.View> - </View> - <View style={{overflow: 'hidden'}}> - <Text - testID="likeCount" - style={[ - [ - big ? a.text_md : {fontSize: 15}, - a.user_select_none, - isLiked ? [a.font_bold, s.likeColor] : defaultCtrlColor, - {opacity: shouldRollLike ? 0 : 1}, - ], - ]}> - {likeCount > 0 ? formatCount(i18n, likeCount) : ''} - </Text> - <Animated.View - aria-hidden={true} - style={[ - countStyle, - { - position: 'absolute', - top: 0, - left: 0, - opacity: shouldRollLike ? 1 : 0, - }, - ]}> - <Text - testID="likeCount" - style={[ - [ - big ? a.text_md : {fontSize: 15}, - a.user_select_none, - isLiked ? [a.font_bold, s.likeColor] : defaultCtrlColor, - {height: big ? 22 : 18}, - ], - ]}> - {prevFormattedCount} - </Text> - <Text - testID="likeCount" - style={[ - [ - big ? a.text_md : {fontSize: 15}, - a.user_select_none, - isLiked ? [a.font_bold, s.likeColor] : defaultCtrlColor, - {height: big ? 22 : 18}, - ], - ]}> - {nextFormattedCount} - </Text> - </Animated.View> - </View> - </> - ) -} |