1import { createContext, use, useSyncExternalStore } from "react";
2
3/**
4 * Full swipe rows (pan gesture, animated actions, hidden action buttons) only
5 * exist around the viewport. Every other Home row renders a dormant frame that
6 * paints the same content with a fraction of the native views, so a row the
7 * list rebuilds while scrolling is cheap. The scroll gate already disables
8 * swipes while the list moves, so rows are activated once it rests.
9 */
10export function createSwipeRowActivation() {
11 let activeKeys = new Set<string>();
12 // Swapping a row's frame remounts it, which would cancel a press or long
13 // press in progress, so changes wait until every finger that started on the
14 // list has lifted.
15 const listTouches = new Set<string>();
16 let pendingKeys: ReadonlyArray<string> | null = null;
17 const listeners = new Set<() => void>();
18 const apply = (keys: ReadonlyArray<string>) => {
19 if (keys.length === activeKeys.size && keys.every((key) => activeKeys.has(key))) return;
20 activeKeys = new Set(keys);
21 for (const listener of listeners) listener();
22 };
23 return {
24 subscribe(listener: () => void) {
25 listeners.add(listener);
26 return () => void listeners.delete(listener);
27 },
28 isActive: (key: string) => activeKeys.has(key),
29 activate(keys: ReadonlyArray<string>) {
30 if (listTouches.size > 0) pendingKeys = keys;
31 else apply(keys);
32 },
33 /**
34 * `started` are touches that just began on the list; `onScreen` is every
35 * finger still down anywhere. A finger on another control never holds
36 * changes, and one whose end event went missing is dropped here.
37 */
38 trackTouches(started: ReadonlyArray<string>, onScreen: ReadonlyArray<string>) {
39 for (const id of started) listTouches.add(id);
40 for (const id of listTouches) if (!onScreen.includes(id)) listTouches.delete(id);
41 if (listTouches.size > 0 || pendingKeys === null) return;
42 const keys = pendingKeys;
43 pendingKeys = null;
44 apply(keys);
45 },
46 };
47}
48
49export type SwipeRowActivation = ReturnType<typeof createSwipeRowActivation>;
50
51export const SwipeRowActivationContext = createContext<SwipeRowActivation | null>(null);
52
53const subscribeNever = () => () => {};
54
55/** Rows outside an activation provider (e.g. the iPad sidebar) stay live. */
56export function useSwipeRowDormant(key: string | undefined): boolean {
57 const activation = use(SwipeRowActivationContext);
58 return useSyncExternalStore(
59 activation?.subscribe ?? subscribeNever,
60 () => activation !== null && key !== undefined && !activation.isActive(key),
61 );
62}