Files
purewhiter--mobilegym/os/hooks/useAppNavigationHandler.ts
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

141 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useContext } from 'react';
// UNSAFE_NavigationContext: react-router v7 no longer exposes MemoryHistory.entries,
// but still provides navigator.index. We use this to sync the shadow HistoryTracker
// that powers popTo. If a future version removes this context, the tracker init
// and UNSAFE_NavigationContext usage are confined to this file + app navigation.ts files.
import { useLocation, useNavigate, UNSAFE_NavigationContext } from 'react-router-dom';
import { AppNavigatorRegistry } from '../AppNavigatorRegistry';
import { AppLifecycle, type LifecycleEvent } from '../AppLifecycle';
import { BackDispatcher } from '../BackDispatcher';
import { useActivityContext } from '../ActivityContext';
import { TaskManager } from '../TaskManager';
import { useAppReady } from './useAppReady';
import { syncTracker, getTracker } from '../utils/memoryHistoryTracker';
import { memoryHistoryPopTo } from '../utils/memoryHistoryPopTo';
interface Options {
onBack: () => boolean;
onForeground?: () => void;
onBackground?: () => void;
onDestroy?: () => void;
onNavigate?: (path: string, navigate: (nextPath: string) => void) => void;
}
interface NavOptions { replace?: boolean }
export function useAppNavigationHandler(appId: string, options: Options) {
const navigate = useNavigate();
const location = useLocation();
const { navigator } = useContext(UNSAFE_NavigationContext);
const { taskId } = useActivityContext();
// Sync the shadow history tracker on every location change.
useEffect(() => {
syncTracker(navigator, location);
}, [navigator, location.pathname, location.search, location.key]);
const optionsRef = useRef(options);
const routeRef = useRef<AppRouteInfo>({
app: appId,
path: location.pathname + location.search,
});
const navigateRef = useRef<(path: string, opts?: NavOptions) => void>(() => {});
const isForegroundRef = useRef(false);
optionsRef.current = options;
useEffect(() => {
routeRef.current = { app: appId, path: location.pathname + location.search };
}, [appId, location.pathname, location.search]);
useEffect(() => {
const state = TaskManager.getState();
const task = taskId ? state.tasks.find(t => t.taskId === taskId) : null;
const isInForeignTask = task != null && task.rootAppId !== appId;
if (isInForeignTask) {
return;
}
navigateRef.current = (path: string, opts?: NavOptions) => {
const shouldReplace = opts?.replace ?? true;
const directNavigate = (nextPath: string) => navigate(nextPath, { replace: shouldReplace });
if (optionsRef.current.onNavigate) {
optionsRef.current.onNavigate(path, directNavigate);
return;
}
directNavigate(path);
};
AppNavigatorRegistry.register(appId, {
navigate: (path: string, opts?: NavOptions) => navigateRef.current(path, opts),
back: () => {
// 与下面 `app.back.${appId}` BackDispatcher 条目同样的 activeTask 校验。
// OS 的 `os.appBack`os/OSContext.tsx)通过 AppNavigatorRegistry.getBackHandler 直接取这个
// 闭包,绕过 BackDispatcher 注册时的过滤;如果不在这里也加一遍,foreign-task push 同 appId
// 的场景下背景 own-task 实例的 onBack 仍会被调用(消费别人的返回事件)。
const state = TaskManager.getState();
if (taskId && state.activeTaskId !== taskId) return false;
return optionsRef.current.onBack();
},
route: () => routeRef.current,
popToRoot: () => {
const tracker = getTracker(navigator as object);
if (tracker && tracker.findPopToDelta('/', false) >= 0) {
memoryHistoryPopTo(navigator, '/', { inclusive: false });
return true;
}
// Fallback: 历史中找不到 '/'(极少数 replace 链覆盖了根条目),直接 replace 当前条目为 '/'.
navigate('/', { replace: true });
return true;
},
});
isForegroundRef.current = window.__OS__?.state.activeAppId === appId;
const unregisterBack = BackDispatcher.register(`app.back.${appId}`, () => {
// foreground 检查:只有当我自己的 own-task 是当前 active task 时才处理 back。
// 否则(同 appId 的 Activity 被 foreign-task push 到别的 task 上时,原本 own-task 在后台
// 但 activeAppId 仍 == 我的 appId)让位给 activity-level handlerpriority 50),
// 否则 own-task 实例的 handleBackPress 会用自己 MemoryRouter 的状态消费 back
// 导致前台 foreign-task Activity 不动。
if (!isForegroundRef.current) return false;
const state = TaskManager.getState();
if (taskId && state.activeTaskId !== taskId) return false;
return !!optionsRef.current.onBack();
}, 100);
return () => {
unregisterBack();
AppNavigatorRegistry.unregister(appId);
};
}, [appId, navigate, navigator, taskId]);
useEffect(() => {
const state = TaskManager.getState();
const task = taskId ? state.tasks.find(t => t.taskId === taskId) : null;
const isInForeignTask = task != null && task.rootAppId !== appId;
if (isInForeignTask) return;
return AppLifecycle.subscribe(appId, (event: LifecycleEvent) => {
if (event === 'foreground') {
isForegroundRef.current = true;
optionsRef.current.onForeground?.();
return;
}
if (event === 'background') {
isForegroundRef.current = false;
optionsRef.current.onBackground?.();
return;
}
if (event === 'destroy') {
isForegroundRef.current = false;
AppNavigatorRegistry.setBackOverride(appId);
optionsRef.current.onDestroy?.();
}
});
}, [appId, taskId]);
useAppReady(appId);
}