11import { useState , useEffect , useCallback , useRef } from 'preact/hooks' ;
2+ import { useSignalEffect } from '@preact/signals' ;
23import { animateCount } from './animateCount.js' ;
34
45/**
@@ -20,16 +21,30 @@ import { animateCount } from './animateCount.js';
2021 * @param {number } targetValue - The target value to animate to
2122 * @param {import('preact').RefObject<HTMLElement> } [elementRef] - Optional ref
2223 * to element for viewport detection
24+ * @param {boolean | import("@preact/signals").Signal<boolean> } [skipAnimation] - If true or signal value is true, skip animation and immediately set to target value
2325 * @returns {number } The current animated value
2426 *
2527 * @todo IDEAL SOLUTION: Native code should send a message (e.g.,
2628 * 'ntp_becameVisible') when the NTP webview becomes visible to the user. This
2729 * would be more reliable than JavaScript-only detection. We could subscribe to
2830 * this message and trigger animation when received.
2931 */
30- export function useAnimatedCount ( targetValue , elementRef ) {
32+ export function useAnimatedCount ( targetValue , elementRef , skipAnimation = false ) {
3133 // Initialize to 0 so first render triggers percentage-based animation from spec
3234 const [ animatedValue , setAnimatedValue ] = useState ( 0 ) ;
35+
36+ // Track skipAnimation reactively if it's a signal
37+ const isSignal = typeof skipAnimation === 'object' && 'value' in skipAnimation ;
38+ const [ shouldSkipAnimation , setShouldSkipAnimation ] = useState (
39+ isSignal ? skipAnimation . value : skipAnimation
40+ ) ;
41+
42+ // Reactively track signal value changes (always call hook, but only track if it's a signal)
43+ useSignalEffect ( ( ) => {
44+ if ( isSignal ) {
45+ setShouldSkipAnimation ( skipAnimation . value ) ;
46+ }
47+ } ) ;
3348
3449 // Track current animated value to enable smooth incremental updates
3550 // Initialize to 0 so first animation uses spec's percentage-based starting point
@@ -121,22 +136,36 @@ export function useAnimatedCount(targetValue, elementRef) {
121136 wasVisibleRef . current = isCurrentlyVisible ;
122137
123138 if ( isCurrentlyVisible ) {
124- // Determine starting value for animation
125- let startValue = animatedValueRef . current ;
126-
127- // If we're returning to NTP and the target value has changed, animate from last seen value
128- if ( isReturningToNTP && lastSeenValueRef . current !== null && lastSeenValueRef . current !== targetValue ) {
129- startValue = lastSeenValueRef . current ;
130- // Reset animation state to allow re-animation
131- hasAnimatedRef . current = false ;
132- }
139+ // If skipAnimation is true (e.g., after burn all), immediately set to target value
140+ // This skips the countdown animation and goes directly to empty state (when targetValue is 0)
141+ if ( shouldSkipAnimation ) {
142+ // Cancel any ongoing animation immediately
143+ cancelAnimation ( ) ;
144+ // Immediately set to target value without animation
145+ // When targetValue is 0, this will trigger the empty state display
146+ setAnimatedValue ( targetValue ) ;
147+ animatedValueRef . current = targetValue ;
148+ hasAnimatedRef . current = true ;
149+ // Update last seen value so it's correct for future animations
150+ lastSeenValueRef . current = targetValue ;
151+ } else {
152+ // Determine starting value for animation
153+ let startValue = animatedValueRef . current ;
133154
134- // Animate from start value to target
135- cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , startValue ) ;
136- hasAnimatedRef . current = true ;
155+ // If we're returning to NTP and the target value has changed, animate from last seen value
156+ if ( isReturningToNTP && lastSeenValueRef . current !== null && lastSeenValueRef . current !== targetValue ) {
157+ startValue = lastSeenValueRef . current ;
158+ // Reset animation state to allow re-animation
159+ hasAnimatedRef . current = false ;
160+ }
137161
138- // After animation starts, update last seen to target (will be updated as animation progresses)
139- // This ensures next time we hide, we save the correct final value
162+ // Animate from start value to target
163+ cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , startValue ) ;
164+ hasAnimatedRef . current = true ;
165+
166+ // After animation starts, update last seen to target (will be updated as animation progresses)
167+ // This ensures next time we hide, we save the correct final value
168+ }
140169 } else {
141170 // Page is not visible
142171 if ( wasVisible ) {
@@ -164,19 +193,32 @@ export function useAnimatedCount(targetValue, elementRef) {
164193 wasVisibleRef . current = isNowVisible ;
165194
166195 if ( isNowVisible ) {
167- // Determine starting value
168- let startValue = animatedValueRef . current ;
196+ // If skipAnimation is true (e.g., after burn all), immediately set to target value
197+ // This skips the countdown animation and goes directly to empty state (when targetValue is 0)
198+ if ( shouldSkipAnimation ) {
199+ cancelAnimation ( ) ;
200+ // Immediately set to target value without animation
201+ // When targetValue is 0, this will trigger the empty state display
202+ setAnimatedValue ( targetValue ) ;
203+ animatedValueRef . current = targetValue ;
204+ hasAnimatedRef . current = true ;
205+ // Update last seen value so it's correct for future animations
206+ lastSeenValueRef . current = targetValue ;
207+ } else {
208+ // Determine starting value
209+ let startValue = animatedValueRef . current ;
210+
211+ // If returning to NTP and value changed, animate from last seen
212+ if ( isReturningToNTPNow && lastSeenValueRef . current !== null && lastSeenValueRef . current !== targetValue ) {
213+ startValue = lastSeenValueRef . current ;
214+ hasAnimatedRef . current = false ;
215+ }
169216
170- // If returning to NTP and value changed, animate from last seen
171- if ( isReturningToNTPNow && lastSeenValueRef . current !== null && lastSeenValueRef . current !== targetValue ) {
172- startValue = lastSeenValueRef . current ;
173- hasAnimatedRef . current = false ;
217+ // Page became visible and element is in viewport - start animation
218+ cancelAnimation ( ) ;
219+ cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , startValue ) ;
220+ hasAnimatedRef . current = true ;
174221 }
175-
176- // Page became visible and element is in viewport - start animation
177- cancelAnimation ( ) ;
178- cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , startValue ) ;
179- hasAnimatedRef . current = true ;
180222 } else if ( document . visibilityState === 'hidden' ) {
181223 // Page became hidden - save current value and cancel animation
182224 cancelAnimation ( ) ;
@@ -195,7 +237,7 @@ export function useAnimatedCount(targetValue, elementRef) {
195237 cancelAnimation ( ) ;
196238 document . removeEventListener ( 'visibilitychange' , handleVisibilityChange ) ;
197239 } ;
198- } , [ targetValue , updateAnimatedCount , isInViewport ] ) ;
240+ } , [ targetValue , updateAnimatedCount , isInViewport , shouldSkipAnimation ] ) ;
199241
200242 return animatedValue ;
201243}
0 commit comments