From 7928e0faf3c6bba88ee2e5faab22e2be2201195c Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 13:10:19 +0800 Subject: [PATCH 1/8] init Signed-off-by: Innei --- icons/mgc/line_cute_re.svg | 1 + src/renderer/src/lib/route-builder.test.ts | 4 +- src/renderer/src/lib/route-builder.ts | 2 +- src/renderer/src/modules/feed-column/item.tsx | 3 + .../feed/{[:id] => [id]}/index.tsx | 0 .../feed/{[:id] => [id]}/layout.tsx | 0 .../src/pages/(main)/(context)/layout.tsx | 17 ----- .../(subview)}/discover/index.tsx | 0 .../(subview)}/discover/layout.tsx | 0 .../(subview)}/layout.tsx | 0 .../(subview)}/profile/index.tsx | 0 .../(subview)}/profile/layout.tsx | 0 .../feeds/[feedId]}/index.tsx | 8 +-- .../src/pages/(main)/(layer)/layout.tsx | 1 + src/renderer/src/pages/(main)/index.tsx | 70 +++++++++++++++++++ src/renderer/src/pages/(main)/layout.tsx | 33 ++++----- src/renderer/src/router.tsx | 1 - 17 files changed, 94 insertions(+), 46 deletions(-) create mode 100644 icons/mgc/line_cute_re.svg rename src/renderer/src/pages/(external)/(with-layout)/feed/{[:id] => [id]}/index.tsx (100%) rename src/renderer/src/pages/(external)/(with-layout)/feed/{[:id] => [id]}/layout.tsx (100%) delete mode 100644 src/renderer/src/pages/(main)/(context)/layout.tsx rename src/renderer/src/pages/(main)/{(context)/(nonhome) => (layer)/(subview)}/discover/index.tsx (100%) rename src/renderer/src/pages/(main)/{(context)/(nonhome) => (layer)/(subview)}/discover/layout.tsx (100%) rename src/renderer/src/pages/(main)/{(context)/(nonhome) => (layer)/(subview)}/layout.tsx (100%) rename src/renderer/src/pages/(main)/{(context)/(nonhome) => (layer)/(subview)}/profile/index.tsx (100%) rename src/renderer/src/pages/(main)/{(context)/(nonhome) => (layer)/(subview)}/profile/layout.tsx (100%) rename src/renderer/src/pages/(main)/{(context) => (layer)/feeds/[feedId]}/index.tsx (91%) create mode 100644 src/renderer/src/pages/(main)/(layer)/layout.tsx create mode 100644 src/renderer/src/pages/(main)/index.tsx diff --git a/icons/mgc/line_cute_re.svg b/icons/mgc/line_cute_re.svg new file mode 100644 index 000000000..1878e76c1 --- /dev/null +++ b/icons/mgc/line_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/lib/route-builder.test.ts b/src/renderer/src/lib/route-builder.test.ts index b3c61a373..875c4129a 100644 --- a/src/renderer/src/lib/route-builder.test.ts +++ b/src/renderer/src/lib/route-builder.test.ts @@ -10,8 +10,8 @@ describe("test route builder", () => { "./pages/(external)/layout.tsx": fakePromise, "./pages/(external)/(with-layout)/index.tsx": fakePromise, "./pages/(external)/(with-layout)/layout.tsx": fakePromise, - "./pages/(external)/(with-layout)/feed/[:id]/index.tsx": fakePromise, - "./pages/(external)/(with-layout)/feed/[:id]/layout.tsx": fakePromise, + "./pages/(external)/(with-layout)/feed/[id]/index.tsx": fakePromise, + "./pages/(external)/(with-layout)/feed/[id]/layout.tsx": fakePromise, "./pages/(main)/layout.tsx": fakePromise, "./pages/(main)/(context)/layout.tsx": fakePromise, diff --git a/src/renderer/src/lib/route-builder.ts b/src/renderer/src/lib/route-builder.ts index 659893e14..4595f3677 100644 --- a/src/renderer/src/lib/route-builder.ts +++ b/src/renderer/src/lib/route-builder.ts @@ -189,7 +189,7 @@ const normalizePathKey = (key: string) => { return "" } - if (key.startsWith("[:") && key.endsWith("]")) { + if (key.startsWith("[") && key.endsWith("]")) { return `:${key.slice(2, -1)}` } return key diff --git a/src/renderer/src/modules/feed-column/item.tsx b/src/renderer/src/modules/feed-column/item.tsx index 5e1392473..04bb8de5c 100644 --- a/src/renderer/src/modules/feed-column/item.tsx +++ b/src/renderer/src/modules/feed-column/item.tsx @@ -21,6 +21,7 @@ import { useUnreadStore, } from "@renderer/store" import { useMutation } from "@tanstack/react-query" +import { useNavigate } from "react-router-dom" import { toast } from "sonner" import { FeedForm } from "../discover/feed-form" @@ -97,6 +98,7 @@ export function FeedItem({ const feedUnread = useUnreadStore((state) => state.data[feed.feedId] || 0) const { present } = useModalStack() + const navigate = useNavigate() return (
{ e.stopPropagation() setFeedActive(feed) + navigate(`/feeds/${feed.feedId}`) }} onDoubleClick={() => { window.open( diff --git a/src/renderer/src/pages/(external)/(with-layout)/feed/[:id]/index.tsx b/src/renderer/src/pages/(external)/(with-layout)/feed/[id]/index.tsx similarity index 100% rename from src/renderer/src/pages/(external)/(with-layout)/feed/[:id]/index.tsx rename to src/renderer/src/pages/(external)/(with-layout)/feed/[id]/index.tsx diff --git a/src/renderer/src/pages/(external)/(with-layout)/feed/[:id]/layout.tsx b/src/renderer/src/pages/(external)/(with-layout)/feed/[id]/layout.tsx similarity index 100% rename from src/renderer/src/pages/(external)/(with-layout)/feed/[:id]/layout.tsx rename to src/renderer/src/pages/(external)/(with-layout)/feed/[id]/layout.tsx diff --git a/src/renderer/src/pages/(main)/(context)/layout.tsx b/src/renderer/src/pages/(main)/(context)/layout.tsx deleted file mode 100644 index 0341d292e..000000000 --- a/src/renderer/src/pages/(main)/(context)/layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { setMainContainerElement } from "@renderer/atoms" -import { FeedColumn } from "@renderer/modules/feed-column" -import { Outlet } from "react-router-dom" - -export function Component() { - return ( -
-
- -
- {/* NOTE: tabIndex for main element can get by `document.activeElement` */} -
- -
-
- ) -} diff --git a/src/renderer/src/pages/(main)/(context)/(nonhome)/discover/index.tsx b/src/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx similarity index 100% rename from src/renderer/src/pages/(main)/(context)/(nonhome)/discover/index.tsx rename to src/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx diff --git a/src/renderer/src/pages/(main)/(context)/(nonhome)/discover/layout.tsx b/src/renderer/src/pages/(main)/(layer)/(subview)/discover/layout.tsx similarity index 100% rename from src/renderer/src/pages/(main)/(context)/(nonhome)/discover/layout.tsx rename to src/renderer/src/pages/(main)/(layer)/(subview)/discover/layout.tsx diff --git a/src/renderer/src/pages/(main)/(context)/(nonhome)/layout.tsx b/src/renderer/src/pages/(main)/(layer)/(subview)/layout.tsx similarity index 100% rename from src/renderer/src/pages/(main)/(context)/(nonhome)/layout.tsx rename to src/renderer/src/pages/(main)/(layer)/(subview)/layout.tsx diff --git a/src/renderer/src/pages/(main)/(context)/(nonhome)/profile/index.tsx b/src/renderer/src/pages/(main)/(layer)/(subview)/profile/index.tsx similarity index 100% rename from src/renderer/src/pages/(main)/(context)/(nonhome)/profile/index.tsx rename to src/renderer/src/pages/(main)/(layer)/(subview)/profile/index.tsx diff --git a/src/renderer/src/pages/(main)/(context)/(nonhome)/profile/layout.tsx b/src/renderer/src/pages/(main)/(layer)/(subview)/profile/layout.tsx similarity index 100% rename from src/renderer/src/pages/(main)/(context)/(nonhome)/profile/layout.tsx rename to src/renderer/src/pages/(main)/(layer)/(subview)/profile/layout.tsx diff --git a/src/renderer/src/pages/(main)/(context)/index.tsx b/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/index.tsx similarity index 91% rename from src/renderer/src/pages/(main)/(context)/index.tsx rename to src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/index.tsx index 33298a4e8..ab859c9c1 100644 --- a/src/renderer/src/pages/(main)/(context)/index.tsx +++ b/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/index.tsx @@ -3,13 +3,12 @@ import { cn } from "@renderer/lib/utils" import { EntryColumn } from "@renderer/modules/entry-column" import { EntryContent } from "@renderer/modules/entry-content" import { - feedActions, uiActions, useFeedStore, useUIStore, } from "@renderer/store" import { AnimatePresence } from "framer-motion" -import { useEffect, useMemo, useRef } from "react" +import { useMemo, useRef } from "react" import { useResizable } from "react-resizable-layout" import { useShallow } from "zustand/react/shallow" @@ -20,10 +19,7 @@ export function Component() { activeEntry: state.activeEntryId, })), ) - const { setActiveEntry } = feedActions - useEffect(() => { - setActiveEntry(null) - }, [activeList?.id]) + const containerRef = useRef(null) // Memo this initial value to avoid re-render diff --git a/src/renderer/src/pages/(main)/(layer)/layout.tsx b/src/renderer/src/pages/(main)/(layer)/layout.tsx new file mode 100644 index 000000000..0cb99dba7 --- /dev/null +++ b/src/renderer/src/pages/(main)/(layer)/layout.tsx @@ -0,0 +1 @@ +export { Outlet as Component } from "react-router-dom" diff --git a/src/renderer/src/pages/(main)/index.tsx b/src/renderer/src/pages/(main)/index.tsx new file mode 100644 index 000000000..ab859c9c1 --- /dev/null +++ b/src/renderer/src/pages/(main)/index.tsx @@ -0,0 +1,70 @@ +import { views } from "@renderer/lib/constants" +import { cn } from "@renderer/lib/utils" +import { EntryColumn } from "@renderer/modules/entry-column" +import { EntryContent } from "@renderer/modules/entry-content" +import { + uiActions, + useFeedStore, + useUIStore, +} from "@renderer/store" +import { AnimatePresence } from "framer-motion" +import { useMemo, useRef } from "react" +import { useResizable } from "react-resizable-layout" +import { useShallow } from "zustand/react/shallow" + +export function Component() { + const { activeEntry, activeList } = useFeedStore( + useShallow((state) => ({ + activeList: state.activeList, + activeEntry: state.activeEntryId, + })), + ) + + const containerRef = useRef(null) + + // Memo this initial value to avoid re-render + // eslint-disable-next-line react-compiler/react-compiler + const entryColWidth = useMemo(() => useUIStore.getState().entryColWidth, []) + + const { position, separatorProps } = useResizable({ + axis: "x", + min: 300, + max: 450, + initial: entryColWidth, + containerRef, + onResizeEnd({ position }) { + uiActions.setEntryColWidth(position) + }, + }) + + const inWideMode = activeList && views[activeList.view].wideMode + return ( +
+
+ +
+ {!inWideMode && ( +
+ )} + + {!inWideMode && ( +
+ +
+ )} +
+
+ ) +} diff --git a/src/renderer/src/pages/(main)/layout.tsx b/src/renderer/src/pages/(main)/layout.tsx index 6702108cd..0341d292e 100644 --- a/src/renderer/src/pages/(main)/layout.tsx +++ b/src/renderer/src/pages/(main)/layout.tsx @@ -1,22 +1,17 @@ -import { feedActions, useFeedStore } from "@renderer/store" -import { useEffect } from "react" -import { Outlet, useNavigate } from "react-router-dom" +import { setMainContainerElement } from "@renderer/atoms" +import { FeedColumn } from "@renderer/modules/feed-column" +import { Outlet } from "react-router-dom" -function MainLayout() { - const navigate = useNavigate() - - const changed = useFeedStore( - (state) => `${state.activeList?.view}-${state.activeList?.id}`, +export function Component() { + return ( +
+
+ +
+ {/* NOTE: tabIndex for main element can get by `document.activeElement` */} +
+ +
+
) - - useEffect(() => { - feedActions.setActiveEntry(null) - if (changed) { - navigate("/") - } - }, [changed]) - - return } - -export { MainLayout as Component } diff --git a/src/renderer/src/router.tsx b/src/renderer/src/router.tsx index 23e7f7386..ed417bc4d 100644 --- a/src/renderer/src/router.tsx +++ b/src/renderer/src/router.tsx @@ -5,7 +5,6 @@ import { buildGlobRoutes } from "./lib/route-builder" const globTree = import.meta.glob("./pages/**/*.tsx") const tree = buildGlobRoutes(globTree) -// console.log(tree) export const router = createBrowserRouter([ { From e7913dfecf025953530399915d5521073af8b5dd Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 16:07:02 +0800 Subject: [PATCH 2/8] refactor: feed store Signed-off-by: Innei --- src/renderer/src/hooks/biz/useAsRead.ts | 7 +- .../src/hooks/biz/useNavigateEntry.ts | 43 +++++++++++ src/renderer/src/hooks/biz/useRouteParams.ts | 47 ++++++++++++ src/renderer/src/lib/constants.tsx | 5 +- src/renderer/src/lib/route-builder.ts | 2 +- src/renderer/src/models/types.ts | 13 ++-- .../src/modules/entry-column/index.tsx | 54 ++++++------- .../src/modules/entry-column/item.tsx | 19 +++-- .../src/modules/entry-content/index.tsx | 6 +- .../src/modules/feed-column/category.tsx | 16 ++-- .../src/modules/feed-column/index.tsx | 38 +++++----- src/renderer/src/modules/feed-column/item.tsx | 61 +++++++-------- src/renderer/src/modules/feed-column/list.tsx | 21 +++--- .../feeds/[feedId]/[entryId]/index.tsx | 20 +++++ .../feeds/[feedId]/{index.tsx => layout.tsx} | 30 ++------ src/renderer/src/pages/(main)/index.tsx | 75 ++----------------- src/renderer/src/store/entry/hooks.ts | 2 +- 17 files changed, 246 insertions(+), 213 deletions(-) create mode 100644 src/renderer/src/hooks/biz/useNavigateEntry.ts create mode 100644 src/renderer/src/hooks/biz/useRouteParams.ts create mode 100644 src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx rename src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/{index.tsx => layout.tsx} (65%) diff --git a/src/renderer/src/hooks/biz/useAsRead.ts b/src/renderer/src/hooks/biz/useAsRead.ts index bdc549b82..713ec9f10 100644 --- a/src/renderer/src/hooks/biz/useAsRead.ts +++ b/src/renderer/src/hooks/biz/useAsRead.ts @@ -1,10 +1,11 @@ import { FEED_COLLECTION_LIST, levels } from "@renderer/lib/constants" import type { EntryModel } from "@renderer/models" -import { useFeedStore } from "@renderer/store" + +import { useRouteParms } from "./useRouteParams" export function useAsRead(entry?: EntryModel) { - const activeList = useFeedStore((state) => state.activeList) + const { feedId, level } = useRouteParms() if (!entry) return false - return entry.read && !(activeList?.level === levels.folder && activeList?.id === FEED_COLLECTION_LIST) + return entry.read && !(level === levels.folder && feedId === FEED_COLLECTION_LIST) } diff --git a/src/renderer/src/hooks/biz/useNavigateEntry.ts b/src/renderer/src/hooks/biz/useNavigateEntry.ts new file mode 100644 index 000000000..efbd6fe71 --- /dev/null +++ b/src/renderer/src/hooks/biz/useNavigateEntry.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { ROUTE_FEED_PENDING } from "@renderer/lib/constants" +import type { FeedViewType } from "@renderer/lib/enum" +import { isUndefined } from "lodash-es" +import { useCallback } from "react" +import { useNavigate, useParams, useSearchParams } from "react-router-dom" + +type NavigateEntryOptions = Partial<{ + feedId: string | null + entryId: string | null + view: FeedViewType + level: string | null +}> +/** + * @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level` + */ +export const useNavigateEntry = () => { + const [searchParams] = useSearchParams() + const navigate = useNavigate() + const params = useParams() + return useCallback( + (options: NavigateEntryOptions) => { + const { entryId, feedId, level, view } = options || {} + let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING + + if ("feedId" in options && feedId === null) { + finalFeedId = ROUTE_FEED_PENDING + } + + const nextSearchParams = new URLSearchParams(searchParams) + + !isUndefined(view) && nextSearchParams.set("view", view.toString()) + level && nextSearchParams.set("level", level.toString()) + + return navigate( + `/feeds/${finalFeedId}/${ + entryId || ROUTE_FEED_PENDING + }?${nextSearchParams.toString()}`, + ) + }, + [navigate, params.feedId, searchParams], + ) +} diff --git a/src/renderer/src/hooks/biz/useRouteParams.ts b/src/renderer/src/hooks/biz/useRouteParams.ts new file mode 100644 index 000000000..266328a3a --- /dev/null +++ b/src/renderer/src/hooks/biz/useRouteParams.ts @@ -0,0 +1,47 @@ +import { FeedViewType } from "@renderer/lib/enum" +import { useParams, useSearchParams } from "react-router-dom" +// '0', '1', '2', '3', '4', '5', +const FeedViewTypeValues = (() => { + const values = Object.values(FeedViewType) + return values.slice(values.length / 2).map((v) => v.toString()) +})() +export const useRouteView = () => { + const [search] = useSearchParams() + const view = search.get("view") + + return ( + (view && FeedViewTypeValues.includes(view) ? + +view : + FeedViewType.Articles) || FeedViewType.Articles + ) +} + +export const useRouteEntryId = () => { + const { entryId } = useParams() + return entryId +} + +export const useRouteFeedId = () => { + const { feedId } = useParams() + return feedId +} + +export const useRouteParms = () => { + const params = useParams() + const [search] = useSearchParams() + const view = useRouteView() + + let feedId: string | number = params.feedId! + + // If feedId is a number, it's a FeedViewType + if (feedId && FeedViewTypeValues.includes(feedId as string)) { + feedId = Number.parseInt(feedId as string) + } + + return { + view, + entryId: params.entryId || undefined, + feedId: params.feedId || undefined, + level: search.get("level") || undefined, + } +} diff --git a/src/renderer/src/lib/constants.tsx b/src/renderer/src/lib/constants.tsx index 98afc818c..f3323e2ba 100644 --- a/src/renderer/src/lib/constants.tsx +++ b/src/renderer/src/lib/constants.tsx @@ -69,9 +69,12 @@ export const settingTabs = [ }, ] -//// +/// App export const APP_NAME = "Follow" /// Feed export const FEED_COLLECTION_LIST = "collections" /// Local storage keys export const QUERY_PERSIST_KEY = buildStorageNS("REACT_QUERY_OFFLINE_CACHE") + +/// Route Keys +export const ROUTE_FEED_PENDING = "pending" diff --git a/src/renderer/src/lib/route-builder.ts b/src/renderer/src/lib/route-builder.ts index 4595f3677..4eac6eb91 100644 --- a/src/renderer/src/lib/route-builder.ts +++ b/src/renderer/src/lib/route-builder.ts @@ -190,7 +190,7 @@ const normalizePathKey = (key: string) => { } if (key.startsWith("[") && key.endsWith("]")) { - return `:${key.slice(2, -1)}` + return `:${key.slice(1, -1)}` } return key } diff --git a/src/renderer/src/models/types.ts b/src/renderer/src/models/types.ts index 7845407af..93fad4d68 100644 --- a/src/renderer/src/models/types.ts +++ b/src/renderer/src/models/types.ts @@ -51,9 +51,10 @@ export type DiscoverResponse = Array< >[number] > -export type ActionsResponse = Exclude["data"], undefined>["rules"] +export type ActionsResponse = Exclude< + ExtractBizResponse["data"], + undefined +>["rules"] export type ListResponse = { code: number @@ -67,7 +68,7 @@ export type DataResponse = { data?: T } -export type ActiveEntryId = string | null +export type ActiveEntryId = Nullable export type SubscriptionModel = SubscriptionResponse[number] @@ -78,4 +79,6 @@ export type FeedListModel = { }[] } -export type SupportedLanguages = Parameters[0]["query"]["language"] +export type SupportedLanguages = Parameters< + typeof apiClient.ai.translation.$get +>[0]["query"]["language"] diff --git a/src/renderer/src/modules/entry-column/index.tsx b/src/renderer/src/modules/entry-column/index.tsx index 176a6040f..ac50c68fd 100644 --- a/src/renderer/src/modules/entry-column/index.tsx +++ b/src/renderer/src/modules/entry-column/index.tsx @@ -1,8 +1,5 @@ import { useMainContainerElement } from "@renderer/atoms" -import { - ActionButton, - StyledButton, -} from "@renderer/components/ui/button" +import { ActionButton, StyledButton } from "@renderer/components/ui/button" import { Popover, PopoverClose, @@ -10,6 +7,9 @@ import { PopoverTrigger, } from "@renderer/components/ui/popover" import { useRead, useRefValue } from "@renderer/hooks" +import { + useRouteParms, +} from "@renderer/hooks/biz/useRouteParams" import { apiClient } from "@renderer/lib/api-fetch" import { views } from "@renderer/lib/constants" import { buildStorageNS } from "@renderer/lib/ns" @@ -19,7 +19,6 @@ import { feedActions, getCurrentEntryId, subscriptionActions, - useFeedStore, } from "@renderer/store" import { entryActions } from "@renderer/store/entry/entry" import { @@ -44,7 +43,6 @@ import { import type { ListRange, VirtuosoHandle, VirtuosoProps } from "react-virtuoso" import { Virtuoso, VirtuosoGrid } from "react-virtuoso" import { useEventCallback } from "usehooks-ts" -import { useShallow } from "zustand/react/shallow" import { EmptyIcon } from "../../components/icons/empty" import { LoadingCircle } from "../../components/ui/loading" @@ -60,13 +58,9 @@ const unreadOnlyAtom = atomWithStorage( ) export function EntryColumn() { - const { activeList, activeEntryId } = useFeedStore((state) => ({ - activeList: state.activeList, - activeEntryId: state.activeEntryId, - })) const entries = useEntriesByView() const { entriesIds, isFetchingNextPage } = entries - + const { entryId: activeEntryId, view, feedId } = useRouteParms() const activeEntry = useEntry(activeEntryId) const markReadMutation = useRead() useEffect(() => { @@ -131,11 +125,9 @@ export function EntryColumn() { (_, entryId: string) => { if (!entryId) return null - return ( - - ) + return }, - [activeList?.view], + [view], ), } @@ -147,7 +139,7 @@ export function EntryColumn() { > {virtuosoOptions.totalCount === 0 ? ( - ) : activeList?.view && views[activeList.view].gridMode ? + ) : view && views[view].gridMode ? ( { - const activeList = useFeedStore(useShallow((state) => state.activeList)) + const activeList = useRouteParms() const unreadOnly = useAtomValue(unreadOnlyAtom) const query = useEntries({ level: activeList?.level, - id: activeList.id, + id: activeList.feedId, view: activeList?.view, ...(unreadOnly === true && { read: false }), }) - const entries = useEntryIdsByFeedIdOrView(activeList.id, { + const entries = useEntryIdsByFeedIdOrView(activeList.feedId!, { unread: unreadOnly, }) @@ -190,7 +182,7 @@ const useEntriesByView = () => { useEffect(() => { prevEntries.current = [] - }, [activeList.id]) + }, [activeList.feedId]) const localEntries = useMemo(() => { if (!unreadOnly) { prevEntries.current = [] @@ -238,31 +230,31 @@ const useEntriesByView = () => { const ListHeader: FC<{ totalCount: number }> = ({ totalCount }) => { - const activeList = useFeedStore(useShallow((state) => state.activeList)) + const routerParams = useRouteParms() const [unreadOnly, setUnreadOnly] = useAtom(unreadOnlyAtom) const [markPopoverOpen, setMarkPopoverOpen] = useState(false) const handleMarkAllAsRead = useCallback(async () => { - if (!activeList) return + if (!routerParams) return await apiClient.reads.all.$post({ json: { ...getEntriesParams({ - level: activeList?.level, - id: activeList?.id, - view: activeList?.view, + level: routerParams?.level, + id: routerParams?.feedId, + view: routerParams?.view, }), }, }) - if (typeof activeList.id === "number") { - subscriptionActions.markReadByView(activeList.view) + if (typeof routerParams.feedId === "number") { + subscriptionActions.markReadByView(routerParams.view) } else { - activeList.id.split(",").forEach((feedId) => { + routerParams.feedId?.split(",").forEach((feedId) => { entryActions.markReadByFeedId(feedId) }) } setMarkPopoverOpen(false) - }, [activeList]) + }, [routerParams]) return (
@@ -300,7 +292,7 @@ const ListHeader: FC<{
-
{activeList?.name}
+
{routerParams?.name}
{totalCount || 0} {" "} diff --git a/src/renderer/src/modules/entry-column/item.tsx b/src/renderer/src/modules/entry-column/item.tsx index 04776f218..8686d42b4 100644 --- a/src/renderer/src/modules/entry-column/item.tsx +++ b/src/renderer/src/modules/entry-column/item.tsx @@ -1,11 +1,12 @@ import { useAsRead, useBizQuery, useEntryActions } from "@renderer/hooks" +import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" +import { useRouteEntryId } from "@renderer/hooks/biz/useRouteParams" import { views } from "@renderer/lib/constants" import { FeedViewType } from "@renderer/lib/enum" import { showNativeMenu } from "@renderer/lib/native-menu" import { cn } from "@renderer/lib/utils" import type { EntryModel } from "@renderer/models" import { Queries } from "@renderer/queries" -import { feedActions, useFeedStore } from "@renderer/store" import { useEntry } from "@renderer/store/entry/hooks" import type { FC } from "react" import { memo, useCallback } from "react" @@ -40,7 +41,7 @@ function EntryItemImpl({ entry, view }: { entry: EntryModel, view?: number }) { }, ) - const activeEntry = useFeedStore((state) => state.activeEntryId) + const currentEntryId = useRouteEntryId() const asRead = useAsRead(entry) @@ -75,14 +76,17 @@ function EntryItemImpl({ entry, view }: { entry: EntryModel, view?: number }) { Item = ArticleItem } } - const handleKeyDown: React.KeyboardEventHandler = - useCallback(() => {}, []) + + const navigate = useNavigateEntry() const handleClick: React.MouseEventHandler = useCallback( (e) => { e.stopPropagation() - feedActions.setActiveEntry(entry.entries.id) + + navigate({ + entryId: entry.entries.id, + }) }, - [entry.entries.id], + [entry.entries.id, navigate], ) const handleDoubleClick: React.MouseEventHandler = useCallback( @@ -116,11 +120,10 @@ function EntryItemImpl({ entry, view }: { entry: EntryModel, view?: number }) { className={cn( "rounded-md bg-theme-background transition-colors", !views[view || 0].wideMode && - activeEntry === entry.entries.id && + currentEntryId === entry.entries.id && "bg-theme-item-active", asRead ? "text-zinc-500/90" : "text-zinc-900 dark:text-white/90", )} - onKeyDown={handleKeyDown} onClick={handleClick} onDoubleClick={handleDoubleClick} onContextMenu={handleContextMenu} diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index 63eaba30a..eacc46c13 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -17,10 +17,10 @@ import { EntryTranslation } from "../entry-column/translation" import { setEntryTitleMeta } from "./atoms" import { EntryHeader } from "./header" -export const EntryContent = ({ entry }: { entry: ActiveEntryId }) => { +export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => { const activeList = useFeedStore((state) => state.activeList) - if (!entry) { + if (!entryId) { return ( { ) } - return + return } function EntryContentRender({ entryId }: { entryId: string }) { diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx index 28f0babb7..c37d7b06b 100644 --- a/src/renderer/src/modules/feed-column/category.tsx +++ b/src/renderer/src/modules/feed-column/category.tsx @@ -2,13 +2,13 @@ import { Collapsible, CollapsibleTrigger, } from "@renderer/components/ui/collapsible" +import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" import { levels } from "@renderer/lib/constants" import { showNativeMenu } from "@renderer/lib/native-menu" import { cn } from "@renderer/lib/utils" import type { FeedListModel } from "@renderer/models" import { feedActions, - useFeedActiveList, useUnreadStore, } from "@renderer/store" import { AnimatePresence, m } from "framer-motion" @@ -16,9 +16,7 @@ import { useEffect, useState } from "react" import { useModalStack } from "../../components/ui/modal/stacked/hooks" import { CategoryRemoveDialogContent } from "./category-remove-dialog" -import { - CategoryRenameContent, -} from "./category-rename-dialog" +import { CategoryRenameContent } from "./category-rename-dialog" import { FeedItem } from "./item" const { setActiveList } = feedActions @@ -32,7 +30,7 @@ export function FeedCategory({ view?: number expansion: boolean }) { - const activeList = useFeedActiveList() + const routerParams = useRouteParms() const [open, setOpen] = useState(!data.name) @@ -75,8 +73,8 @@ export function FeedCategory({
{ @@ -125,13 +123,11 @@ export function FeedCategory({ - {!setActiveList && {data.name}} - {!!setActiveList && {data.name}} + {data.name}
{!!unread && (
{unread}
diff --git a/src/renderer/src/modules/feed-column/index.tsx b/src/renderer/src/modules/feed-column/index.tsx index 311c4bbc9..48abc3a1c 100644 --- a/src/renderer/src/modules/feed-column/index.tsx +++ b/src/renderer/src/modules/feed-column/index.tsx @@ -1,10 +1,10 @@ import { Logo } from "@renderer/components/icons/logo" import { ActionButton } from "@renderer/components/ui/button" import { ProfileButton } from "@renderer/components/user-button" +import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" import { APP_NAME, levels, views } from "@renderer/lib/constants" import { stopPropagation } from "@renderer/lib/dom" import { clamp, cn } from "@renderer/lib/utils" -import { feedActions } from "@renderer/store" import { useWheel } from "@use-gesture/react" import { m, useSpring } from "framer-motion" import { Lethargy } from "lethargy" @@ -17,7 +17,6 @@ import { FeedList } from "./list" const lethargy = new Lethargy() export function FeedColumn() { - const { setActiveList } = feedActions const carouselRef = useRef(null) const [active, setActive] = useState(0) @@ -56,16 +55,20 @@ export function FeedColumn() { const normalStyle = !window.electron || window.electron.process.platform !== "darwin" + const navigate = useNavigateEntry() + + const navigateBackHome = () => { + navigate({ + feedId: null, + entryId: null, + view: active, + level: levels.view, + }) + } return ( - setActiveList({ - level: levels.view, - id: active, - name: views[active].name, - view: active, - })} + onClick={navigateBackHome} >
{ e.stopPropagation() - setActiveList({ - level: levels.view, - id: active, - name: views[active].name, - view: active, - }) + + navigateBackHome() }} > @@ -116,11 +115,12 @@ export function FeedColumn() { )} onClick={(e) => { setActive(index) - setActiveList?.({ - level: "view", - id: index, - name: views[index].name, + + navigate({ + feedId: null, + entryId: null, view: index, + level: levels.view, }) e.stopPropagation() }} diff --git a/src/renderer/src/modules/feed-column/item.tsx b/src/renderer/src/modules/feed-column/item.tsx index 04bb8de5c..5203949d3 100644 --- a/src/renderer/src/modules/feed-column/item.tsx +++ b/src/renderer/src/modules/feed-column/item.tsx @@ -8,6 +8,8 @@ import { TooltipProvider, TooltipTrigger, } from "@renderer/components/ui/tooltip" +import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" +import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" import { apiClient } from "@renderer/lib/api-fetch" import { levels } from "@renderer/lib/constants" import dayjs from "@renderer/lib/dayjs" @@ -16,12 +18,10 @@ import { cn } from "@renderer/lib/utils" import type { SubscriptionResponse } from "@renderer/models" import { Queries } from "@renderer/queries" import { - feedActions, - useFeedActiveList, useUnreadStore, } from "@renderer/store" import { useMutation } from "@tanstack/react-query" -import { useNavigate } from "react-router-dom" +import { useCallback } from "react" import { toast } from "sonner" import { FeedForm } from "../discover/feed-form" @@ -35,25 +35,26 @@ export function FeedItem({ view?: number className?: string }) { - const activeList = useFeedActiveList() - const { setActiveList } = feedActions - - const setFeedActive = (feed: SubscriptionResponse[number]) => { - if (view === undefined) return - - setActiveList({ - level: levels.feed, - id: feed.feedId, - name: feed.feeds.title || "", - view, - }) - // focus to main container in order to let keyboard can navigate entry items by arrow keys - requestAnimationFrame(() => { - requestAnimationFrame(() => { - getMainContainerElement()?.focus() + const navigate = useNavigateEntry() + const handleNavigate: React.MouseEventHandler = useCallback( + (e) => { + e.stopPropagation() + if (view === undefined) return + navigate({ + feedId: feed.feedId, + entryId: null, + view, + level: levels.feed, }) - }) - } + // focus to main container in order to let keyboard can navigate entry items by arrow keys + requestAnimationFrame(() => { + requestAnimationFrame(() => { + getMainContainerElement()?.focus() + }) + }) + }, + [feed.feedId, navigate, view], + ) const deleteMutation = useMutation({ mutationFn: async (feed: SubscriptionResponse[number]) => @@ -98,21 +99,19 @@ export function FeedItem({ const feedUnread = useUnreadStore((state) => state.data[feed.feedId] || 0) const { present } = useModalStack() - const navigate = useNavigate() + + const routerParmas = useRouteParms() + return (
{ - e.stopPropagation() - setFeedActive(feed) - navigate(`/feeds/${feed.feedId}`) - }} + onClick={handleNavigate} onDoubleClick={() => { window.open( `${import.meta.env.VITE_WEB_URL}/feed/${feed.feedId}?view=${view}`, @@ -129,7 +128,9 @@ export function FeedItem({ click: () => { present({ title: "Edit Feed", - content: ({ dismiss }) => , + content: ({ dismiss }) => ( + + ), }) }, }, diff --git a/src/renderer/src/modules/feed-column/list.tsx b/src/renderer/src/modules/feed-column/list.tsx index 69fce739f..5c6914ec7 100644 --- a/src/renderer/src/modules/feed-column/list.tsx +++ b/src/renderer/src/modules/feed-column/list.tsx @@ -1,4 +1,6 @@ import { useBizQuery } from "@renderer/hooks" +import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" +import { useRouteFeedId } from "@renderer/hooks/biz/useRouteParams" import { FEED_COLLECTION_LIST, levels, views } from "@renderer/lib/constants" import { stopPropagation } from "@renderer/lib/dom" import type { FeedViewType } from "@renderer/lib/enum" @@ -7,7 +9,6 @@ import type { FeedListModel, SubscriptionResponse } from "@renderer/models" import { Queries } from "@renderer/queries" import { feedActions, - useFeedActiveList, useSubscriptionByView, useUnreadStore, } from "@renderer/store" @@ -94,7 +95,6 @@ export function FeedList({ }) { const [expansion, setExpansion] = useState(false) const data = useData(view) - const activeList = useFeedActiveList() useBizQuery(Queries.subscription.unreadAll()) @@ -108,8 +108,6 @@ export function FeedList({ return unread }) - const { setActiveList } = feedActions - const sortedByUnread = useUnreadStore((state) => data?.list?.sort( (a, b) => @@ -118,6 +116,9 @@ export function FeedList({ ), ) + const feedId = useRouteFeedId() + const navigate = useNavigateEntry() + return (
{!hideTitle && ( @@ -130,7 +131,7 @@ export function FeedList({ onClick={(e) => { e.stopPropagation() if (view !== undefined) { - setActiveList({ + feedActions.setActiveList({ level: levels.view, id: view, name: views[view].name, @@ -160,15 +161,15 @@ export function FeedList({
{ e.stopPropagation() if (view !== undefined) { - setActiveList({ - level: levels.folder, - id: FEED_COLLECTION_LIST, - name: "Collections", + navigate({ + entryId: null, + feedId: FEED_COLLECTION_LIST, + level: levels.feed, view, }) } diff --git a/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx b/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx new file mode 100644 index 000000000..190cba1d8 --- /dev/null +++ b/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx @@ -0,0 +1,20 @@ +import { useRouteView } from "@renderer/hooks/biz/useRouteParams" +import { ROUTE_FEED_PENDING, views } from "@renderer/lib/constants" +import { EntryContent } from "@renderer/modules/entry-content" +import { AnimatePresence } from "framer-motion" +import { useParams } from "react-router-dom" + +export const Component = () => { + const { entryId } = useParams() + const view = useRouteView() + const inWideMode = view ? views[view].wideMode : false + return ( + + {!inWideMode && ( +
+ +
+ )} +
+ ) +} diff --git a/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/index.tsx b/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/layout.tsx similarity index 65% rename from src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/index.tsx rename to src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/layout.tsx index ab859c9c1..ff13e2e36 100644 --- a/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/index.tsx +++ b/src/renderer/src/pages/(main)/(layer)/feeds/[feedId]/layout.tsx @@ -1,31 +1,20 @@ +import { useRouteView } from "@renderer/hooks/biz/useRouteParams" import { views } from "@renderer/lib/constants" import { cn } from "@renderer/lib/utils" import { EntryColumn } from "@renderer/modules/entry-column" -import { EntryContent } from "@renderer/modules/entry-content" -import { - uiActions, - useFeedStore, - useUIStore, -} from "@renderer/store" -import { AnimatePresence } from "framer-motion" +import { uiActions, useUIStore } from "@renderer/store" import { useMemo, useRef } from "react" import { useResizable } from "react-resizable-layout" -import { useShallow } from "zustand/react/shallow" +import { Outlet } from "react-router-dom" export function Component() { - const { activeEntry, activeList } = useFeedStore( - useShallow((state) => ({ - activeList: state.activeList, - activeEntry: state.activeEntryId, - })), - ) - const containerRef = useRef(null) // Memo this initial value to avoid re-render // eslint-disable-next-line react-compiler/react-compiler const entryColWidth = useMemo(() => useUIStore.getState().entryColWidth, []) - + const view = useRouteView() + const inWideMode = view ? views[view].wideMode : false const { position, separatorProps } = useResizable({ axis: "x", min: 300, @@ -37,7 +26,6 @@ export function Component() { }, }) - const inWideMode = activeList && views[activeList.view].wideMode return (
)} - - {!inWideMode && ( -
- -
- )} -
+
) } diff --git a/src/renderer/src/pages/(main)/index.tsx b/src/renderer/src/pages/(main)/index.tsx index ab859c9c1..c6d607ec7 100644 --- a/src/renderer/src/pages/(main)/index.tsx +++ b/src/renderer/src/pages/(main)/index.tsx @@ -1,70 +1,11 @@ -import { views } from "@renderer/lib/constants" -import { cn } from "@renderer/lib/utils" -import { EntryColumn } from "@renderer/modules/entry-column" -import { EntryContent } from "@renderer/modules/entry-content" -import { - uiActions, - useFeedStore, - useUIStore, -} from "@renderer/store" -import { AnimatePresence } from "framer-motion" -import { useMemo, useRef } from "react" -import { useResizable } from "react-resizable-layout" -import { useShallow } from "zustand/react/shallow" +import { levels, ROUTE_FEED_PENDING } from "@renderer/lib/constants" +import { FeedViewType } from "@renderer/lib/enum" +import { redirect } from "react-router-dom" export function Component() { - const { activeEntry, activeList } = useFeedStore( - useShallow((state) => ({ - activeList: state.activeList, - activeEntry: state.activeEntryId, - })), - ) - - const containerRef = useRef(null) - - // Memo this initial value to avoid re-render - // eslint-disable-next-line react-compiler/react-compiler - const entryColWidth = useMemo(() => useUIStore.getState().entryColWidth, []) - - const { position, separatorProps } = useResizable({ - axis: "x", - min: 300, - max: 450, - initial: entryColWidth, - containerRef, - onResizeEnd({ position }) { - uiActions.setEntryColWidth(position) - }, - }) - - const inWideMode = activeList && views[activeList.view].wideMode - return ( -
-
- -
- {!inWideMode && ( -
- )} - - {!inWideMode && ( -
- -
- )} -
-
- ) + return null } + +export const loader = () => + // navigate to the first feed + redirect(`/feeds/${ROUTE_FEED_PENDING}?view=${FeedViewType.Articles}&level=${levels.view}`) diff --git a/src/renderer/src/store/entry/hooks.ts b/src/renderer/src/store/entry/hooks.ts index ba0c746c9..37b217e56 100644 --- a/src/renderer/src/store/entry/hooks.ts +++ b/src/renderer/src/store/entry/hooks.ts @@ -10,7 +10,7 @@ interface EntryFilter { unread?: boolean } -export const useEntry = (entryId: string | null): EntryModel | null => +export const useEntry = (entryId: Nullable): EntryModel | null => useEntryStore(useShallow((state) => entryId ? state.flatMapEntries[entryId] : null)) // feedId: single feedId, multiple feedId joint by `,`, and `collections` export const useEntryIdsByFeedId = (feedId: string, filter?: EntryFilter) => From f26caf5122a8635865ef2e26f63dd1fc9193fa76 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 19:27:03 +0800 Subject: [PATCH 3/8] fix: feed store Signed-off-by: Innei --- src/renderer/src/hono.ts | 18 +---- .../src/hooks/biz/useEntryActions.tsx | 2 +- .../src/hooks/biz/useNavigateEntry.ts | 23 ++++-- src/renderer/src/hooks/biz/useRouteParams.ts | 1 + .../src/modules/entry-column/index.tsx | 28 +++++--- .../src/modules/entry-content/index.tsx | 11 +-- .../src/modules/feed-column/category.tsx | 33 +++++---- src/renderer/src/modules/feed-column/item.tsx | 71 ++++++++++++------- src/renderer/src/modules/feed-column/list.tsx | 25 ++++--- src/renderer/src/queries/entries.ts | 2 +- src/renderer/src/store/entry/hooks.ts | 2 +- src/renderer/src/store/entry/index.ts | 2 +- .../src/store/entry/{entry.ts => store.ts} | 5 ++ src/renderer/src/store/feed.ts | 38 ---------- src/renderer/src/store/feed/hooks.ts | 23 ++++++ src/renderer/src/store/feed/index.ts | 3 + src/renderer/src/store/feed/store.ts | 39 ++++++++++ src/renderer/src/store/feed/types.ts | 13 ++++ src/renderer/src/store/subscription.ts | 32 ++++----- src/renderer/src/store/utils/local.ts | 2 +- 20 files changed, 230 insertions(+), 143 deletions(-) rename src/renderer/src/store/entry/{entry.ts => store.ts} (95%) delete mode 100644 src/renderer/src/store/feed.ts create mode 100644 src/renderer/src/store/feed/hooks.ts create mode 100644 src/renderer/src/store/feed/index.ts create mode 100644 src/renderer/src/store/feed/store.ts create mode 100644 src/renderer/src/store/feed/types.ts diff --git a/src/renderer/src/hono.ts b/src/renderer/src/hono.ts index f7ebcf45e..3c5ede976 100644 --- a/src/renderer/src/hono.ts +++ b/src/renderer/src/hono.ts @@ -21,20 +21,6 @@ declare const routes: hono_hono_base.HonoBase /** * @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level` @@ -18,26 +22,37 @@ export const useNavigateEntry = () => { const [searchParams] = useSearchParams() const navigate = useNavigate() const params = useParams() + + const paramsRef = useRefValue(params) + const searchRef = useRefValue(searchParams) return useCallback( (options: NavigateEntryOptions) => { - const { entryId, feedId, level, view } = options || {} - let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING + const { entryId, feedId, level, view, category } = options || {} + let finalFeedId = feedId || paramsRef.current.feedId || ROUTE_FEED_PENDING if ("feedId" in options && feedId === null) { finalFeedId = ROUTE_FEED_PENDING } - const nextSearchParams = new URLSearchParams(searchParams) + const nextSearchParams = new URLSearchParams(searchRef.current) !isUndefined(view) && nextSearchParams.set("view", view.toString()) level && nextSearchParams.set("level", level.toString()) + if ("category" in options) { + if (!category) { + nextSearchParams.delete("category") + } else { + nextSearchParams.set("category", category.toString()) + } + } + return navigate( `/feeds/${finalFeedId}/${ entryId || ROUTE_FEED_PENDING }?${nextSearchParams.toString()}`, ) }, - [navigate, params.feedId, searchParams], + [navigate, paramsRef, searchRef], ) } diff --git a/src/renderer/src/hooks/biz/useRouteParams.ts b/src/renderer/src/hooks/biz/useRouteParams.ts index 266328a3a..4997f7ce0 100644 --- a/src/renderer/src/hooks/biz/useRouteParams.ts +++ b/src/renderer/src/hooks/biz/useRouteParams.ts @@ -43,5 +43,6 @@ export const useRouteParms = () => { entryId: params.entryId || undefined, feedId: params.feedId || undefined, level: search.get("level") || undefined, + category: search.get("category") || undefined, } } diff --git a/src/renderer/src/modules/entry-column/index.tsx b/src/renderer/src/modules/entry-column/index.tsx index ac50c68fd..102ee8826 100644 --- a/src/renderer/src/modules/entry-column/index.tsx +++ b/src/renderer/src/modules/entry-column/index.tsx @@ -7,7 +7,9 @@ import { PopoverTrigger, } from "@renderer/components/ui/popover" import { useRead, useRefValue } from "@renderer/hooks" +import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" import { + useRouteEntryId, useRouteParms, } from "@renderer/hooks/biz/useRouteParams" import { apiClient } from "@renderer/lib/api-fetch" @@ -16,11 +18,10 @@ import { buildStorageNS } from "@renderer/lib/ns" import { getEntriesParams } from "@renderer/lib/utils" import { useEntries } from "@renderer/queries/entries" import { - feedActions, - getCurrentEntryId, + entryActions, subscriptionActions, + useFeedHeaderTitle, } from "@renderer/store" -import { entryActions } from "@renderer/store/entry/entry" import { useEntry, useEntryIdsByFeedIdOrView, @@ -131,10 +132,14 @@ export function EntryColumn() { ), } + const navigate = useNavigateEntry() return (
feedActions.setActiveEntry(null)} + onClick={() => + navigate({ + entryId: null, + })} data-total-count={virtuosoOptions.totalCount} > @@ -256,6 +261,7 @@ const ListHeader: FC<{ setMarkPopoverOpen(false) }, [routerParams]) + const headerTitle = useFeedHeaderTitle() return (
@@ -292,7 +298,7 @@ const ListHeader: FC<{
-
{routerParams?.name}
+
{headerTitle}
{totalCount || 0} {" "} @@ -343,6 +349,9 @@ const EntryList: FC> = ({ const dataRef = useRefValue(virtuosoOptions.data!) + const currentEntryIdRef = useRefValue(useRouteEntryId()) + + const navigate = useNavigateEntry() useEffect(() => { if (!virtuosoRef.current) return if (!$mainContainer) return @@ -364,7 +373,7 @@ const EntryList: FC> = ({ hotkeys(registerKeys, scope, (handler) => { const data = dataRef.current const currentActiveEntryIndex = data.indexOf( - getCurrentEntryId() || "", + currentEntryIdRef.current || "", ) switch (handler.key) { @@ -387,7 +396,10 @@ const EntryList: FC> = ({ index: nextIndex, }) const nextId = data![nextIndex] - feedActions.setActiveEntry(nextId) + + navigate({ + entryId: nextId, + }) } } }) @@ -409,7 +421,7 @@ const EntryList: FC> = ({ document.removeEventListener("focusin", focusHandler) } - }, [$mainContainer]) + }, [$mainContainer, currentEntryIdRef, dataRef, navigate]) const handleKeyDown: React.KeyboardEventHandler = useCallback( (e) => { diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index eacc46c13..74bc9cfc6 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -8,7 +8,7 @@ import { WrappedElementProvider, } from "@renderer/providers/wrapped-element-provider" import { Queries } from "@renderer/queries" -import { useEntry, useFeedStore } from "@renderer/store" +import { useEntry, useFeedHeaderTitle } from "@renderer/store" import { m } from "framer-motion" import { useEffect, useState } from "react" @@ -18,8 +18,7 @@ import { setEntryTitleMeta } from "./atoms" import { EntryHeader } from "./header" export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => { - const activeList = useFeedStore((state) => state.activeList) - + const title = useFeedHeaderTitle() if (!entryId) { return ( { animate={{ opacity: 1, y: 0 }} > - {activeList?.name} + {title} ) } @@ -169,7 +168,9 @@ const TitleMetaHandler: Component<{ } = useEntry(entryId)! useEffect(() => { - if (!isAtTop && entryTitle && feedTitle) { setEntryTitleMeta({ title: entryTitle, description: feedTitle }) } + if (!isAtTop && entryTitle && feedTitle) { + setEntryTitleMeta({ title: entryTitle, description: feedTitle }) + } return () => { setEntryTitleMeta(null) } diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx index c37d7b06b..9341bdc4f 100644 --- a/src/renderer/src/modules/feed-column/category.tsx +++ b/src/renderer/src/modules/feed-column/category.tsx @@ -2,25 +2,21 @@ import { Collapsible, CollapsibleTrigger, } from "@renderer/components/ui/collapsible" +import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" import { levels } from "@renderer/lib/constants" import { showNativeMenu } from "@renderer/lib/native-menu" import { cn } from "@renderer/lib/utils" import type { FeedListModel } from "@renderer/models" -import { - feedActions, - useUnreadStore, -} from "@renderer/store" +import { useUnreadStore } from "@renderer/store" import { AnimatePresence, m } from "framer-motion" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { useModalStack } from "../../components/ui/modal/stacked/hooks" import { CategoryRemoveDialogContent } from "./category-remove-dialog" import { CategoryRenameContent } from "./category-rename-dialog" import { FeedItem } from "./item" -const { setActiveList } = feedActions - export function FeedCategory({ data, view, @@ -42,13 +38,17 @@ export function FeedCategory({ } }, [expansion]) + const navigate = useNavigateEntry() + const setCategoryActive = () => { if (view !== undefined) { - setActiveList({ + navigate({ + entryId: null, + // TODO joint feedId is too long, need to be optimized + feedId: data.list.map((feed) => feed.feedId).join(","), level: levels.folder, - id: data.list.map((feed) => feed.feedId).join(","), - name: data.name, view, + category: data.name, }) } } @@ -62,6 +62,13 @@ export function FeedCategory({ (a, b) => (state.data[b.feedId] || 0) - (state.data[a.feedId] || 0), ), ) + + const isActive = useMemo( + () => + routerParams?.level === levels.folder && + routerParams.feedId === data.list.map((feed) => feed.feedId).join(","), + [data.list, routerParams.feedId, routerParams?.level], + ) const { present } = useModalStack() return ( { e.stopPropagation() @@ -154,7 +159,7 @@ export function FeedCategory({ {sortByUnreadFeedList.map((feed) => ( diff --git a/src/renderer/src/modules/feed-column/item.tsx b/src/renderer/src/modules/feed-column/item.tsx index 5203949d3..d2b738d10 100644 --- a/src/renderer/src/modules/feed-column/item.tsx +++ b/src/renderer/src/modules/feed-column/item.tsx @@ -15,23 +15,22 @@ import { levels } from "@renderer/lib/constants" import dayjs from "@renderer/lib/dayjs" import { showNativeMenu } from "@renderer/lib/native-menu" import { cn } from "@renderer/lib/utils" -import type { SubscriptionResponse } from "@renderer/models" import { Queries } from "@renderer/queries" -import { - useUnreadStore, -} from "@renderer/store" +import type { SubscriptionPlainModel } from "@renderer/store" +import { getFeedById, useFeedById, useUnreadStore } from "@renderer/store" import { useMutation } from "@tanstack/react-query" import { useCallback } from "react" import { toast } from "sonner" import { FeedForm } from "../discover/feed-form" +type FeedItemData = SubscriptionPlainModel export function FeedItem({ - feed, + subscription, view, className, }: { - feed: SubscriptionResponse[number] + subscription: FeedItemData view?: number className?: string }) { @@ -41,10 +40,11 @@ export function FeedItem({ e.stopPropagation() if (view === undefined) return navigate({ - feedId: feed.feedId, + feedId: subscription.feedId, entryId: null, view, level: levels.feed, + category: null, }) // focus to main container in order to let keyboard can navigate entry items by arrow keys requestAnimationFrame(() => { @@ -53,11 +53,11 @@ export function FeedItem({ }) }) }, - [feed.feedId, navigate, view], + [subscription.feedId, navigate, view], ) const deleteMutation = useMutation({ - mutationFn: async (feed: SubscriptionResponse[number]) => + mutationFn: async (feed: SubscriptionPlainModel) => apiClient.subscriptions.$delete({ json: { feedId: feed.feedId, @@ -67,13 +67,15 @@ export function FeedItem({ onSuccess: (_, variables) => { Queries.subscription.byView(variables.view).invalidate() + const feed = getFeedById(variables.feedId) toast( <> Feed {" "} - {variables.feeds.title} + {feed.title} {" "} - has been unfollowed. + has been + unfollowed. , { duration: 3000, @@ -82,14 +84,14 @@ export function FeedItem({ onClick: async () => { await apiClient.subscriptions.$post({ json: { - url: variables.feeds.url, + url: feed.url, view: variables.view, category: variables.category, isPrivate: variables.isPrivate, }, }) - Queries.subscription.byView(feed.view).invalidate() + Queries.subscription.byView(variables.view).invalidate() }, }, }, @@ -97,24 +99,31 @@ export function FeedItem({ }, }) - const feedUnread = useUnreadStore((state) => state.data[feed.feedId] || 0) + const feedUnread = useUnreadStore( + (state) => state.data[subscription.feedId] || 0, + ) const { present } = useModalStack() const routerParmas = useRouteParms() + const feed = useFeedById(subscription.feedId) + + if (!feed) return null return (
{ window.open( - `${import.meta.env.VITE_WEB_URL}/feed/${feed.feedId}?view=${view}`, + `${import.meta.env.VITE_WEB_URL}/feed/${ + subscription.feedId + }?view=${view}`, "_blank", ) }} @@ -129,7 +138,11 @@ export function FeedItem({ present({ title: "Edit Feed", content: ({ dismiss }) => ( - + ), }) }, @@ -137,7 +150,7 @@ export function FeedItem({ { type: "text", label: "Unfollow", - click: () => deleteMutation.mutate(feed), + click: () => deleteMutation.mutate(subscription), }, { type: "separator", @@ -148,7 +161,7 @@ export function FeedItem({ click: () => window.open( `${import.meta.env.VITE_WEB_URL}/feed/${ - feed.feedId + subscription.feedId }?view=${view}`, "_blank", ), @@ -156,8 +169,12 @@ export function FeedItem({ { type: "text", label: "Open Site in Browser", - click: () => - feed.feeds.siteUrl && window.open(feed.feeds.siteUrl, "_blank"), + click: () => { + const feed = getFeedById(subscription.feedId) + if (feed) { + feed.siteUrl && window.open(feed.siteUrl, "_blank") + } + }, }, ], e, @@ -167,12 +184,12 @@ export function FeedItem({
- -
{feed.feeds.title}
- {feed.feeds.errorAt && ( + +
{feed.title}
+ {feed.errorAt && ( @@ -184,7 +201,7 @@ export function FeedItem({ {" "} {dayjs .duration( - dayjs(feed.feeds.errorAt).diff(dayjs(), "minute"), + dayjs(feed.errorAt).diff(dayjs(), "minute"), "minute", ) .humanize(true)} @@ -193,7 +210,7 @@ export function FeedItem({ )} - {feed.isPrivate && ( + {subscription.isPrivate && ( diff --git a/src/renderer/src/modules/feed-column/list.tsx b/src/renderer/src/modules/feed-column/list.tsx index 5c6914ec7..9b70d2a9f 100644 --- a/src/renderer/src/modules/feed-column/list.tsx +++ b/src/renderer/src/modules/feed-column/list.tsx @@ -5,10 +5,13 @@ import { FEED_COLLECTION_LIST, levels, views } from "@renderer/lib/constants" import { stopPropagation } from "@renderer/lib/dom" import type { FeedViewType } from "@renderer/lib/enum" import { cn } from "@renderer/lib/utils" -import type { FeedListModel, SubscriptionResponse } from "@renderer/models" +import type { + FeedListModel, +} from "@renderer/models" import { Queries } from "@renderer/queries" +import type { SubscriptionPlainModel } from "@renderer/store" import { - feedActions, + getFeedById, useSubscriptionByView, useUnreadStore, } from "@renderer/store" @@ -31,7 +34,7 @@ const useData = (view: FeedViewType) => { list: Record< string, { - list: SubscriptionResponse + list: SubscriptionPlainModel[] } > } @@ -39,8 +42,9 @@ const useData = (view: FeedViewType) => { const subscriptions = structuredClone(data) for (const subscription of subscriptions) { - if (!subscription.category && subscription.feeds.siteUrl) { - const { domain } = parse(subscription.feeds.siteUrl) + const feed = getFeedById(subscription.feedId) + if (!subscription.category && feed.siteUrl) { + const { domain } = parse(feed.siteUrl) if (domain) { if (!domains[domain]) { domains[domain] = 0 @@ -51,11 +55,12 @@ const useData = (view: FeedViewType) => { } for (const subscription of subscriptions) { + const feed = getFeedById(subscription.feedId) if (!subscription.category) { - if (subscription.feeds.siteUrl) { + if (feed.siteUrl) { // FIXME @DIYgod // The logic here makes it impossible to remove the auto-generated category based on domain - const { domain } = parse(subscription.feeds.siteUrl) + const { domain } = parse(feed.siteUrl) if (domain && domains[domain] > 1) { subscription.category = domain.slice(0, 1).toUpperCase() + domain.slice(1) @@ -131,10 +136,10 @@ export function FeedList({ onClick={(e) => { e.stopPropagation() if (view !== undefined) { - feedActions.setActiveList({ + navigate({ + entryId: null, + feedId: null, level: levels.view, - id: view, - name: views[view].name, view, }) } diff --git a/src/renderer/src/queries/entries.ts b/src/renderer/src/queries/entries.ts index 09223734f..af1192c15 100644 --- a/src/renderer/src/queries/entries.ts +++ b/src/renderer/src/queries/entries.ts @@ -1,7 +1,7 @@ import { useBizInfiniteQuery, useBizQuery } from "@renderer/hooks" import { apiClient } from "@renderer/lib/api-fetch" import { defineQuery } from "@renderer/lib/defineQuery" -import { entryActions } from "@renderer/store/entry/entry" +import { entryActions } from "@renderer/store" export const entries = { entries: ({ diff --git a/src/renderer/src/store/entry/hooks.ts b/src/renderer/src/store/entry/hooks.ts index 37b217e56..6c96dc2a0 100644 --- a/src/renderer/src/store/entry/hooks.ts +++ b/src/renderer/src/store/entry/hooks.ts @@ -4,7 +4,7 @@ import type { EntryModel } from "@renderer/models" import { useShallow } from "zustand/react/shallow" import { useFeedIdByView } from "../subscription" -import { useEntryStore } from "./entry" +import { useEntryStore } from "./store" interface EntryFilter { unread?: boolean diff --git a/src/renderer/src/store/entry/index.ts b/src/renderer/src/store/entry/index.ts index 812f374e0..a92fe14dc 100644 --- a/src/renderer/src/store/entry/index.ts +++ b/src/renderer/src/store/entry/index.ts @@ -1,3 +1,3 @@ -export * from "./entry" export * from "./hooks" +export * from "./store" export * from "./types" diff --git a/src/renderer/src/store/entry/entry.ts b/src/renderer/src/store/entry/store.ts similarity index 95% rename from src/renderer/src/store/entry/entry.ts rename to src/renderer/src/store/entry/store.ts index 145c0d973..b30dc5713 100644 --- a/src/renderer/src/store/entry/entry.ts +++ b/src/renderer/src/store/entry/store.ts @@ -4,6 +4,7 @@ import type { EntryModel } from "@renderer/models" import { produce } from "immer" import { merge, omit } from "lodash-es" +import { feedActions } from "../feed" import { unreadActions } from "../unread" import { createZustandStore, getStoreActions } from "../utils/helper" import type { EntryActions, EntryState } from "./types" @@ -132,6 +133,10 @@ export const useEntryStore = createZustandStore( draft.flatMapEntries[entry.entries.id] || {}, entry, ) + + const feeds = entries.map((entry) => entry.feeds) + // Insert to feed store + feedActions.upsertMany(feeds) } return draft }), diff --git a/src/renderer/src/store/feed.ts b/src/renderer/src/store/feed.ts deleted file mode 100644 index 19ce197ba..000000000 --- a/src/renderer/src/store/feed.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ActiveEntryId, ActiveList } from "@renderer/models" -import { create } from "zustand" - -import { getStoreActions } from "./utils/helper" - -interface FeedStoreActions { - setActiveList: (value: ActiveList) => void - setActiveEntry: (value: ActiveEntryId) => void -} -interface FeedStoreState { - activeList: ActiveList - activeEntryId: ActiveEntryId -} - -type FeedStore = FeedStoreState & Readonly -export const useFeedStore = create((set) => ({ - activeList: { - level: "view", - id: 0, - name: "Articles", - view: 0, - }, - activeEntryId: null, - activeEntryIndex: null, - - // Actions - setActiveEntry: (value) => set({ activeEntryId: value }), - setActiveList: (value) => set({ activeList: value }), -})) - -export const feedActions = getStoreActions(useFeedStore) - -export const getCurrentFeedId = () => useFeedStore.getState().activeList.id -export const getCurrentEntryId = () => useFeedStore.getState().activeEntryId - -/** Hooks */ -export const useFeedActiveList = () => - useFeedStore((state) => state.activeList) diff --git a/src/renderer/src/store/feed/hooks.ts b/src/renderer/src/store/feed/hooks.ts new file mode 100644 index 000000000..06d3bb1dd --- /dev/null +++ b/src/renderer/src/store/feed/hooks.ts @@ -0,0 +1,23 @@ +import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" +import { ROUTE_FEED_PENDING, views } from "@renderer/lib/constants" +import type { FeedModel } from "@renderer/models" +import { useShallow } from "zustand/react/shallow" + +import { useFeedStore } from "./store" + +export const useFeedById = (feedId: Nullable): FeedModel | null => + useFeedStore((state) => (feedId ? state.feeds[feedId] : null)) + +export const useFeedByIdSelector = ( + feedId: Nullable, + selector: (feed: FeedModel) => T, +) => useFeedStore(useShallow((state) => (feedId && state.feeds[feedId] ? selector(state.feeds[feedId]) : null))) + +export const useFeedHeaderTitle = () => { + const { feedId: currentFeedId, category, view } = useRouteParms() + + const feedTitle = useFeedByIdSelector(currentFeedId, (feed) => feed.title) + return currentFeedId === ROUTE_FEED_PENDING ? + views[view].name : + category || feedTitle +} diff --git a/src/renderer/src/store/feed/index.ts b/src/renderer/src/store/feed/index.ts new file mode 100644 index 000000000..a92fe14dc --- /dev/null +++ b/src/renderer/src/store/feed/index.ts @@ -0,0 +1,3 @@ +export * from "./hooks" +export * from "./store" +export * from "./types" diff --git a/src/renderer/src/store/feed/store.ts b/src/renderer/src/store/feed/store.ts new file mode 100644 index 000000000..53302fe46 --- /dev/null +++ b/src/renderer/src/store/feed/store.ts @@ -0,0 +1,39 @@ +import { produce } from "immer" + +import { createZustandStore, getStoreActions } from "../utils/helper" +import type { FeedActions, FeedState } from "./types" + +export const useFeedStore = createZustandStore( + "feed", + { + version: 1, + }, +)((set) => ({ + feeds: {}, + clear() { + set({ feeds: {} }) + }, + upsertMany(feeds) { + set((state) => + produce(state, (state) => { + for (const feed of feeds) { + if (feed.id) { state.feeds[feed.id] = feed } + } + }), + ) + }, + + optimisticUpdate(feedId, changed) { + set((state) => + produce(state, (state) => { + const feed = state.feeds[feedId] + if (!feed) return + + Object.assign(feed, changed) + }), + ) + }, +})) +export const feedActions = getStoreActions(useFeedStore) + +export const getFeedById = (feedId: string) => useFeedStore.getState().feeds[feedId] diff --git a/src/renderer/src/store/feed/types.ts b/src/renderer/src/store/feed/types.ts new file mode 100644 index 000000000..927666226 --- /dev/null +++ b/src/renderer/src/store/feed/types.ts @@ -0,0 +1,13 @@ +import type { FeedModel } from "@renderer/models" + +type FeedId = string + +export interface FeedState { + feeds: Record +} + +export interface FeedActions { + upsertMany: (feeds: FeedModel[]) => void + optimisticUpdate: (feedId: FeedId, changed: Partial) => void + clear: () => void +} diff --git a/src/renderer/src/store/subscription.ts b/src/renderer/src/store/subscription.ts index 4e1a47fe6..ed42ec77d 100644 --- a/src/renderer/src/store/subscription.ts +++ b/src/renderer/src/store/subscription.ts @@ -2,20 +2,23 @@ import { apiClient } from "@renderer/lib/api-fetch" import { FeedViewType } from "@renderer/lib/enum" import type { SubscriptionModel } from "@renderer/models" import { produce } from "immer" +import { omit } from "lodash-es" -import { entryActions } from "./entry/entry" +import { entryActions } from "./entry/store" +import { feedActions } from "./feed" import { unreadActions } from "./unread" import { createZustandStore, getStoreActions } from "./utils/helper" type FeedId = string +export type SubscriptionPlainModel = Omit interface SubscriptionState { - data: Record + data: Record dataIdByView: Record } interface SubscriptionActions { - upsert: (feedId: FeedId, subscription: SubscriptionModel) => void - fetchByView: (view?: FeedViewType) => Promise + upsertMany: (subscription: SubscriptionPlainModel[]) => void + fetchByView: (view?: FeedViewType) => Promise markReadByView: (view?: FeedViewType) => void internal_reset: () => void clear: () => void @@ -66,23 +69,20 @@ export const useSubscriptionStore = createZustandStore< })) } - set((state) => - produce(state, (state) => { - res.data.forEach((subscription) => { - state.data[subscription.feeds.id] = subscription - state.dataIdByView[subscription.view].push(subscription.feeds.id) - return state - }) - }), - ) + get().upsertMany(res.data) + feedActions.upsertMany(res.data.map((s) => s.feeds)) return res.data }, - upsert: (feedId, subscription) => { + upsertMany: (subscriptions) => { set((state) => produce(state, (state) => { - state.data[feedId] = subscription - return state + subscriptions.forEach((subscription) => { + state.data[subscription.feedId] = omit(subscription, "feeds") + state.dataIdByView[subscription.view].push(subscription.feedId) + + return state + }) }), ) }, diff --git a/src/renderer/src/store/utils/local.ts b/src/renderer/src/store/utils/local.ts index 112815d5e..96163980a 100644 --- a/src/renderer/src/store/utils/local.ts +++ b/src/renderer/src/store/utils/local.ts @@ -1,4 +1,4 @@ -import { entryActions } from "../entry/entry" +import { entryActions } from "../entry/store" import { subscriptionActions } from "../subscription" import { uiActions } from "../ui" import { unreadActions } from "../unread" From 5b1b1d88fce9fdc021f627a25eeedeb6613d04dd Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 19:37:08 +0800 Subject: [PATCH 4/8] fix: cleanup Signed-off-by: Innei --- src/renderer/src/lib/api-fetch.ts | 3 +-- src/renderer/src/store/utils/local.ts | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/lib/api-fetch.ts b/src/renderer/src/lib/api-fetch.ts index c4f487f91..9e88c261a 100644 --- a/src/renderer/src/lib/api-fetch.ts +++ b/src/renderer/src/lib/api-fetch.ts @@ -7,6 +7,7 @@ import { ofetch } from "ofetch" export abstract class RequestError extends Error { name = "RequestError" } +const csrfToken = await getCsrfToken() export const apiFetch = ofetch.create({ baseURL: import.meta.env.VITE_API_URL, @@ -14,8 +15,6 @@ export const apiFetch = ofetch.create({ retry: false, onRequest: async ({ options }) => { if (options.method && options.method.toLowerCase() !== "get") { - const csrfToken = await getCsrfToken() - if (typeof options.body === "string") { options.body = JSON.parse(options.body) } diff --git a/src/renderer/src/store/utils/local.ts b/src/renderer/src/store/utils/local.ts index 96163980a..eb304c4ae 100644 --- a/src/renderer/src/store/utils/local.ts +++ b/src/renderer/src/store/utils/local.ts @@ -1,11 +1,12 @@ import { entryActions } from "../entry/store" +import { feedActions } from "../feed" import { subscriptionActions } from "../subscription" import { uiActions } from "../ui" import { unreadActions } from "../unread" export const clearLocalPersistStoreData = () => { // All clear and reset method will aggregate here - [entryActions, subscriptionActions, unreadActions, uiActions].forEach( + [entryActions, subscriptionActions, unreadActions, uiActions, feedActions].forEach( (actions) => { actions.clear() }, From dcdd65a871ee4706b24e7d9127d40af44fad2b84 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 19:51:58 +0800 Subject: [PATCH 5/8] chore: optmize re-render Signed-off-by: Innei --- src/renderer/src/components/user-button.tsx | 6 ++++-- src/renderer/src/modules/feed-column/category.tsx | 2 ++ src/renderer/src/modules/feed-column/index.tsx | 6 +++--- src/renderer/src/modules/feed-column/list.tsx | 3 ++- src/renderer/src/store/feed/store.ts | 3 ++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/user-button.tsx b/src/renderer/src/components/user-button.tsx index 1e6b5ebe1..dffbd1400 100644 --- a/src/renderer/src/components/user-button.tsx +++ b/src/renderer/src/components/user-button.tsx @@ -10,6 +10,7 @@ import { cn } from "@renderer/lib/utils" import { useSession } from "@renderer/queries/auth" import { m } from "framer-motion" import type { FC } from "react" +import { memo } from "react" import { Link } from "react-router-dom" import { FollowIcon } from "./icons/follow" @@ -102,7 +103,7 @@ export const LoginButton: FC = (props) => { ) } -export const ProfileButton: FC = (props) => { +export const ProfileButton: FC = memo((props) => { const { status } = useSession() if (status !== "authenticated") { @@ -115,7 +116,8 @@ export const ProfileButton: FC = (props) => { ) -} +}) +ProfileButton.displayName = "ProfileButton" export function UserButton({ className, diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx index 9341bdc4f..215c23a4c 100644 --- a/src/renderer/src/modules/feed-column/category.tsx +++ b/src/renderer/src/modules/feed-column/category.tsx @@ -5,6 +5,7 @@ import { import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" import { levels } from "@renderer/lib/constants" +import { stopPropagation } from "@renderer/lib/dom" import { showNativeMenu } from "@renderer/lib/native-menu" import { cn } from "@renderer/lib/utils" import type { FeedListModel } from "@renderer/models" @@ -126,6 +127,7 @@ export function FeedCategory({ >
{ + const navigateBackHome = useCallback(() => { navigate({ feedId: null, entryId: null, view: active, level: levels.view, }) - } + }, [active, navigate]) return ( { for (const subscription of subscriptions) { const feed = getFeedById(subscription.feedId) - if (!subscription.category && feed.siteUrl) { + if (!subscription.category && feed && feed.siteUrl) { const { domain } = parse(feed.siteUrl) if (domain) { if (!domains[domain]) { @@ -56,6 +56,7 @@ const useData = (view: FeedViewType) => { for (const subscription of subscriptions) { const feed = getFeedById(subscription.feedId) + if (!feed) continue if (!subscription.category) { if (feed.siteUrl) { // FIXME @DIYgod diff --git a/src/renderer/src/store/feed/store.ts b/src/renderer/src/store/feed/store.ts index 53302fe46..2a0f58e95 100644 --- a/src/renderer/src/store/feed/store.ts +++ b/src/renderer/src/store/feed/store.ts @@ -1,3 +1,4 @@ +import type { FeedModel } from "@renderer/models" import { produce } from "immer" import { createZustandStore, getStoreActions } from "../utils/helper" @@ -36,4 +37,4 @@ export const useFeedStore = createZustandStore( })) export const feedActions = getStoreActions(useFeedStore) -export const getFeedById = (feedId: string) => useFeedStore.getState().feeds[feedId] +export const getFeedById = (feedId: string): Nullable => useFeedStore.getState().feeds[feedId] From 132758d7c88fc3a8d30065d12171349953058399 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 20:39:34 +0800 Subject: [PATCH 6/8] feat: route params selector Signed-off-by: Innei --- src/renderer/src/atoms/index.ts | 1 + src/renderer/src/atoms/route.ts | 25 +++++++++++++ .../src/hooks/biz/useNavigateEntry.ts | 17 ++++----- src/renderer/src/hooks/biz/useRouteParams.ts | 35 +++++++++++++++++++ .../src/modules/feed-column/category.tsx | 14 +++----- src/renderer/src/modules/feed-column/item.tsx | 15 +++++--- src/renderer/src/providers/root-providers.tsx | 3 ++ .../src/providers/router-prams-provider.tsx | 16 +++++++++ 8 files changed, 101 insertions(+), 25 deletions(-) create mode 100644 src/renderer/src/atoms/route.ts create mode 100644 src/renderer/src/providers/router-prams-provider.tsx diff --git a/src/renderer/src/atoms/index.ts b/src/renderer/src/atoms/index.ts index d87053269..ca6d44f23 100644 --- a/src/renderer/src/atoms/index.ts +++ b/src/renderer/src/atoms/index.ts @@ -1 +1,2 @@ export * from "./dom" +export * from "./route" diff --git a/src/renderer/src/atoms/route.ts b/src/renderer/src/atoms/route.ts new file mode 100644 index 000000000..c5014f8dc --- /dev/null +++ b/src/renderer/src/atoms/route.ts @@ -0,0 +1,25 @@ +/* eslint-disable unicorn/no-unreadable-array-destructuring */ +import { createAtomHooks } from "@renderer/lib/jotai" +import { atom, useAtomValue } from "jotai" +import { selectAtom } from "jotai/utils" +import { useMemo } from "react" +import type { Params } from "react-router-dom" + +interface RouteAtom { + params: Readonly> + searchParams: URLSearchParams +} + +export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks( + atom({ + params: {}, + searchParams: new URLSearchParams(), + }), +) + +export const useReadonlyRouteSelector = ( + selector: (route: RouteAtom) => T, +): T => + useAtomValue( + useMemo(() => selectAtom(routeAtom, (route) => selector(route)), []), + ) diff --git a/src/renderer/src/hooks/biz/useNavigateEntry.ts b/src/renderer/src/hooks/biz/useNavigateEntry.ts index 363f4f47e..d1dedafe9 100644 --- a/src/renderer/src/hooks/biz/useNavigateEntry.ts +++ b/src/renderer/src/hooks/biz/useNavigateEntry.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ +import { getReadonlyRoute } from "@renderer/atoms" import { ROUTE_FEED_PENDING } from "@renderer/lib/constants" import type { FeedViewType } from "@renderer/lib/enum" import { isUndefined } from "lodash-es" import { useCallback } from "react" -import { useNavigate, useParams, useSearchParams } from "react-router-dom" - -import { useRefValue } from "../common" +import { useNavigate } from "react-router-dom" type NavigateEntryOptions = Partial<{ feedId: string | null @@ -19,22 +18,18 @@ type NavigateEntryOptions = Partial<{ * @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level` */ export const useNavigateEntry = () => { - const [searchParams] = useSearchParams() const navigate = useNavigate() - const params = useParams() - - const paramsRef = useRefValue(params) - const searchRef = useRefValue(searchParams) return useCallback( (options: NavigateEntryOptions) => { const { entryId, feedId, level, view, category } = options || {} - let finalFeedId = feedId || paramsRef.current.feedId || ROUTE_FEED_PENDING + const { params, searchParams } = getReadonlyRoute() + let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING if ("feedId" in options && feedId === null) { finalFeedId = ROUTE_FEED_PENDING } - const nextSearchParams = new URLSearchParams(searchRef.current) + const nextSearchParams = new URLSearchParams(searchParams) !isUndefined(view) && nextSearchParams.set("view", view.toString()) level && nextSearchParams.set("level", level.toString()) @@ -53,6 +48,6 @@ export const useNavigateEntry = () => { }?${nextSearchParams.toString()}`, ) }, - [navigate, paramsRef, searchRef], + [navigate], ) } diff --git a/src/renderer/src/hooks/biz/useRouteParams.ts b/src/renderer/src/hooks/biz/useRouteParams.ts index 4997f7ce0..fb953632b 100644 --- a/src/renderer/src/hooks/biz/useRouteParams.ts +++ b/src/renderer/src/hooks/biz/useRouteParams.ts @@ -1,3 +1,4 @@ +import { useReadonlyRouteSelector } from "@renderer/atoms" import { FeedViewType } from "@renderer/lib/enum" import { useParams, useSearchParams } from "react-router-dom" // '0', '1', '2', '3', '4', '5', @@ -46,3 +47,37 @@ export const useRouteParms = () => { category: search.get("category") || undefined, } } + +export const useRouteParamsSelector = ( + selector: (params: { + entryId: string | undefined + feedId: string | undefined + level: string | undefined + category: string | undefined + view: FeedViewType + }) => T, +): T => useReadonlyRouteSelector((route) => { + const { searchParams, params } = route + + let feedId: string | number = params.feedId! + + // If feedId is a number, it's a FeedViewType + if (feedId && FeedViewTypeValues.includes(feedId as string)) { + feedId = Number.parseInt(feedId as string) + } + + const view = searchParams.get("view") + + const finalView = + (view && FeedViewTypeValues.includes(view) ? + +view : + FeedViewType.Articles) || FeedViewType.Articles + + return selector({ + entryId: params.entryId || undefined, + feedId: params.feedId || undefined, + level: searchParams.get("level") || undefined, + category: searchParams.get("category") || undefined, + view: finalView, + }) + }) diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx index 215c23a4c..9247eddee 100644 --- a/src/renderer/src/modules/feed-column/category.tsx +++ b/src/renderer/src/modules/feed-column/category.tsx @@ -3,7 +3,7 @@ import { CollapsibleTrigger, } from "@renderer/components/ui/collapsible" import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" -import { useRouteParms } from "@renderer/hooks/biz/useRouteParams" +import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams" import { levels } from "@renderer/lib/constants" import { stopPropagation } from "@renderer/lib/dom" import { showNativeMenu } from "@renderer/lib/native-menu" @@ -11,7 +11,7 @@ import { cn } from "@renderer/lib/utils" import type { FeedListModel } from "@renderer/models" import { useUnreadStore } from "@renderer/store" import { AnimatePresence, m } from "framer-motion" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useState } from "react" import { useModalStack } from "../../components/ui/modal/stacked/hooks" import { CategoryRemoveDialogContent } from "./category-remove-dialog" @@ -27,8 +27,6 @@ export function FeedCategory({ view?: number expansion: boolean }) { - const routerParams = useRouteParms() - const [open, setOpen] = useState(!data.name) const feedIdList = data.list.map((feed) => feed.feedId) @@ -64,12 +62,8 @@ export function FeedCategory({ ), ) - const isActive = useMemo( - () => - routerParams?.level === levels.folder && - routerParams.feedId === data.list.map((feed) => feed.feedId).join(","), - [data.list, routerParams.feedId, routerParams?.level], - ) + const isActive = useRouteParamsSelector((routerParams) => routerParams?.level === levels.folder && + routerParams.feedId === data.list.map((feed) => feed.feedId).join(",")) const { present } = useModalStack() return ( Feed @@ -104,7 +108,11 @@ export function FeedItem({ ) const { present } = useModalStack() - const routerParmas = useRouteParms() + const isActive = useRouteParamsSelector( + (routerParams) => + routerParams?.level === levels.feed && + routerParams.feedId === subscription.feedId, + ) const feed = useFeedById(subscription.feedId) @@ -113,8 +121,7 @@ export function FeedItem({
import("../framer-lazy-feature").then((res) => res.default) export const RootProviders: FC = ({ children }) => ( @@ -28,6 +30,7 @@ export const RootProviders: FC = ({ children }) => ( {children} + diff --git a/src/renderer/src/providers/router-prams-provider.tsx b/src/renderer/src/providers/router-prams-provider.tsx new file mode 100644 index 000000000..f17c22c78 --- /dev/null +++ b/src/renderer/src/providers/router-prams-provider.tsx @@ -0,0 +1,16 @@ +import { setRoute } from "@renderer/atoms" +import { useLayoutEffect } from "react" +import { useParams, useSearchParams } from "react-router-dom" + +export const RouterParamsProvider = () => { + const [searchParams] = useSearchParams() + const params = useParams() + + useLayoutEffect(() => { + setRoute({ + params, + searchParams, + }) + }, [searchParams, params]) + return null +} From a37628e029851ee3378fb55f3864e0641bdc088f Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 20:53:38 +0800 Subject: [PATCH 7/8] fix: update snapshot Signed-off-by: Innei --- src/renderer/src/lib/route-builder.test.ts | 6 +++--- src/renderer/src/modules/entry-column/item.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/lib/route-builder.test.ts b/src/renderer/src/lib/route-builder.test.ts index 875c4129a..7af1c1885 100644 --- a/src/renderer/src/lib/route-builder.test.ts +++ b/src/renderer/src/lib/route-builder.test.ts @@ -118,7 +118,7 @@ describe("test route builder", () => { "children": [ { "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[:id]/index/", + "fs": "./pages/(external)/(with-layout)/feed/[id]/index/", "fullPath": "/feed/:id/", }, "lazy": [Function], @@ -126,7 +126,7 @@ describe("test route builder", () => { }, ], "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[:id]/layout", + "fs": "./pages/(external)/(with-layout)/feed/[id]/layout", "fullPath": "/feed/:id", }, "lazy": [Function], @@ -134,7 +134,7 @@ describe("test route builder", () => { }, ], "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[:id]/:id", + "fs": "./pages/(external)/(with-layout)/feed/[id]/:id", "fullPath": "/feed/:id", }, "path": ":id", diff --git a/src/renderer/src/modules/entry-column/item.tsx b/src/renderer/src/modules/entry-column/item.tsx index 8686d42b4..51edd67e1 100644 --- a/src/renderer/src/modules/entry-column/item.tsx +++ b/src/renderer/src/modules/entry-column/item.tsx @@ -1,6 +1,6 @@ import { useAsRead, useBizQuery, useEntryActions } from "@renderer/hooks" import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" -import { useRouteEntryId } from "@renderer/hooks/biz/useRouteParams" +import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams" import { views } from "@renderer/lib/constants" import { FeedViewType } from "@renderer/lib/enum" import { showNativeMenu } from "@renderer/lib/native-menu" @@ -41,7 +41,7 @@ function EntryItemImpl({ entry, view }: { entry: EntryModel, view?: number }) { }, ) - const currentEntryId = useRouteEntryId() + const isActive = useRouteParamsSelector(({ entryId }) => entryId === entry.entries.id) const asRead = useAsRead(entry) @@ -120,7 +120,7 @@ function EntryItemImpl({ entry, view }: { entry: EntryModel, view?: number }) { className={cn( "rounded-md bg-theme-background transition-colors", !views[view || 0].wideMode && - currentEntryId === entry.entries.id && + isActive && "bg-theme-item-active", asRead ? "text-zinc-500/90" : "text-zinc-900 dark:text-white/90", )} From 55a4cebee38f6495fe680f94e653502d6917c19d Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 21:26:17 +0800 Subject: [PATCH 8/8] fix: stable navigate fn Signed-off-by: Innei --- src/renderer/src/atoms/route.ts | 17 +++++- src/renderer/src/hooks/biz/useAsRead.ts | 10 ++-- .../src/hooks/biz/useNavigateEntry.ts | 53 ++++++++----------- src/renderer/src/hooks/biz/useRouteParams.ts | 38 ++++++------- .../src/modules/feed-column/category.tsx | 6 ++- src/renderer/src/modules/feed-column/item.tsx | 15 +++--- .../src/providers/biz-router-provider.tsx | 17 ++++++ src/renderer/src/providers/root-providers.tsx | 4 +- .../src/providers/router-prams-provider.tsx | 16 ------ 9 files changed, 93 insertions(+), 83 deletions(-) create mode 100644 src/renderer/src/providers/biz-router-provider.tsx delete mode 100644 src/renderer/src/providers/router-prams-provider.tsx diff --git a/src/renderer/src/atoms/route.ts b/src/renderer/src/atoms/route.ts index c5014f8dc..868b49268 100644 --- a/src/renderer/src/atoms/route.ts +++ b/src/renderer/src/atoms/route.ts @@ -3,7 +3,7 @@ import { createAtomHooks } from "@renderer/lib/jotai" import { atom, useAtomValue } from "jotai" import { selectAtom } from "jotai/utils" import { useMemo } from "react" -import type { Params } from "react-router-dom" +import type { NavigateFunction, Params } from "react-router-dom" interface RouteAtom { params: Readonly> @@ -17,9 +17,22 @@ export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks( }), ) +const noop = [] export const useReadonlyRouteSelector = ( selector: (route: RouteAtom) => T, + deps: any[] = noop, ): T => useAtomValue( - useMemo(() => selectAtom(routeAtom, (route) => selector(route)), []), + useMemo(() => selectAtom(routeAtom, (route) => selector(route)), deps), ) + +// VITE HMR will create new router instance, but RouterProvider always stable + +const [, , , , navigate, setNavigate] = createAtomHooks( + atom<{ fn: NavigateFunction | null }>({ fn() {} }), +) +const getStableRouterNavigate = () => navigate().fn +export { + getStableRouterNavigate, + setNavigate, +} diff --git a/src/renderer/src/hooks/biz/useAsRead.ts b/src/renderer/src/hooks/biz/useAsRead.ts index 713ec9f10..0e684331b 100644 --- a/src/renderer/src/hooks/biz/useAsRead.ts +++ b/src/renderer/src/hooks/biz/useAsRead.ts @@ -1,11 +1,11 @@ import { FEED_COLLECTION_LIST, levels } from "@renderer/lib/constants" import type { EntryModel } from "@renderer/models" -import { useRouteParms } from "./useRouteParams" +import { useRouteParamsSelector } from "./useRouteParams" export function useAsRead(entry?: EntryModel) { - const { feedId, level } = useRouteParms() - - if (!entry) return false - return entry.read && !(level === levels.folder && feedId === FEED_COLLECTION_LIST) + return useRouteParamsSelector(({ feedId, level }) => { + if (!entry) return false + return entry.read && !(level === levels.folder && feedId === FEED_COLLECTION_LIST) + }, [entry?.read]) } diff --git a/src/renderer/src/hooks/biz/useNavigateEntry.ts b/src/renderer/src/hooks/biz/useNavigateEntry.ts index d1dedafe9..d4f56aeb6 100644 --- a/src/renderer/src/hooks/biz/useNavigateEntry.ts +++ b/src/renderer/src/hooks/biz/useNavigateEntry.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ -import { getReadonlyRoute } from "@renderer/atoms" +import { getReadonlyRoute, getStableRouterNavigate } from "@renderer/atoms" import { ROUTE_FEED_PENDING } from "@renderer/lib/constants" import type { FeedViewType } from "@renderer/lib/enum" import { isUndefined } from "lodash-es" import { useCallback } from "react" -import { useNavigate } from "react-router-dom" type NavigateEntryOptions = Partial<{ feedId: string | null @@ -17,37 +16,31 @@ type NavigateEntryOptions = Partial<{ /** * @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level` */ -export const useNavigateEntry = () => { - const navigate = useNavigate() - return useCallback( - (options: NavigateEntryOptions) => { - const { entryId, feedId, level, view, category } = options || {} - const { params, searchParams } = getReadonlyRoute() - let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING +export const useNavigateEntry = () => useCallback((options: NavigateEntryOptions) => { + const { entryId, feedId, level, view, category } = options || {} + const { params, searchParams } = getReadonlyRoute() + let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING - if ("feedId" in options && feedId === null) { - finalFeedId = ROUTE_FEED_PENDING - } + if ("feedId" in options && feedId === null) { + finalFeedId = ROUTE_FEED_PENDING + } - const nextSearchParams = new URLSearchParams(searchParams) + const nextSearchParams = new URLSearchParams(searchParams) - !isUndefined(view) && nextSearchParams.set("view", view.toString()) - level && nextSearchParams.set("level", level.toString()) + !isUndefined(view) && nextSearchParams.set("view", view.toString()) + level && nextSearchParams.set("level", level.toString()) - if ("category" in options) { - if (!category) { - nextSearchParams.delete("category") - } else { - nextSearchParams.set("category", category.toString()) - } - } + if ("category" in options) { + if (!category) { + nextSearchParams.delete("category") + } else { + nextSearchParams.set("category", category.toString()) + } + } - return navigate( - `/feeds/${finalFeedId}/${ - entryId || ROUTE_FEED_PENDING - }?${nextSearchParams.toString()}`, - ) - }, - [navigate], + return getStableRouterNavigate()?.( + `/feeds/${finalFeedId}/${ + entryId || ROUTE_FEED_PENDING + }?${nextSearchParams.toString()}`, ) -} +}, []) diff --git a/src/renderer/src/hooks/biz/useRouteParams.ts b/src/renderer/src/hooks/biz/useRouteParams.ts index fb953632b..e5b3b0b7c 100644 --- a/src/renderer/src/hooks/biz/useRouteParams.ts +++ b/src/renderer/src/hooks/biz/useRouteParams.ts @@ -47,7 +47,7 @@ export const useRouteParms = () => { category: search.get("category") || undefined, } } - +const noop = [] as any[] export const useRouteParamsSelector = ( selector: (params: { entryId: string | undefined @@ -56,28 +56,30 @@ export const useRouteParamsSelector = ( category: string | undefined view: FeedViewType }) => T, -): T => useReadonlyRouteSelector((route) => { - const { searchParams, params } = route + deps = noop, +): T => + useReadonlyRouteSelector((route) => { + const { searchParams, params } = route - let feedId: string | number = params.feedId! + let feedId: string | number = params.feedId! - // If feedId is a number, it's a FeedViewType - if (feedId && FeedViewTypeValues.includes(feedId as string)) { - feedId = Number.parseInt(feedId as string) - } + // If feedId is a number, it's a FeedViewType + if (feedId && FeedViewTypeValues.includes(feedId as string)) { + feedId = Number.parseInt(feedId as string) + } - const view = searchParams.get("view") + const view = searchParams.get("view") - const finalView = + const finalView = (view && FeedViewTypeValues.includes(view) ? +view : FeedViewType.Articles) || FeedViewType.Articles - return selector({ - entryId: params.entryId || undefined, - feedId: params.feedId || undefined, - level: searchParams.get("level") || undefined, - category: searchParams.get("category") || undefined, - view: finalView, - }) - }) + return selector({ + entryId: params.entryId || undefined, + feedId: params.feedId || undefined, + level: searchParams.get("level") || undefined, + category: searchParams.get("category") || undefined, + view: finalView, + }) + }, deps) diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx index 9247eddee..e2ef0dabc 100644 --- a/src/renderer/src/modules/feed-column/category.tsx +++ b/src/renderer/src/modules/feed-column/category.tsx @@ -11,14 +11,14 @@ import { cn } from "@renderer/lib/utils" import type { FeedListModel } from "@renderer/models" import { useUnreadStore } from "@renderer/store" import { AnimatePresence, m } from "framer-motion" -import { useEffect, useState } from "react" +import { memo, useEffect, useState } from "react" import { useModalStack } from "../../components/ui/modal/stacked/hooks" import { CategoryRemoveDialogContent } from "./category-remove-dialog" import { CategoryRenameContent } from "./category-rename-dialog" import { FeedItem } from "./item" -export function FeedCategory({ +function FeedCategoryImpl({ data, view, expansion, @@ -166,3 +166,5 @@ export function FeedCategory({ ) } + +export const FeedCategory = memo(FeedCategoryImpl) diff --git a/src/renderer/src/modules/feed-column/item.tsx b/src/renderer/src/modules/feed-column/item.tsx index 6aac6b3b4..7038d55c1 100644 --- a/src/renderer/src/modules/feed-column/item.tsx +++ b/src/renderer/src/modules/feed-column/item.tsx @@ -9,9 +9,7 @@ import { TooltipTrigger, } from "@renderer/components/ui/tooltip" import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" -import { - useRouteParamsSelector, -} from "@renderer/hooks/biz/useRouteParams" +import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams" import { apiClient } from "@renderer/lib/api-fetch" import { levels } from "@renderer/lib/constants" import dayjs from "@renderer/lib/dayjs" @@ -21,13 +19,13 @@ import { Queries } from "@renderer/queries" import type { SubscriptionPlainModel } from "@renderer/store" import { getFeedById, useFeedById, useUnreadStore } from "@renderer/store" import { useMutation } from "@tanstack/react-query" -import { useCallback } from "react" +import { memo, useCallback } from "react" import { toast } from "sonner" import { FeedForm } from "../discover/feed-form" type FeedItemData = SubscriptionPlainModel -export function FeedItem({ +const FeedItemImpl = ({ subscription, view, className, @@ -35,7 +33,7 @@ export function FeedItem({ subscription: FeedItemData view?: number className?: string -}) { +}) => { const navigate = useNavigateEntry() const handleNavigate: React.MouseEventHandler = useCallback( (e) => { @@ -121,8 +119,7 @@ export function FeedItem({
) } + +export const FeedItem = memo(FeedItemImpl) diff --git a/src/renderer/src/providers/biz-router-provider.tsx b/src/renderer/src/providers/biz-router-provider.tsx new file mode 100644 index 000000000..68ef8b612 --- /dev/null +++ b/src/renderer/src/providers/biz-router-provider.tsx @@ -0,0 +1,17 @@ +import { setNavigate, setRoute } from "@renderer/atoms" +import { useLayoutEffect } from "react" +import { useNavigate, useParams, useSearchParams } from "react-router-dom" + +export const BizRouterProvider = () => { + const [searchParams] = useSearchParams() + const params = useParams() + const nav = useNavigate() + useLayoutEffect(() => { + setRoute({ + params, + searchParams, + }) + setNavigate({ fn: nav }) + }, [searchParams, params, nav]) + return null +} diff --git a/src/renderer/src/providers/root-providers.tsx b/src/renderer/src/providers/root-providers.tsx index e7cf517ca..4a5d362f8 100644 --- a/src/renderer/src/providers/root-providers.tsx +++ b/src/renderer/src/providers/root-providers.tsx @@ -9,7 +9,7 @@ import { Provider } from "jotai" import type { FC, PropsWithChildren } from "react" import { HelmetProvider } from "react-helmet-async" -import { RouterParamsProvider } from "./router-prams-provider" +import { BizRouterProvider } from "./biz-router-provider" const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default) @@ -30,7 +30,7 @@ export const RootProviders: FC = ({ children }) => ( {children} - + diff --git a/src/renderer/src/providers/router-prams-provider.tsx b/src/renderer/src/providers/router-prams-provider.tsx deleted file mode 100644 index f17c22c78..000000000 --- a/src/renderer/src/providers/router-prams-provider.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { setRoute } from "@renderer/atoms" -import { useLayoutEffect } from "react" -import { useParams, useSearchParams } from "react-router-dom" - -export const RouterParamsProvider = () => { - const [searchParams] = useSearchParams() - const params = useParams() - - useLayoutEffect(() => { - setRoute({ - params, - searchParams, - }) - }, [searchParams, params]) - return null -}