import * as React from 'react'; import { connect } from 'react-redux'; import Setting from '@joplin/lib/models/Setting'; import { AppState, AppStateRoute } from '../app.reducer'; import bridge from '../services/bridge'; import { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { WindowIdContext } from './NewWindowOrIFrame'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each registered screen has its own props shape; the navigator just spreads them through type ScreenProps = any; interface AppScreen { screen: React.ComponentType; title?: ()=> string; } interface Props { route: AppStateRoute; screens: Record; style: React.CSSProperties; className?: string; } const useWindowTitleManager = (screenInfo: AppScreen) => { const windowTitle = useMemo(() => { const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : ''; const windowTitle = [`CHM${devMarker}`]; if (screenInfo?.title) { windowTitle.push(screenInfo.title()); } return windowTitle.join(' - '); }, [screenInfo]); const windowId = useContext(WindowIdContext); useEffect(() => { bridge().windowById(windowId)?.setTitle(windowTitle); }, [windowTitle, windowId]); }; const useWindowRefocusManager = (route: AppStateRoute) => { const windowId = useContext(WindowIdContext); const prevRouteName = useRef(null); const routeName = route?.routeName; useEffect(() => { // When a navigation happens in an unfocused window, show the window to the user. // This might happen if, for example, a secondary window triggers a navigation in // the main window. if (routeName && routeName !== prevRouteName.current) { bridge().switchToWindow(windowId); } prevRouteName.current = routeName; }, [routeName, windowId]); }; const useContainerSize = (container: HTMLElement|null) => { const [size, setSize] = useState({ // Show the container as soon as possible: Default to the window size, // which is usually correct: width: container?.clientWidth ?? window.innerWidth, height: container?.clientHeight ?? window.innerHeight, }); const currentSizeRef = useRef(size); currentSizeRef.current = size; useEffect(() => { if (!container) return () => {}; const updateSizeIfDifferent = () => { const { width: lastWidth, height: lastHeight } = currentSizeRef.current; if (lastWidth !== container.clientWidth || lastHeight !== container.clientHeight) { setSize({ width: container.clientWidth, height: container.clientHeight, }); } }; // Ensure that the initial size is set, even if the ResizeObserver doesn't run the callback initially updateSizeIfDifferent(); const observer = new ResizeObserver(updateSizeIfDifferent); observer.observe(container); return () => { observer.disconnect(); }; }, [container]); return size; }; const NavigatorComponent: React.FC = props => { const route = props.route; const screenInfo = props.screens[route?.routeName]; const [container, setContainer] = useState(null); useWindowTitleManager(screenInfo); useWindowRefocusManager(route); const size = useContainerSize(container); if (!route) throw new Error('Route must not be null'); const screenProps = route.props ? route.props : {}; const Screen = screenInfo.screen; return (
); }; const Navigator = connect((state: AppState) => { return { route: state.route, }; })(NavigatorComponent); export default Navigator;