Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
5.4 kB · 169 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170import {useEffect, useState} from 'react'import {AppState, type AppStateStatus} from 'react-native'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'import {IS_IOS} from '#/env'
export const getCurrentState = () => AppState.currentState
export function onAppStateChange(cb: (state: AppStateStatus) => void) { let prev = AppState.currentState return AppState.addEventListener('change', next => { if (next === prev) return prev = next cb(next) })}
export function useOnAppStateChange(cb: (state: AppStateStatus) => void) { useEffect(() => { const sub = onAppStateChange(next => cb(next)) return () => sub.remove() }, [cb])}
export function useAppState() { const [state, setState] = useState(AppState.currentState) useOnAppStateChange(setState) return state}
/** One observed return, shared by every listener. */export type AppReturn = { /** Increases for each return during this JS runtime. */ readonly id: number /** `Date.now()` when the app became active. */ readonly timestamp: number}
/** * Any return from `background`, significant or not, shared by every listener. */export type AnyAppReturn = { /** Increases for each return from `background` during this JS runtime. */ readonly id: number /** `Date.now()` when the app became active. */ readonly timestamp: number /** How long the app was away, measured from the first `background`. */ readonly timeAway: number /** * Whether it was away at least {@link RETURN_MIN_TIME_AWAY}, which makes it * a significant return (see {@link onAppReturnedFromBackground}). */ readonly isSignificant: boolean}
/** * Android backgrounds the app for its own activities (pickers, share sheets, * etc.), so it needs a longer cutoff than iOS to filter those trips. */export const RETURN_MIN_TIME_AWAY = IS_IOS ? 30 * 1000 : 60 * 1000
const returnListeners = new Set<(appReturn: AppReturn) => void>()const anyReturnListeners = new Set<(appReturn: AnyAppReturn) => void>()let isTrackingReturns = falselet lastReturnId = 0let lastAnyReturnId = 0
/** * Keep one tracker alive across subscriptions so a new listener cannot lose * the background transition that started a trip away. */function trackReturns() { if (isTrackingReturns) return isTrackingReturns = true
let backgroundedAt = AppState.currentState === 'background' ? Date.now() : undefined onAppStateChange(next => { if (next === 'background') { backgroundedAt ??= Date.now() } else if (next === 'active' && backgroundedAt !== undefined) { const now = Date.now() const timeAway = now - backgroundedAt backgroundedAt = undefined const isSignificant = timeAway >= RETURN_MIN_TIME_AWAY const anyReturn: AnyAppReturn = { id: ++lastAnyReturnId, timestamp: now, timeAway, isSignificant, } for (const listener of [...anyReturnListeners]) { // an earlier listener may have removed this one if (anyReturnListeners.has(listener)) listener(anyReturn) } if (!isSignificant) return const appReturn: AppReturn = {id: ++lastReturnId, timestamp: now} for (const listener of [...returnListeners]) { // an earlier listener may have removed this one if (returnListeners.has(listener)) listener(appReturn) } } })}
/** * Calls `cb` on a significant return: the first `active` after at least * {@link RETURN_MIN_TIME_AWAY} in `background`. An `inactive` interruption * alone does not count. */export function onAppReturnedFromBackground( cb: (appReturn: AppReturn) => void,) { trackReturns() // Give each subscription its own identity, even for the same callback. const listener = (appReturn: AppReturn) => cb(appReturn) returnListeners.add(listener) return { remove: () => { returnListeners.delete(listener) }, }}
/** * Hook form of {@link onAppReturnedFromBackground}, using the latest `cb`. Not * with `useEffectEvent`, which React 19.2 never updates in `memo()` and * `forwardRef()` components (react/react#35187). */export function useOnAppReturnedFromBackground( cb: (appReturn: AppReturn) => void,) { const onReturn = useNonReactiveCallback(cb) useEffect(() => { const sub = onAppReturnedFromBackground(appReturn => onReturn(appReturn)) return () => sub.remove() }, [onReturn])}
/** * Calls `cb` on every `active` after `background`, however long the app was * away, saying whether it was a significant return (see * {@link onAppReturnedFromBackground}). An `inactive` interruption alone does * not count. */export function onAnyAppReturn(cb: (appReturn: AnyAppReturn) => void) { trackReturns() // Give each subscription its own identity, even for the same callback. const listener = (appReturn: AnyAppReturn) => cb(appReturn) anyReturnListeners.add(listener) return { remove: () => { anyReturnListeners.delete(listener) }, }}
/** * Hook form of {@link onAnyAppReturn}, using the latest `cb`. Not with * `useEffectEvent`, which React 19.2 never updates in `memo()` and * `forwardRef()` components (react/react#35187). */export function useOnAnyAppReturn(cb: (appReturn: AnyAppReturn) => void) { const onReturn = useNonReactiveCallback(cb) useEffect(() => { const sub = onAnyAppReturn(appReturn => onReturn(appReturn)) return () => sub.remove() }, [onReturn])}