Files
paseo/packages/app/src/hooks/use-app-visible.ts
T
Mohamed BoudraandGitHub 7917532716 Keep focused agent timelines live and catch up instantly (#2196)
* fix(app): keep focused agent timelines live

Window focus was conflated with app visibility, so switching OS windows could remove the selected timeline subscription. Separate those signals, grace every visibility-driven removal, and cover retained history until authoritative catch-up completes.

* fix(app): keep timeline catch-up failures non-blocking

* fix(app): surface timeline catch-up failures

* fix(app): preserve file preview refocus

* fix(app): preserve optimistic catch-up flow
2026-07-18 16:30:59 +02:00

66 lines
2.3 KiB
TypeScript

import { useSyncExternalStore } from "react";
import { AppState } from "react-native";
import { getIsAppActivelyVisible, getIsAppVisible } from "@/utils/app-visibility";
import { isWeb } from "@/constants/platform";
let visible = getIsAppVisible();
let activelyVisible = getIsAppActivelyVisible();
const visibilityListeners = new Set<() => void>();
const activeVisibilityListeners = new Set<() => void>();
function notify(): void {
const nextVisible = getIsAppVisible();
if (nextVisible !== visible) {
visible = nextVisible;
for (const listener of visibilityListeners) listener();
}
const nextActivelyVisible = getIsAppActivelyVisible();
if (nextActivelyVisible !== activelyVisible) {
activelyVisible = nextActivelyVisible;
for (const listener of activeVisibilityListeners) listener();
}
}
// Track visibility for the app's whole lifetime, not per consumer: transitions that happen while
// no consumer is mounted must still be reflected in the snapshot the next consumer reads, or a
// component mounting right after a focus change acts on stale visibility.
// AppState needs no environment guard of its own — react-native-web's implementation already
// no-ops when there is no DOM, unlike the raw document/window listeners below.
AppState.addEventListener("change", notify);
if (isWeb && typeof document !== "undefined") {
document.addEventListener("visibilitychange", notify);
window.addEventListener("focus", notify);
window.addEventListener("blur", notify);
}
function subscribeToVisibility(listener: () => void): () => void {
visibilityListeners.add(listener);
return () => visibilityListeners.delete(listener);
}
function subscribeToActiveVisibility(listener: () => void): () => void {
activeVisibilityListeners.add(listener);
return () => activeVisibilityListeners.delete(listener);
}
function getVisibilitySnapshot(): boolean {
return visible;
}
function getActiveVisibilitySnapshot(): boolean {
return activelyVisible;
}
export function useAppVisible(): boolean {
return useSyncExternalStore(subscribeToVisibility, getVisibilitySnapshot, getVisibilitySnapshot);
}
export function useAppActivelyVisible(): boolean {
return useSyncExternalStore(
subscribeToActiveVisibility,
getActiveVisibilitySnapshot,
getActiveVisibilitySnapshot,
);
}