about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-09-02 01:37:24 -0700
committerGitHub <noreply@github.com>2024-09-02 01:37:24 -0700
commit1225e8448524633466379d5ac00a78b53e1a9a51 (patch)
tree6215245576fed72912eeee9a51bcce01baf725ba
parenteb868a042ad4f767b8e1b90d33bf1171b66a5238 (diff)
downloadvoidsky-1225e8448524633466379d5ac00a78b53e1a9a51.tar.zst
Improve animations for like button (#5074)
-rw-r--r--src/lib/custom-animations/CountWheel.tsx177
-rw-r--r--src/lib/custom-animations/CountWheel.web.tsx121
-rw-r--r--src/lib/custom-animations/LikeIcon.tsx139
-rw-r--r--src/lib/custom-animations/LikeIcon.web.tsx115
-rw-r--r--src/lib/custom-animations/util.ts21
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx254
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>
-    </>
-  )
-}