Skip to content
Source
Sign inSign up

home-route-visibility.ts

90 lines3.5 KB

1import { RegistryContext } from "@effect/atom-react";
2import { useNavigation } from "@react-navigation/native";
3import { useContext, useRef, useSyncExternalStore } from "react";
4import type { Atom } from "effect/reactivity";
5​
6import {
7 NativePrimaryColumnContext,
8 NativeWorkspaceTabActivityContext,
9} from "../../native/v5-workspace-context";
10​
11/**
12 * Home stays mounted under a pushed Thread, and native-stack never freezes the
13 * route directly below the focused one on Fabric, so Home has to stop its own
14 * work. The Home screen's static `listeners` (Stack.tsx) drive this store.
15 */
16let homeVisible: boolean | null = null;
17const visibilityListeners = new Set<() => void>();
18​
19function setHomeVisible(next: boolean) {
20 if (next === homeVisible) return;
21 homeVisible = next;
22 for (const listener of visibilityListeners) listener();
24​
25function subscribeHomeVisibility(onChange: () => void) {
26 visibilityListeners.add(onChange);
27 return () => {
28 visibilityListeners.delete(onChange);
29 // The value describes one Home instance. A later Home can mount under a
30 // Thread with no event of its own (Android activity replacement, a cold
31 // deep link), so it must fall back to `isFocused()` instead of this one.
32 if (visibilityListeners.size === 0) homeVisible = null;
33 };
35​
36let onHomeCovered: (() => void) | null = null;
37​
38/** Runs `action` once the page opening over Home covers it; Home starting to reappear first drops it. */
39export function runWhenHomeCovered(action: () => void): void {
40 onHomeCovered = action;
42​
43export function recordHomeTransitionStart(closing: boolean): void {
44 if (closing) return;
45 onHomeCovered = null;
46 setHomeVisible(true);
48​
49/**
50 * Each iOS tab slot shows its own copy of the Home route, and the slot a tab switch leaves
51 * disappears last. So a disappear hides Home only when another route has the focus.
52 */
53export function recordHomeTransitionEnd(closing: boolean, homeFocused: boolean): void {
54 if (!closing || homeFocused) return;
55 setHomeVisible(false);
56 const action = onHomeCovered;
57 onHomeCovered = null;
58 action?.();
60​
61/** Popping a sheet or a deep-linked route above Home has no appear event. */
62export function recordHomeFocus(): void {
63 setHomeVisible(true);
65​
66export function useHomeRouteVisible(): boolean {
67 const navigation = useNavigation();
68 const primaryColumn = useContext(NativePrimaryColumnContext);
69 const tabActive = useContext(NativeWorkspaceTabActivityContext)?.active ?? true;
70 const visible = useSyncExternalStore(subscribeHomeVisibility, () => homeVisible);
71 // Every tab slot's primary column reports to the one Home route, so a tab
72 // switch can end on the leaving slot's disappear. Trust this column instead.
73 if (primaryColumn) return primaryColumn.presented;
74 // The store is true for the Home route of every tab; this list shows in one of them.
75 if (!tabActive) return false;
76 // Before Home's first navigation event: a cold deep link mounts Home under
77 // its Thread without ever showing it.
78 return visible ?? navigation.isFocused();
80​
81export function useAtomValueWhileVisible<A>(atom: Atom.Atom<A>, visible: boolean): A {
82 const registry = useContext(RegistryContext);
83 const held = useRef<{ readonly value: A } | null>(null);
84 const subscribe = (onChange: () => void) =>
85 visible ? registry.subscribe(atom, onChange) : () => undefined;
86 return useSyncExternalStore(subscribe, () => {
87 if (visible || held.current === null) held.current = { value: registry.get(atom) };
88 return held.current.value;
89 });