···2828#
2929#
30303131-# Bluesky's metrics API
3131+# Optional direct metrics ingestion endpoint.
3232+# If unset, metrics fall back to Sentry/GlitchTip when EXPO_PUBLIC_SENTRY_DSN is set.
3233EXPO_PUBLIC_METRICS_API_HOST=
33343435# Growthbook config
3536EXPO_PUBLIC_GROWTHBOOK_API_HOST=
3637EXPO_PUBLIC_GROWTHBOOK_CLIENT_KEY=
37383838-# Sentry DSN for telemetry
3939+# Sentry-compatible DSN for telemetry, including GlitchTip.
3940EXPO_PUBLIC_SENTRY_DSN=
40414142# Bitdrift API key. If undefined, Bitdrift will be disabled.
···191191 const alsoLikedHeaderRef = useRef<View | null>(null)
192192 const currentScrollOffsetRef = useRef(0)
193193 const scrollStateRequestIdRef = useRef(0)
194194+ const scrollStateAnimationFrameRef = useRef<number | null>(null)
195195+ const contentSizeAnimationFrameRef = useRef<number | null>(null)
194196 const [isAlsoLikedFocused, setIsAlsoLikedFocused] = useState(false)
195197196198 useEffect(() => {
···232234 * The result being: any intentional change in view by the user will result
233235 * in the anchor being pinned as the first item.
234236 */
235235- const onContentSizeChangeWebOnly = web(() => {
236236- const list = listRef.current
237237- const anchorElement = anchorRef.current as any as Element
238238- const header = headerRef.current as any as Element
237237+ const onContentSizeChangeWebOnly = web(
238238+ useNonReactiveCallback(() => {
239239+ if (contentSizeAnimationFrameRef.current !== null) {
240240+ cancelAnimationFrame(contentSizeAnimationFrameRef.current)
241241+ }
242242+243243+ contentSizeAnimationFrameRef.current = requestAnimationFrame(() => {
244244+ contentSizeAnimationFrameRef.current = null
245245+ const list = listRef.current
246246+ const anchorElement = anchorRef.current as any as Element
247247+ const header = headerRef.current as any as Element
239248240240- if (list && anchorElement && header && shouldHandleScroll.current) {
241241- const anchorOffsetTop = anchorElement.getBoundingClientRect().top
242242- const headerHeight = header.getBoundingClientRect().height
249249+ if (list && anchorElement && header && shouldHandleScroll.current) {
250250+ const anchorOffsetTop = anchorElement.getBoundingClientRect().top
251251+ const headerHeight = header.getBoundingClientRect().height
243252244244- /*
245245- * `deferParents` is `true` on a cold load, and always reset to
246246- * `true` when params change via `prepareForParamsUpdate`.
247247- *
248248- * On a cold load or a push to a new post, on the first pass of this
249249- * logic, the anchor post is the first item in the list. Therefore
250250- * `anchorOffsetTop - headerHeight` will be 0.
251251- *
252252- * When a user changes thread params, on the first pass of this logic,
253253- * the anchor post may not move (if there are no parents above it), or it
254254- * may have gone off the screen above, because of the sudden lack of
255255- * parents due to `deferParents === true`. This negative value (minus
256256- * `headerHeight`) will result in a _negative_ `offset` value, which will
257257- * scroll the anchor post _down_ to the top of the screen.
258258- *
259259- * However, `prepareForParamsUpdate` also resets scroll to `0`, so when a user
260260- * changes params, the anchor post's offset will actually be equivalent
261261- * to the `headerHeight` because of how the DOM is stacked on web.
262262- * Therefore, `anchorOffsetTop - headerHeight` will once again be 0,
263263- * which means the first pass in this case will result in no scroll.
264264- *
265265- * Then, once parents are prepended, this will fire again. Now, the
266266- * `anchorOffsetTop` will be positive, which minus the header height,
267267- * will give us a _positive_ offset, which will scroll the anchor post
268268- * back _up_ to the top of the screen.
269269- */
270270- const offset = anchorOffsetTop - headerHeight
271271- list.scrollToOffset({offset})
253253+ /*
254254+ * `deferParents` is `true` on a cold load, and always reset to
255255+ * `true` when params change via `prepareForParamsUpdate`.
256256+ *
257257+ * On a cold load or a push to a new post, on the first pass of this
258258+ * logic, the anchor post is the first item in the list. Therefore
259259+ * `anchorOffsetTop - headerHeight` will be 0.
260260+ *
261261+ * When a user changes thread params, on the first pass of this logic,
262262+ * the anchor post may not move (if there are no parents above it), or it
263263+ * may have gone off the screen above, because of the sudden lack of
264264+ * parents due to `deferParents === true`. This negative value (minus
265265+ * `headerHeight`) will result in a _negative_ `offset` value, which will
266266+ * scroll the anchor post _down_ to the top of the screen.
267267+ *
268268+ * However, `prepareForParamsUpdate` also resets scroll to `0`, so when a user
269269+ * changes params, the anchor post's offset will actually be equivalent
270270+ * to the `headerHeight` because of how the DOM is stacked on web.
271271+ * Therefore, `anchorOffsetTop - headerHeight` will once again be 0,
272272+ * which means the first pass in this case will result in no scroll.
273273+ *
274274+ * Then, once parents are prepended, this will fire again. Now, the
275275+ * `anchorOffsetTop` will be positive, which minus the header height,
276276+ * will give us a _positive_ offset, which will scroll the anchor post
277277+ * back _up_ to the top of the screen.
278278+ */
279279+ const offset = anchorOffsetTop - headerHeight
280280+ list.scrollToOffset({offset})
272281273273- /*
274274- * After we manage to do a positive adjustment, we need to ensure this
275275- * doesn't run again until scroll handling is requested again via
276276- * `shouldHandleScroll.current === true` and a params change via
277277- * `prepareForParamsUpdate`.
278278- *
279279- * The `isRoot` here is needed because if we're looking at the anchor
280280- * post, this handler will not fire after `deferParents` is set to
281281- * `false`, since there are no parents to render above it. In this case,
282282- * we want to make sure `shouldHandleScroll` is set to `false` right away
283283- * so that subsequent size changes unrelated to a params change (like
284284- * pagination) do not affect scroll.
285285- */
286286- if (offset > 0 || isRoot) shouldHandleScroll.current = false
287287- }
288288- })
282282+ /*
283283+ * After we manage to do a positive adjustment, we need to ensure this
284284+ * doesn't run again until scroll handling is requested again via
285285+ * `shouldHandleScroll.current === true` and a params change via
286286+ * `prepareForParamsUpdate`.
287287+ *
288288+ * The `isRoot` here is needed because if we're looking at the anchor
289289+ * post, this handler will not fire after `deferParents` is set to
290290+ * `false`, since there are no parents to render above it. In this case,
291291+ * we want to make sure `shouldHandleScroll` is set to `false` right away
292292+ * so that subsequent size changes unrelated to a params change (like
293293+ * pagination) do not affect scroll.
294294+ */
295295+ if (offset > 0 || isRoot) shouldHandleScroll.current = false
296296+ }
297297+ })
298298+ }),
299299+ )
289300290301 /**
291302 * Ditto the above, but for native.
···605616 const toggleAlsoLikedCollapsed = useCallback(() => {
606617 setAlsoLikedCollapsed(current => !current)
607618 }, [])
608608- const handleScrollOffsetChange = useNonReactiveCallback((offsetY: number) => {
609609- currentScrollOffsetRef.current = offsetY
610610- if (offsetY <= 1) {
611611- scrollStateRequestIdRef.current += 1
612612- setIsAlsoLikedFocused(false)
613613- return
614614- }
615615- scrollStateRequestIdRef.current += 1
616616- updateAlsoLikedScrollState(offsetY, scrollStateRequestIdRef.current)
617617- })
618618- const updateAlsoLikedScrollState = useNonReactiveCallback(
619619- (offsetY: number, requestId: number) => {
619619+ const runAlsoLikedScrollStateUpdate = useNonReactiveCallback(
620620+ (requestId: number) => {
621621+ scrollStateAnimationFrameRef.current = null
622622+620623 if (
621624 !alsoLikedVisible ||
622625 alsoLikedCollapsed ||
···651654 })
652655 },
653656 )
657657+ const scheduleAlsoLikedScrollStateUpdate = useNonReactiveCallback(() => {
658658+ if (scrollStateAnimationFrameRef.current !== null) {
659659+ cancelAnimationFrame(scrollStateAnimationFrameRef.current)
660660+ }
661661+662662+ const requestId = scrollStateRequestIdRef.current
663663+ scrollStateAnimationFrameRef.current = requestAnimationFrame(() => {
664664+ runAlsoLikedScrollStateUpdate(requestId)
665665+ })
666666+ })
667667+ const handleScrollOffsetChange = useNonReactiveCallback((offsetY: number) => {
668668+ currentScrollOffsetRef.current = offsetY
669669+ if (offsetY <= 1) {
670670+ scrollStateRequestIdRef.current += 1
671671+ setIsAlsoLikedFocused(false)
672672+ return
673673+ }
674674+ if (!alsoLikedVisible || alsoLikedCollapsed) {
675675+ return
676676+ }
677677+ scrollStateRequestIdRef.current += 1
678678+ scheduleAlsoLikedScrollStateUpdate()
679679+ })
680680+681681+ useEffect(() => {
682682+ return () => {
683683+ if (scrollStateAnimationFrameRef.current !== null) {
684684+ cancelAnimationFrame(scrollStateAnimationFrameRef.current)
685685+ }
686686+ if (contentSizeAnimationFrameRef.current !== null) {
687687+ cancelAnimationFrame(contentSizeAnimationFrameRef.current)
688688+ }
689689+ }
690690+ }, [])
654691655692 useEffect(() => {
656693 scrollStateRequestIdRef.current += 1
657657- updateAlsoLikedScrollState(
658658- currentScrollOffsetRef.current,
659659- scrollStateRequestIdRef.current,
660660- )
694694+ scheduleAlsoLikedScrollStateUpdate()
661695 }, [
662696 alsoLikedCollapsed,
663697 alsoLikedPosts.length,
664698 alsoLikedVisible,
665665- updateAlsoLikedScrollState,
699699+ scheduleAlsoLikedScrollStateUpdate,
666700 ])
667701668702 return (