@@ -2,30 +2,10 @@ import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
22import { animateCount } from './animateCount.js' ;
33
44/**
5- * Custom hook to animate a count value with visibility-aware and
6- * viewport-aware animation
7- *
8- * IMPORTANT: In webview contexts (especially macOS), the NTP webview stays
9- * alive and processes updates in the background even when not visible. This
10- * hook tracks the "last seen" value when the element becomes invisible and
11- * animates from that value to the new target when it becomes visible again.
12- *
13- * Detection methods (in order of reliability):
14- * 1. Native visibility notification via messaging (e.g., 'ntp_becameVisible'
15- * subscription) - MOST RELIABLE (Not yet implemented - see @todo below)
16- * 2. IntersectionObserver (element enters/exits webview viewport) - CURRENTLY USED
17- * 3. document.visibilityState (page visibility API) - CURRENTLY USED (may not
18- * work if webview stays alive)
19- *
5+ * Custom hook to animate a count value with visibility-aware and viewport-aware animation
206 * @param {number } targetValue - The target value to animate to
21- * @param {import('preact').RefObject<HTMLElement> } [elementRef] - Optional ref
22- * to element for viewport detection
7+ * @param {import('preact').RefObject<HTMLElement> } [elementRef] - Optional ref to element for viewport detection
238 * @returns {number } The current animated value
24- *
25- * @todo IDEAL SOLUTION: Native code should send a message (e.g.,
26- * 'ntp_becameVisible') when the NTP webview becomes visible to the user. This
27- * would be more reliable than JavaScript-only detection. We could subscribe to
28- * this message and trigger animation when received.
299 */
3010export function useAnimatedCount ( targetValue , elementRef ) {
3111 // Initialize to 0 so first render triggers percentage-based animation from spec
@@ -41,13 +21,9 @@ export function useAnimatedCount(targetValue, elementRef) {
4121 // Track if we've animated at least once (to prevent re-animation on re-entry)
4222 const hasAnimatedRef = useRef ( false ) ;
4323
44- // Track the last value that was displayed when the page was visible
45- // This allows us to animate from the last seen value when returning to NTP
24+ // Track the last value that was displayed when element exited viewport
4625 const lastSeenValueRef = useRef ( /** @type {number | null } */ ( null ) ) ;
47-
48- // Track whether we were visible the last time we checked
49- // Used to detect transitions from hidden -> visible
50- const wasVisibleRef = useRef ( false ) ;
26+ const wasInViewportRef = useRef ( false ) ;
5127
5228 // Memoize the update callback to avoid recreating it on every render
5329 const updateAnimatedCount = useCallback (
@@ -60,16 +36,11 @@ export function useAnimatedCount(targetValue, elementRef) {
6036 [ ] ,
6137 ) ;
6238
63- // Track previous viewport state for IntersectionObserver callback
64- const wasInViewportRef = useRef ( false ) ;
65-
6639 // Setup IntersectionObserver for viewport detection
6740 useEffect ( ( ) => {
6841 // If no elementRef provided, element is always considered "in viewport"
6942 if ( ! elementRef || ! elementRef . current ) {
7043 setIsInViewport ( true ) ;
71- wasInViewportRef . current = true ;
72-
7344 return ;
7445 }
7546
@@ -79,11 +50,8 @@ export function useAnimatedCount(targetValue, elementRef) {
7950 const wasInViewport = wasInViewportRef . current ;
8051 const isNowInViewport = entry . isIntersecting ;
8152
82- // When element exits viewport, save current displayed value as "last seen"
83- // This is the value the user last saw, and we'll animate from this when returning
53+ // When element exits viewport, save current displayed value
8454 if ( wasInViewport && ! isNowInViewport ) {
85- // Save the value that was displayed when element became invisible
86- // This handles the case where user navigates away while NTP is visible
8755 lastSeenValueRef . current = animatedValueRef . current ;
8856 }
8957
@@ -110,78 +78,40 @@ export function useAnimatedCount(targetValue, elementRef) {
11078 useEffect ( ( ) => {
11179 let cancelAnimation = ( ) => { } ;
11280
113- const isCurrentlyVisible = document . visibilityState === 'visible' && isInViewport ;
114- const wasVisible = wasVisibleRef . current ;
115- const becameVisible = isCurrentlyVisible && ! wasVisible ;
116- // We're returning to NTP if we became visible AND we have a last seen value
117- // This means the element was visible before, became invisible, and is now visible again
118- const isReturningToNTP = becameVisible && lastSeenValueRef . current !== null ;
119-
120- // Update visibility tracking
121- wasVisibleRef . current = isCurrentlyVisible ;
81+ const shouldAnimate = document . visibilityState === 'visible' && isInViewport ;
12282
123- if ( isCurrentlyVisible ) {
83+ if ( shouldAnimate ) {
12484 // Determine starting value for animation
12585 let startValue = animatedValueRef . current ;
12686
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 ) {
87+ // If we have a last seen value, animate from that
88+ if ( lastSeenValueRef . current !== null && lastSeenValueRef . current !== targetValue ) {
12989 startValue = lastSeenValueRef . current ;
130- // Reset animation state to allow re-animation
131- hasAnimatedRef . current = false ;
90+ lastSeenValueRef . current = null ; // Clear after use
13291 }
13392
13493 // Animate from start value to target
13594 cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , startValue ) ;
13695 hasAnimatedRef . current = true ;
137-
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
140- } else {
141- // Page is not visible
142- if ( wasVisible ) {
143- // We just became hidden - save the current displayed value as last seen
144- // This is the value the user last saw, and we'll animate from this when returning
145- lastSeenValueRef . current = animatedValueRef . current ;
146- }
147-
148- if ( hasAnimatedRef . current ) {
149- // If we've already animated once and conditions aren't met, just snap to value
150- // This handles the case where value changes while element is out of viewport
151- setAnimatedValue ( targetValue ) ;
152- animatedValueRef . current = targetValue ;
153- }
154- // else: conditions not met and haven't animated yet, do nothing (wait for viewport entry)
96+ } else if ( hasAnimatedRef . current ) {
97+ // If we've already animated once and conditions aren't met, just snap to value
98+ // This handles the case where value changes while element is out of viewport
99+ setAnimatedValue ( targetValue ) ;
100+ animatedValueRef . current = targetValue ;
155101 }
102+ // else: conditions not met and haven't animated yet, do nothing (wait for viewport entry)
156103
157104 // Listen for visibility changes
158105 const handleVisibilityChange = ( ) => {
159- const isNowVisible = document . visibilityState === 'visible' && isInViewport ;
160- const wasVisibleBefore = wasVisibleRef . current ;
161- const becameVisibleNow = isNowVisible && ! wasVisibleBefore ;
162- const isReturningToNTPNow = becameVisibleNow && lastSeenValueRef . current !== null ;
163-
164- wasVisibleRef . current = isNowVisible ;
165-
166- if ( isNowVisible ) {
167- // Determine starting value
168- let startValue = animatedValueRef . current ;
169-
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 ;
174- }
175-
106+ if ( document . visibilityState === 'visible' && isInViewport ) {
176107 // Page became visible and element is in viewport - start animation
177108 cancelAnimation ( ) ;
178- cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , startValue ) ;
109+ cancelAnimation = animateCount ( targetValue , updateAnimatedCount , undefined , animatedValueRef . current ) ;
179110 hasAnimatedRef . current = true ;
180111 } else if ( document . visibilityState === 'hidden' ) {
181- // Page became hidden - save current value and cancel animation
112+ // Page became hidden - cancel animation and snap to final value
182113 cancelAnimation ( ) ;
183114 if ( hasAnimatedRef . current ) {
184- lastSeenValueRef . current = animatedValueRef . current ;
185115 setAnimatedValue ( targetValue ) ;
186116 animatedValueRef . current = targetValue ;
187117 }
0 commit comments