forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {View} from 'react-native'
3import Animated, {
4 Easing,
5 LayoutAnimationConfig,
6 useReducedMotion,
7 withTiming,
8} from 'react-native-reanimated'
9
10import {decideShouldRoll} from '#/lib/custom-animations/util'
11import {s} from '#/lib/styles'
12import {Text} from '#/view/com/util/text/Text'
13import {atoms as a, useTheme} from '#/alf'
14import {useFormatPostStatCount} from '#/components/PostControls/util'
15
16const animationConfig = {
17 duration: 400,
18 easing: Easing.out(Easing.cubic),
19}
20
21function EnteringUp() {
22 'worklet'
23 const animations = {
24 opacity: withTiming(1, animationConfig),
25 transform: [{translateY: withTiming(0, animationConfig)}],
26 }
27 const initialValues = {
28 opacity: 0,
29 transform: [{translateY: 18}],
30 }
31 return {
32 animations,
33 initialValues,
34 }
35}
36
37function EnteringDown() {
38 'worklet'
39 const animations = {
40 opacity: withTiming(1, animationConfig),
41 transform: [{translateY: withTiming(0, animationConfig)}],
42 }
43 const initialValues = {
44 opacity: 0,
45 transform: [{translateY: -18}],
46 }
47 return {
48 animations,
49 initialValues,
50 }
51}
52
53function ExitingUp() {
54 'worklet'
55 const animations = {
56 opacity: withTiming(0, animationConfig),
57 transform: [
58 {
59 translateY: withTiming(-18, animationConfig),
60 },
61 ],
62 }
63 const initialValues = {
64 opacity: 1,
65 transform: [{translateY: 0}],
66 }
67 return {
68 animations,
69 initialValues,
70 }
71}
72
73function ExitingDown() {
74 'worklet'
75 const animations = {
76 opacity: withTiming(0, animationConfig),
77 transform: [{translateY: withTiming(18, animationConfig)}],
78 }
79 const initialValues = {
80 opacity: 1,
81 transform: [{translateY: 0}],
82 }
83 return {
84 animations,
85 initialValues,
86 }
87}
88
89export function CountWheel({
90 likeCount,
91 big,
92 isLiked,
93 hasBeenToggled,
94}: {
95 likeCount: number
96 big?: boolean
97 isLiked: boolean
98 hasBeenToggled: boolean
99}) {
100 const t = useTheme()
101 const shouldAnimate = !useReducedMotion() && hasBeenToggled
102 const shouldRoll = decideShouldRoll(isLiked, likeCount)
103
104 // Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
105 // animation
106 // The initial entering/exiting animations will get skipped, since these will happen on screen mounts and would
107 // be unnecessary
108 const [key, setKey] = React.useState(0)
109 const [prevCount, setPrevCount] = React.useState(likeCount)
110 const prevIsLiked = React.useRef(isLiked)
111 const formatPostStatCount = useFormatPostStatCount()
112 const formattedCount = formatPostStatCount(likeCount)
113 const formattedPrevCount = formatPostStatCount(prevCount)
114
115 React.useEffect(() => {
116 if (isLiked === prevIsLiked.current) {
117 return
118 }
119
120 const newPrevCount = isLiked ? likeCount - 1 : likeCount + 1
121 setKey(prev => prev + 1)
122 setPrevCount(newPrevCount)
123 prevIsLiked.current = isLiked
124 }, [isLiked, likeCount])
125
126 const enteringAnimation =
127 shouldAnimate && shouldRoll
128 ? isLiked
129 ? EnteringUp
130 : EnteringDown
131 : undefined
132 const exitingAnimation =
133 shouldAnimate && shouldRoll
134 ? isLiked
135 ? ExitingUp
136 : ExitingDown
137 : undefined
138
139 return (
140 <LayoutAnimationConfig skipEntering skipExiting>
141 {likeCount > 0 ? (
142 <View style={[a.justify_center]}>
143 <Animated.View entering={enteringAnimation} key={key}>
144 <Text
145 testID="likeCount"
146 style={[
147 big ? a.text_md : a.text_sm,
148 a.user_select_none,
149 isLiked
150 ? [a.font_semi_bold, s.likeColor]
151 : {color: t.palette.contrast_500},
152 ]}>
153 {formattedCount}
154 </Text>
155 </Animated.View>
156 {shouldAnimate && (likeCount > 1 || !isLiked) ? (
157 <Animated.View
158 entering={exitingAnimation}
159 // Add 2 to the key so there are never duplicates
160 key={key + 2}
161 style={[a.absolute, {width: 50, opacity: 0}]}
162 aria-disabled={true}>
163 <Text
164 style={[
165 big ? a.text_md : a.text_sm,
166 a.user_select_none,
167 isLiked
168 ? [a.font_semi_bold, s.likeColor]
169 : {color: t.palette.contrast_500},
170 ]}>
171 {formattedPrevCount}
172 </Text>
173 </Animated.View>
174 ) : null}
175 </View>
176 ) : null}
177 </LayoutAnimationConfig>
178 )
179}