From feb64294682e08a0c10b863cf1c2e788dac9356d Mon Sep 17 00:00:00 2001 From: Bruce Liu Date: Sat, 27 Dec 2025 15:48:37 -0800 Subject: [PATCH 1/6] refactor nav as function component --- src/components/nav.tsx | 440 ++++++++++++++++--------------- src/components/root.tsx | 4 +- src/containers/nav-container.tsx | 40 --- 3 files changed, 231 insertions(+), 253 deletions(-) delete mode 100644 src/containers/nav-container.tsx diff --git a/src/components/nav.tsx b/src/components/nav.tsx index dcb4c8f7..02184be1 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -1,250 +1,268 @@ import * as React from "react" +import { useState, useEffect, useCallback } from "react" import intl from "react-intl-universal" +import { useSelector, useDispatch } from "react-redux" import { Icon } from "@fluentui/react/lib/Icon" -import { AppState } from "../scripts/models/app" import { ProgressIndicator, IObjectWithKey } from "@fluentui/react" -import { getWindowBreakpoint } from "../scripts/utils" -import { WindowStateListenerType } from "../schema-types" - -type NavProps = { - state: AppState - itemShown: boolean - menu: () => void - search: () => void - markAllRead: () => void - fetch: () => void - logs: () => void - views: () => void - settings: () => void -} - -type NavState = { - maximized: boolean -} +import { RootState } from "../scripts/reducer" +import { fetchItems } from "../scripts/models/item" +import { + toggleMenu, + toggleLogMenu, + toggleSettings, + openViewMenu, + openMarkAllMenu, +} from "../scripts/models/app" +import { toggleSearch } from "../scripts/models/page" +import { ViewType , WindowStateListenerType } from "../schema-types" -class Nav extends React.Component { - constructor(props) { - super(props) - this.setBodyFocusState(window.utils.isFocused()) - this.setBodyFullscreenState(window.utils.isFullscreen()) - window.utils.addWindowStateListener(this.windowStateListener) - this.state = { - maximized: window.utils.isMaximized(), - } - } +const Nav: React.FC = () => { + const dispatch = useDispatch() + const state = useSelector((state: RootState) => state.app) + const itemShown = useSelector( + (state: RootState) => state.page.itemId && state.page.viewType !== ViewType.List + ) + const [maximized, setMaximized] = useState(window.utils.isMaximized()) - setBodyFocusState = (focused: boolean) => { + const setBodyFocusState = useCallback((focused: boolean) => { if (focused) document.body.classList.remove("blur") else document.body.classList.add("blur") - } + }, []) - setBodyFullscreenState = (fullscreen: boolean) => { + const setBodyFullscreenState = useCallback((fullscreen: boolean) => { if (fullscreen) document.body.classList.remove("not-fullscreen") else document.body.classList.add("not-fullscreen") - } - - windowStateListener = (type: WindowStateListenerType, state: boolean) => { - switch (type) { - case WindowStateListenerType.Maximized: - this.setState({ maximized: state }) - break - case WindowStateListenerType.Fullscreen: - this.setBodyFullscreenState(state) - break - case WindowStateListenerType.Focused: - this.setBodyFocusState(state) - break - } - } + }, []) - navShortcutsHandler = (e: KeyboardEvent | IObjectWithKey) => { - if (!this.props.state.settings.display) { - switch (e.key) { - case "F1": - this.props.menu() + const windowStateListener = useCallback( + (type: WindowStateListenerType, windowState: boolean) => { + switch (type) { + case WindowStateListenerType.Maximized: + setMaximized(windowState) break - case "F2": - this.props.search() + case WindowStateListenerType.Fullscreen: + setBodyFullscreenState(windowState) break - case "F5": - this.fetch() - break - case "F6": - this.props.markAllRead() - break - case "F7": - if (!this.props.itemShown) this.props.logs() - break - case "F8": - if (!this.props.itemShown) this.props.views() - break - case "F9": - if (!this.props.itemShown) this.props.settings() + case WindowStateListenerType.Focused: + setBodyFocusState(windowState) break } + }, + [setBodyFocusState, setBodyFullscreenState] + ) + + const canFetch = useCallback( + () => + state.sourceInit && + state.feedInit && + !state.syncing && + !state.fetchingItems, + [state.sourceInit, state.feedInit, state.syncing, state.fetchingItems] + ) + + const fetch = useCallback(() => { + if (canFetch()) dispatch(fetchItems()) + }, [canFetch, dispatch]) + + const menu = useCallback(() => dispatch(toggleMenu()), [dispatch]) + const logs = useCallback(() => dispatch(toggleLogMenu()), [dispatch]) + const search = useCallback(() => dispatch(toggleSearch()), [dispatch]) + const settings = useCallback(() => dispatch(toggleSettings()), [dispatch]) + const markAll = useCallback(() => dispatch(openMarkAllMenu()), [dispatch]) + const views = useCallback(() => { + if (state.contextMenu.event !== "#view-toggle") { + dispatch(openViewMenu()) } - } + }, [state.contextMenu.event, dispatch]) + + const navShortcutsHandler = useCallback( + (e: KeyboardEvent | IObjectWithKey) => { + if (!state.settings.display) { + switch (e.key) { + case "F1": + menu() + break + case "F2": + search() + break + case "F5": + fetch() + break + case "F6": + markAll() + break + case "F7": + if (!itemShown) logs() + break + case "F8": + if (!itemShown) views() + break + case "F9": + if (!itemShown) settings() + break + } + } + }, + [state.settings.display, itemShown, menu, search, fetch, markAll, logs, views, settings] + ) - componentDidMount() { - document.addEventListener("keydown", this.navShortcutsHandler) + useEffect(() => { + setBodyFocusState(window.utils.isFocused()) + setBodyFullscreenState(window.utils.isFullscreen()) + window.utils.addWindowStateListener(windowStateListener) + + return () => { + // Cleanup will be handled by the event listener removal effect + } + }, [setBodyFocusState, setBodyFullscreenState, windowStateListener]) + + useEffect(() => { + document.addEventListener("keydown", navShortcutsHandler) if (window.utils.platform === "darwin") - window.utils.addTouchBarEventsListener(this.navShortcutsHandler) - } - componentWillUnmount() { - document.removeEventListener("keydown", this.navShortcutsHandler) - } + window.utils.addTouchBarEventsListener(navShortcutsHandler) - minimize = () => { + return () => { + document.removeEventListener("keydown", navShortcutsHandler) + } + }, [navShortcutsHandler]) + + const minimize = () => { window.utils.minimizeWindow() } - maximize = () => { + + const maximize = () => { window.utils.maximizeWindow() - this.setState({ maximized: !this.state.maximized }) + setMaximized(!maximized) } - close = () => { + + const close = () => { window.utils.closeWindow() } - canFetch = () => - this.props.state.sourceInit && - this.props.state.feedInit && - !this.props.state.syncing && - !this.props.state.fetchingItems - fetching = () => (!this.canFetch() ? " fetching" : "") - getClassNames = () => { + const fetching = () => (!canFetch() ? " fetching" : "") + + const getClassNames = () => { const classNames = new Array() - if (this.props.state.settings.display) classNames.push("hide-btns") - if (this.props.state.menu) classNames.push("menu-on") - if (this.props.itemShown) classNames.push("item-on") + if (state.settings.display) classNames.push("hide-btns") + if (state.menu) classNames.push("menu-on") + if (itemShown) classNames.push("item-on") return classNames.join(" ") } - fetch = () => { - if (this.canFetch()) this.props.fetch() - } - - views = () => { - if (this.props.state.contextMenu.event !== "#view-toggle") { - this.props.views() - } - } - - getProgress = () => { - return this.props.state.fetchingTotal > 0 - ? this.props.state.fetchingProgress / this.props.state.fetchingTotal + const getProgress = () => { + return state.fetchingTotal > 0 + ? state.fetchingProgress / state.fetchingTotal : null } - render() { - return ( - + ) } export default Nav diff --git a/src/components/root.tsx b/src/components/root.tsx index 8d75667b..ed1db84c 100644 --- a/src/components/root.tsx +++ b/src/components/root.tsx @@ -3,7 +3,7 @@ import { connect } from "react-redux" import { closeContextMenu } from "../scripts/models/app" import PageContainer from "../containers/page-container" import MenuContainer from "../containers/menu-container" -import NavContainer from "../containers/nav-container" +import Nav from "./nav" import SettingsContainer from "../containers/settings-container" import { RootState } from "../scripts/reducer" import { ContextMenu } from "./context-menu" @@ -15,7 +15,7 @@ const Root = ({ locale, dispatch }) => id="root" key={locale} onMouseDown={() => dispatch(closeContextMenu())}> - +