about summary refs log tree commit diff
path: root/src/lib/custom-animations/CountWheel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/custom-animations/CountWheel.tsx')
-rw-r--r--src/lib/custom-animations/CountWheel.tsx177
1 files changed, 177 insertions, 0 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>
+  )
+}