Merge branch 'refactor/feeds-route'
This commit is contained in:
commit
3c2848dfa2
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path stroke="#10161F" stroke-linecap="round" stroke-width="2" d="M4 20 20 4"/></svg>
|
||||
|
After Width: | Height: | Size: 216 B |
|
|
@ -1 +1,2 @@
|
|||
export * from "./dom"
|
||||
export * from "./route"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/* 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 { NavigateFunction, Params } from "react-router-dom"
|
||||
|
||||
interface RouteAtom {
|
||||
params: Readonly<Params<string>>
|
||||
searchParams: URLSearchParams
|
||||
}
|
||||
|
||||
export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks(
|
||||
atom<RouteAtom>({
|
||||
params: {},
|
||||
searchParams: new URLSearchParams(),
|
||||
}),
|
||||
)
|
||||
|
||||
const noop = []
|
||||
export const useReadonlyRouteSelector = <T>(
|
||||
selector: (route: RouteAtom) => T,
|
||||
deps: any[] = noop,
|
||||
): T =>
|
||||
useAtomValue(
|
||||
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,
|
||||
}
|
||||
|
|
@ -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<LoginProps> = (props) => {
|
|||
</Link>
|
||||
)
|
||||
}
|
||||
export const ProfileButton: FC<LoginProps> = (props) => {
|
||||
export const ProfileButton: FC<LoginProps> = memo((props) => {
|
||||
const { status } = useSession()
|
||||
|
||||
if (status !== "authenticated") {
|
||||
|
|
@ -115,7 +116,8 @@ export const ProfileButton: FC<LoginProps> = (props) => {
|
|||
</ActionButton>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
})
|
||||
ProfileButton.displayName = "ProfileButton"
|
||||
|
||||
export function UserButton({
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -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 { useRouteParamsSelector } from "./useRouteParams"
|
||||
|
||||
export function useAsRead(entry?: EntryModel) {
|
||||
const activeList = useFeedStore((state) => state.activeList)
|
||||
|
||||
if (!entry) return false
|
||||
return entry.read && !(activeList?.level === levels.folder && activeList?.id === FEED_COLLECTION_LIST)
|
||||
return useRouteParamsSelector(({ feedId, level }) => {
|
||||
if (!entry) return false
|
||||
return entry.read && !(level === levels.folder && feedId === FEED_COLLECTION_LIST)
|
||||
}, [entry?.read])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { client } from "@renderer/lib/client"
|
||||
import type { EntryModel } from "@renderer/models"
|
||||
import { entryActions } from "@renderer/store/entry/entry"
|
||||
import { entryActions } from "@renderer/store"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import type { FetchError } from "ofetch"
|
||||
import { ofetch } from "ofetch"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
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"
|
||||
|
||||
type NavigateEntryOptions = Partial<{
|
||||
feedId: string | null
|
||||
entryId: string | null
|
||||
view: FeedViewType
|
||||
level: string | null
|
||||
|
||||
category: string | null
|
||||
}>
|
||||
/**
|
||||
* @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level`
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
const nextSearchParams = new URLSearchParams(searchParams)
|
||||
|
||||
!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 getStableRouterNavigate()?.(
|
||||
`/feeds/${finalFeedId}/${
|
||||
entryId || ROUTE_FEED_PENDING
|
||||
}?${nextSearchParams.toString()}`,
|
||||
)
|
||||
}, [])
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { useReadonlyRouteSelector } from "@renderer/atoms"
|
||||
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,
|
||||
category: search.get("category") || undefined,
|
||||
}
|
||||
}
|
||||
const noop = [] as any[]
|
||||
export const useRouteParamsSelector = <T>(
|
||||
selector: (params: {
|
||||
entryId: string | undefined
|
||||
feedId: string | undefined
|
||||
level: string | undefined
|
||||
category: string | undefined
|
||||
view: FeedViewType
|
||||
}) => T,
|
||||
deps = noop,
|
||||
): 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,
|
||||
})
|
||||
}, deps)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,9 +74,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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -189,8 +189,8 @@ const normalizePathKey = (key: string) => {
|
|||
return ""
|
||||
}
|
||||
|
||||
if (key.startsWith("[:") && key.endsWith("]")) {
|
||||
return `:${key.slice(2, -1)}`
|
||||
if (key.startsWith("[") && key.endsWith("]")) {
|
||||
return `:${key.slice(1, -1)}`
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,10 @@ export type DiscoverResponse = Array<
|
|||
>[number]
|
||||
>
|
||||
|
||||
export type ActionsResponse = Exclude<ExtractBizResponse<
|
||||
typeof apiClient.actions.$get
|
||||
>["data"], undefined>["rules"]
|
||||
export type ActionsResponse = Exclude<
|
||||
ExtractBizResponse<typeof apiClient.actions.$get>["data"],
|
||||
undefined
|
||||
>["rules"]
|
||||
|
||||
export type ListResponse<T> = {
|
||||
code: number
|
||||
|
|
@ -67,7 +68,7 @@ export type DataResponse<T> = {
|
|||
data?: T
|
||||
}
|
||||
|
||||
export type ActiveEntryId = string | null
|
||||
export type ActiveEntryId = Nullable<string>
|
||||
|
||||
export type SubscriptionModel = SubscriptionResponse[number]
|
||||
|
||||
|
|
@ -78,4 +79,6 @@ export type FeedListModel = {
|
|||
}[]
|
||||
}
|
||||
|
||||
export type SupportedLanguages = Parameters<typeof apiClient.ai.translation.$get>[0]["query"]["language"]
|
||||
export type SupportedLanguages = Parameters<
|
||||
typeof apiClient.ai.translation.$get
|
||||
>[0]["query"]["language"]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import {
|
||||
ActionButton,
|
||||
StyledButton,
|
||||
} from "@renderer/components/ui/button"
|
||||
import { ActionButton, StyledButton } from "@renderer/components/ui/button"
|
||||
import {
|
||||
Popover,
|
||||
PopoverClose,
|
||||
|
|
@ -9,6 +6,11 @@ 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"
|
||||
import { views } from "@renderer/lib/constants"
|
||||
import { buildStorageNS } from "@renderer/lib/ns"
|
||||
|
|
@ -16,12 +18,10 @@ import { shortcuts } from "@renderer/lib/shortcuts"
|
|||
import { getEntriesParams } from "@renderer/lib/utils"
|
||||
import { useEntries } from "@renderer/queries/entries"
|
||||
import {
|
||||
feedActions,
|
||||
getCurrentEntryId,
|
||||
entryActions,
|
||||
subscriptionActions,
|
||||
useFeedStore,
|
||||
useFeedHeaderTitle,
|
||||
} from "@renderer/store"
|
||||
import { entryActions } from "@renderer/store/entry/entry"
|
||||
import {
|
||||
useEntry,
|
||||
useEntryIdsByFeedIdOrView,
|
||||
|
|
@ -44,7 +44,6 @@ import { useHotkeys } from "react-hotkeys-hook"
|
|||
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 +59,9 @@ const unreadOnlyAtom = atomWithStorage<boolean>(
|
|||
)
|
||||
|
||||
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,23 +126,25 @@ export function EntryColumn() {
|
|||
(_, entryId: string) => {
|
||||
if (!entryId) return null
|
||||
|
||||
return (
|
||||
<EntryItem key={entryId} entryId={entryId} view={activeList?.view} />
|
||||
)
|
||||
return <EntryItem key={entryId} entryId={entryId} view={view} />
|
||||
},
|
||||
[activeList?.view],
|
||||
[view],
|
||||
),
|
||||
}
|
||||
|
||||
const navigate = useNavigateEntry()
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-full flex-1 flex-col"
|
||||
onClick={() => feedActions.setActiveEntry(null)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
entryId: null,
|
||||
})}
|
||||
data-total-count={virtuosoOptions.totalCount}
|
||||
>
|
||||
<ListHeader totalCount={virtuosoOptions.totalCount} />
|
||||
<m.div
|
||||
key={`${activeList?.id}-${activeList?.view}`}
|
||||
key={`${feedId}-${view}`}
|
||||
className="h-full"
|
||||
initial={{ opacity: 0.01, y: 100 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -155,7 +152,7 @@ export function EntryColumn() {
|
|||
>
|
||||
{virtuosoOptions.totalCount === 0 ? (
|
||||
<EmptyList />
|
||||
) : activeList?.view && views[activeList.view].gridMode ?
|
||||
) : view && views[view].gridMode ?
|
||||
(
|
||||
<VirtuosoGrid
|
||||
listClassName="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 px-4"
|
||||
|
|
@ -171,16 +168,16 @@ export function EntryColumn() {
|
|||
}
|
||||
|
||||
const useEntriesByView = () => {
|
||||
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,
|
||||
})
|
||||
|
||||
|
|
@ -194,7 +191,7 @@ const useEntriesByView = () => {
|
|||
|
||||
useEffect(() => {
|
||||
prevEntries.current = []
|
||||
}, [activeList.id])
|
||||
}, [activeList.feedId])
|
||||
const localEntries = useMemo(() => {
|
||||
if (!unreadOnly) {
|
||||
prevEntries.current = []
|
||||
|
|
@ -242,31 +239,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])
|
||||
|
||||
useHotkeys(shortcuts.entries.markAllAsRead.key, () => {
|
||||
setMarkPopoverOpen(true)
|
||||
|
|
@ -276,6 +273,7 @@ const ListHeader: FC<{
|
|||
setUnreadOnly((prev) => !prev)
|
||||
}, { scopes: ["home"] })
|
||||
|
||||
const headerTitle = useFeedHeaderTitle()
|
||||
return (
|
||||
<div className="mb-5 flex w-full flex-col pl-11 pr-4 pt-2.5">
|
||||
<div className="flex w-full justify-end">
|
||||
|
|
@ -312,7 +310,7 @@ const ListHeader: FC<{
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-bold leading-none">{activeList?.name}</div>
|
||||
<div className="text-lg font-bold leading-none">{headerTitle}</div>
|
||||
<div className="text-xs font-medium text-zinc-400">
|
||||
{totalCount || 0}
|
||||
{" "}
|
||||
|
|
@ -362,10 +360,14 @@ const EntryList: FC<VirtuosoProps<string, unknown>> = ({
|
|||
|
||||
const dataRef = useRefValue(virtuosoOptions.data!)
|
||||
|
||||
const currentEntryIdRef = useRefValue(useRouteEntryId())
|
||||
|
||||
const navigate = useNavigateEntry()
|
||||
|
||||
useHotkeys(shortcuts.entries.next.key, () => {
|
||||
const data = dataRef.current
|
||||
const currentActiveEntryIndex = data.indexOf(
|
||||
getCurrentEntryId() || "",
|
||||
currentEntryIdRef.current || "",
|
||||
)
|
||||
|
||||
const nextIndex = Math.min(currentActiveEntryIndex + 1, data.length - 1)
|
||||
|
|
@ -374,13 +376,16 @@ const EntryList: FC<VirtuosoProps<string, unknown>> = ({
|
|||
index: nextIndex,
|
||||
})
|
||||
const nextId = data![nextIndex]
|
||||
feedActions.setActiveEntry(nextId)
|
||||
|
||||
navigate({
|
||||
entryId: nextId,
|
||||
})
|
||||
}, { scopes: ["home"] })
|
||||
|
||||
useHotkeys(shortcuts.entries.previous.key, () => {
|
||||
const data = dataRef.current
|
||||
const currentActiveEntryIndex = data.indexOf(
|
||||
getCurrentEntryId() || "",
|
||||
currentEntryIdRef.current || "",
|
||||
)
|
||||
|
||||
const nextIndex = currentActiveEntryIndex === -1 ? data.length - 1 : Math.max(0, currentActiveEntryIndex - 1)
|
||||
|
|
@ -389,7 +394,10 @@ const EntryList: FC<VirtuosoProps<string, unknown>> = ({
|
|||
index: nextIndex,
|
||||
})
|
||||
const nextId = data![nextIndex]
|
||||
feedActions.setActiveEntry(nextId)
|
||||
|
||||
navigate({
|
||||
entryId: nextId,
|
||||
})
|
||||
}, { scopes: ["home"] })
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useAsRead, useBizQuery, useEntryActions } from "@renderer/hooks"
|
||||
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
|
||||
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"
|
||||
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 isActive = useRouteParamsSelector(({ entryId }) => entryId === entry.entries.id)
|
||||
|
||||
const asRead = useAsRead(entry)
|
||||
|
||||
|
|
@ -75,14 +76,17 @@ function EntryItemImpl({ entry, view }: { entry: EntryModel, view?: number }) {
|
|||
Item = ArticleItem
|
||||
}
|
||||
}
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> =
|
||||
useCallback(() => {}, [])
|
||||
|
||||
const navigate = useNavigateEntry()
|
||||
const handleClick: React.MouseEventHandler<HTMLDivElement> = 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<HTMLDivElement> =
|
||||
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 &&
|
||||
isActive &&
|
||||
"bg-theme-item-active",
|
||||
asRead ? "text-zinc-500/90" : "text-zinc-900 dark:text-white/90",
|
||||
)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
@ -17,10 +17,9 @@ import { EntryTranslation } from "../entry-column/translation"
|
|||
import { setEntryTitleMeta } from "./atoms"
|
||||
import { EntryHeader } from "./header"
|
||||
|
||||
export const EntryContent = ({ entry }: { entry: ActiveEntryId }) => {
|
||||
const activeList = useFeedStore((state) => state.activeList)
|
||||
|
||||
if (!entry) {
|
||||
export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => {
|
||||
const title = useFeedHeaderTitle()
|
||||
if (!entryId) {
|
||||
return (
|
||||
<m.div
|
||||
className="-mt-2 flex size-full min-w-0 flex-col items-center justify-center gap-1 text-lg font-medium text-zinc-400"
|
||||
|
|
@ -28,12 +27,12 @@ export const EntryContent = ({ entry }: { entry: ActiveEntryId }) => {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<Logo className="size-16 opacity-40 grayscale" />
|
||||
{activeList?.name}
|
||||
{title}
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
return <EntryContentRender entryId={entry} />
|
||||
return <EntryContentRender entryId={entryId} />
|
||||
}
|
||||
|
||||
function EntryContentRender({ entryId }: { entryId: string }) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,23 @@ import {
|
|||
Collapsible,
|
||||
CollapsibleTrigger,
|
||||
} from "@renderer/components/ui/collapsible"
|
||||
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
|
||||
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"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import type { FeedListModel } from "@renderer/models"
|
||||
import {
|
||||
feedActions,
|
||||
useFeedActiveList,
|
||||
useUnreadStore,
|
||||
} from "@renderer/store"
|
||||
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 { CategoryRenameContent } from "./category-rename-dialog"
|
||||
import { FeedItem } from "./item"
|
||||
|
||||
const { setActiveList } = feedActions
|
||||
|
||||
export function FeedCategory({
|
||||
function FeedCategoryImpl({
|
||||
data,
|
||||
view,
|
||||
expansion,
|
||||
|
|
@ -32,8 +27,6 @@ export function FeedCategory({
|
|||
view?: number
|
||||
expansion: boolean
|
||||
}) {
|
||||
const activeList = useFeedActiveList()
|
||||
|
||||
const [open, setOpen] = useState(!data.name)
|
||||
|
||||
const feedIdList = data.list.map((feed) => feed.feedId)
|
||||
|
|
@ -44,13 +37,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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -64,6 +61,9 @@ export function FeedCategory({
|
|||
(a, b) => (state.data[b.feedId] || 0) - (state.data[a.feedId] || 0),
|
||||
),
|
||||
)
|
||||
|
||||
const isActive = useRouteParamsSelector((routerParams) => routerParams?.level === levels.folder &&
|
||||
routerParams.feedId === data.list.map((feed) => feed.feedId).join(","))
|
||||
const { present } = useModalStack()
|
||||
return (
|
||||
<Collapsible
|
||||
|
|
@ -75,9 +75,7 @@ export function FeedCategory({
|
|||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-md px-2.5 transition-colors",
|
||||
activeList?.level === levels.folder &&
|
||||
activeList.name === data.name &&
|
||||
"bg-native-active",
|
||||
isActive && "bg-native-active",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
|
@ -123,15 +121,14 @@ export function FeedCategory({
|
|||
>
|
||||
<div className="flex w-full min-w-0 items-center">
|
||||
<CollapsibleTrigger
|
||||
onClick={stopPropagation}
|
||||
className={cn(
|
||||
"flex h-8 items-center [&_.i-mgc-right-cute-fi]:data-[state=open]:rotate-90",
|
||||
!setActiveList && "flex-1",
|
||||
)}
|
||||
>
|
||||
<i className="i-mgc-right-cute-fi mr-2 transition-transform" />
|
||||
{!setActiveList && <span className="truncate">{data.name}</span>}
|
||||
</CollapsibleTrigger>
|
||||
{!!setActiveList && <span className="truncate">{data.name}</span>}
|
||||
<span className="truncate">{data.name}</span>
|
||||
</div>
|
||||
{!!unread && (
|
||||
<div className="ml-2 text-xs text-zinc-500">{unread}</div>
|
||||
|
|
@ -158,7 +155,7 @@ export function FeedCategory({
|
|||
{sortByUnreadFeedList.map((feed) => (
|
||||
<FeedItem
|
||||
key={feed.feedId}
|
||||
feed={feed}
|
||||
subscription={feed}
|
||||
view={view}
|
||||
className={data.name ? "pl-6" : "pl-2.5"}
|
||||
/>
|
||||
|
|
@ -169,3 +166,5 @@ export function FeedCategory({
|
|||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeedCategory = memo(FeedCategoryImpl)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
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"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { Vibrancy } from "../../components/ui/background"
|
||||
|
|
@ -17,7 +17,6 @@ import { FeedList } from "./list"
|
|||
const lethargy = new Lethargy()
|
||||
|
||||
export function FeedColumn() {
|
||||
const { setActiveList } = feedActions
|
||||
const carouselRef = useRef<HTMLDivElement>(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 = useCallback(() => {
|
||||
navigate({
|
||||
feedId: null,
|
||||
entryId: null,
|
||||
view: active,
|
||||
level: levels.view,
|
||||
})
|
||||
}, [active, navigate])
|
||||
return (
|
||||
<Vibrancy
|
||||
className="flex h-full flex-col gap-3 pt-2.5"
|
||||
onClick={() =>
|
||||
setActiveList({
|
||||
level: levels.view,
|
||||
id: active,
|
||||
name: views[active].name,
|
||||
view: active,
|
||||
})}
|
||||
onClick={navigateBackHome}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -79,12 +82,8 @@ export function FeedColumn() {
|
|||
className="flex items-center gap-1 text-xl font-bold"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setActiveList({
|
||||
level: levels.view,
|
||||
id: active,
|
||||
name: views[active].name,
|
||||
view: active,
|
||||
})
|
||||
|
||||
navigateBackHome()
|
||||
}}
|
||||
>
|
||||
<Logo className="size-6" />
|
||||
|
|
@ -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()
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -8,54 +8,56 @@ import {
|
|||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@renderer/components/ui/tooltip"
|
||||
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
|
||||
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"
|
||||
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 {
|
||||
feedActions,
|
||||
useFeedActiveList,
|
||||
useUnreadStore,
|
||||
} from "@renderer/store"
|
||||
import type { SubscriptionPlainModel } from "@renderer/store"
|
||||
import { getFeedById, useFeedById, useUnreadStore } from "@renderer/store"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { memo, useCallback } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { FeedForm } from "../discover/feed-form"
|
||||
|
||||
export function FeedItem({
|
||||
feed,
|
||||
type FeedItemData = SubscriptionPlainModel
|
||||
const FeedItemImpl = ({
|
||||
subscription,
|
||||
view,
|
||||
className,
|
||||
}: {
|
||||
feed: SubscriptionResponse[number]
|
||||
subscription: FeedItemData
|
||||
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<HTMLDivElement> = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
if (view === undefined) return
|
||||
navigate({
|
||||
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(() => {
|
||||
requestAnimationFrame(() => {
|
||||
getMainContainerElement()?.focus()
|
||||
})
|
||||
})
|
||||
},
|
||||
[subscription.feedId, navigate, view],
|
||||
)
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (feed: SubscriptionResponse[number]) =>
|
||||
mutationFn: async (feed: SubscriptionPlainModel) =>
|
||||
apiClient.subscriptions.$delete({
|
||||
json: {
|
||||
feedId: feed.feedId,
|
||||
|
|
@ -65,13 +67,17 @@ export function FeedItem({
|
|||
onSuccess: (_, variables) => {
|
||||
Queries.subscription.byView(variables.view).invalidate()
|
||||
|
||||
const feed = getFeedById(variables.feedId)
|
||||
|
||||
if (!feed) return
|
||||
toast(
|
||||
<>
|
||||
Feed
|
||||
{" "}
|
||||
<i className="mr-px font-semibold">{variables.feeds.title}</i>
|
||||
<i className="mr-px font-semibold">{feed.title}</i>
|
||||
{" "}
|
||||
has been unfollowed.
|
||||
has been
|
||||
unfollowed.
|
||||
</>,
|
||||
{
|
||||
duration: 3000,
|
||||
|
|
@ -80,14 +86,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()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -95,24 +101,33 @@ 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 isActive = useRouteParamsSelector(
|
||||
(routerParams) =>
|
||||
routerParams?.level === levels.feed &&
|
||||
routerParams.feedId === subscription.feedId,
|
||||
)
|
||||
|
||||
const feed = useFeedById(subscription.feedId)
|
||||
|
||||
if (!feed) return null
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-md py-[2px] pr-2.5 text-sm font-medium leading-loose",
|
||||
activeList?.level === levels.feed &&
|
||||
activeList.id === feed.feedId &&
|
||||
"bg-native-active",
|
||||
isActive && "bg-native-active",
|
||||
className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setFeedActive(feed)
|
||||
}}
|
||||
onClick={handleNavigate}
|
||||
onDoubleClick={() => {
|
||||
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",
|
||||
)
|
||||
}}
|
||||
|
|
@ -126,14 +141,20 @@ export function FeedItem({
|
|||
click: () => {
|
||||
present({
|
||||
title: "Edit Feed",
|
||||
content: ({ dismiss }) => <FeedForm asWidget id={feed.feedId} onSuccess={dismiss} />,
|
||||
content: ({ dismiss }) => (
|
||||
<FeedForm
|
||||
asWidget
|
||||
id={subscription.feedId}
|
||||
onSuccess={dismiss}
|
||||
/>
|
||||
),
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
label: "Unfollow",
|
||||
click: () => deleteMutation.mutate(feed),
|
||||
click: () => deleteMutation.mutate(subscription),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
|
|
@ -144,7 +165,7 @@ export function FeedItem({
|
|||
click: () =>
|
||||
window.open(
|
||||
`${import.meta.env.VITE_WEB_URL}/feed/${
|
||||
feed.feedId
|
||||
subscription.feedId
|
||||
}?view=${view}`,
|
||||
"_blank",
|
||||
),
|
||||
|
|
@ -152,8 +173,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,
|
||||
|
|
@ -163,12 +188,12 @@ export function FeedItem({
|
|||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 items-center",
|
||||
feed.feeds.errorAt && "text-red-900",
|
||||
feed.errorAt && "text-red-900",
|
||||
)}
|
||||
>
|
||||
<FeedIcon feed={feed.feeds} className="size-4" />
|
||||
<div className="truncate">{feed.feeds.title}</div>
|
||||
{feed.feeds.errorAt && (
|
||||
<FeedIcon feed={feed} className="size-4" />
|
||||
<div className="truncate">{feed.title}</div>
|
||||
{feed.errorAt && (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -180,7 +205,7 @@ export function FeedItem({
|
|||
{" "}
|
||||
{dayjs
|
||||
.duration(
|
||||
dayjs(feed.feeds.errorAt).diff(dayjs(), "minute"),
|
||||
dayjs(feed.errorAt).diff(dayjs(), "minute"),
|
||||
"minute",
|
||||
)
|
||||
.humanize(true)}
|
||||
|
|
@ -189,7 +214,7 @@ export function FeedItem({
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{feed.isPrivate && (
|
||||
{subscription.isPrivate && (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
|
@ -210,3 +235,5 @@ export function FeedItem({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeedItem = memo(FeedItemImpl)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
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"
|
||||
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,
|
||||
useFeedActiveList,
|
||||
getFeedById,
|
||||
useSubscriptionByView,
|
||||
useUnreadStore,
|
||||
} from "@renderer/store"
|
||||
|
|
@ -30,7 +34,7 @@ const useData = (view: FeedViewType) => {
|
|||
list: Record<
|
||||
string,
|
||||
{
|
||||
list: SubscriptionResponse
|
||||
list: SubscriptionPlainModel[]
|
||||
}
|
||||
>
|
||||
}
|
||||
|
|
@ -38,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 && feed.siteUrl) {
|
||||
const { domain } = parse(feed.siteUrl)
|
||||
if (domain) {
|
||||
if (!domains[domain]) {
|
||||
domains[domain] = 0
|
||||
|
|
@ -50,11 +55,13 @@ const useData = (view: FeedViewType) => {
|
|||
}
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
const feed = getFeedById(subscription.feedId)
|
||||
if (!feed) continue
|
||||
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)
|
||||
|
|
@ -94,7 +101,6 @@ export function FeedList({
|
|||
}) {
|
||||
const [expansion, setExpansion] = useState(false)
|
||||
const data = useData(view)
|
||||
const activeList = useFeedActiveList()
|
||||
|
||||
useBizQuery(Queries.subscription.unreadAll())
|
||||
|
||||
|
|
@ -108,8 +114,6 @@ export function FeedList({
|
|||
return unread
|
||||
})
|
||||
|
||||
const { setActiveList } = feedActions
|
||||
|
||||
const sortedByUnread = useUnreadStore((state) =>
|
||||
data?.list?.sort(
|
||||
(a, b) =>
|
||||
|
|
@ -118,6 +122,9 @@ export function FeedList({
|
|||
),
|
||||
)
|
||||
|
||||
const feedId = useRouteFeedId()
|
||||
const navigate = useNavigateEntry()
|
||||
|
||||
return (
|
||||
<div className={cn(className, "font-medium")}>
|
||||
{!hideTitle && (
|
||||
|
|
@ -130,10 +137,10 @@ export function FeedList({
|
|||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (view !== undefined) {
|
||||
setActiveList({
|
||||
navigate({
|
||||
entryId: null,
|
||||
feedId: null,
|
||||
level: levels.view,
|
||||
id: view,
|
||||
name: views[view].name,
|
||||
view,
|
||||
})
|
||||
}
|
||||
|
|
@ -160,15 +167,15 @@ export function FeedList({
|
|||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-full items-center rounded-md px-2.5 transition-colors",
|
||||
activeList?.id === FEED_COLLECTION_LIST && "bg-native-active",
|
||||
feedId === FEED_COLLECTION_LIST && "bg-native-active",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex h-full">
|
||||
<div className="w-64 shrink-0 border-r">
|
||||
<FeedColumn />
|
||||
</div>
|
||||
{/* NOTE: tabIndex for main element can get by `document.activeElement` */}
|
||||
<main ref={setMainContainerElement} className="flex min-w-0 flex-1 bg-theme-background !outline-none" tabIndex={-1}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<AnimatePresence>
|
||||
{!inWideMode && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<EntryContent entryId={entryId === ROUTE_FEED_PENDING ? "" : entryId} />
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,36 +1,21 @@
|
|||
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 {
|
||||
feedActions,
|
||||
uiActions,
|
||||
useFeedStore,
|
||||
useUIStore,
|
||||
} from "@renderer/store"
|
||||
import { AnimatePresence } from "framer-motion"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { uiActions, useUIStore } from "@renderer/store"
|
||||
import { useMemo, useRef } from "react"
|
||||
import { HotkeysProvider } from "react-hotkeys-hook"
|
||||
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 { setActiveEntry } = feedActions
|
||||
useEffect(() => {
|
||||
setActiveEntry(null)
|
||||
}, [activeList?.id])
|
||||
const containerRef = useRef<HTMLDivElement>(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,
|
||||
|
|
@ -42,7 +27,6 @@ export function Component() {
|
|||
},
|
||||
})
|
||||
|
||||
const inWideMode = activeList && views[activeList.view].wideMode
|
||||
return (
|
||||
<HotkeysProvider initiallyActiveScopes={["home"]}>
|
||||
<div ref={containerRef} className="flex min-w-0 grow">
|
||||
|
|
@ -64,13 +48,7 @@ export function Component() {
|
|||
className="h-full w-px shrink-0 cursor-ew-resize hover:bg-border"
|
||||
/>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{!inWideMode && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<EntryContent entry={activeEntry} />
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Outlet />
|
||||
</div>
|
||||
</HotkeysProvider>
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { Outlet as Component } from "react-router-dom"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { levels, ROUTE_FEED_PENDING } from "@renderer/lib/constants"
|
||||
import { FeedViewType } from "@renderer/lib/enum"
|
||||
import { redirect } from "react-router-dom"
|
||||
|
||||
export function Component() {
|
||||
return null
|
||||
}
|
||||
|
||||
export const loader = () =>
|
||||
// navigate to the first feed
|
||||
redirect(`/feeds/${ROUTE_FEED_PENDING}?view=${FeedViewType.Articles}&level=${levels.view}`)
|
||||
|
|
@ -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 (
|
||||
<div className="flex h-full">
|
||||
<div className="w-64 shrink-0 border-r">
|
||||
<FeedColumn />
|
||||
</div>
|
||||
{/* NOTE: tabIndex for main element can get by `document.activeElement` */}
|
||||
<main ref={setMainContainerElement} className="flex min-w-0 flex-1 bg-theme-background !outline-none" tabIndex={-1}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
feedActions.setActiveEntry(null)
|
||||
if (changed) {
|
||||
navigate("/")
|
||||
}
|
||||
}, [changed])
|
||||
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
export { MainLayout as Component }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import { Provider } from "jotai"
|
|||
import type { FC, PropsWithChildren } from "react"
|
||||
import { HelmetProvider } from "react-helmet-async"
|
||||
|
||||
import { BizRouterProvider } from "./biz-router-provider"
|
||||
|
||||
const loadFeatures = () =>
|
||||
import("../framer-lazy-feature").then((res) => res.default)
|
||||
export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
|
||||
|
|
@ -28,6 +30,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
|
|||
<Provider store={jotaiStore}>
|
||||
<ModalStackProvider />
|
||||
<HelmetProvider>{children}</HelmetProvider>
|
||||
<BizRouterProvider />
|
||||
</Provider>
|
||||
</TooltipProvider>
|
||||
</PersistQueryClientProvider>
|
||||
|
|
|
|||
|
|
@ -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: ({
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ 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
|
||||
}
|
||||
|
||||
export const useEntry = (entryId: string | null): EntryModel | null =>
|
||||
export const useEntry = (entryId: Nullable<string >): 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) =>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export * from "./entry"
|
||||
export * from "./hooks"
|
||||
export * from "./store"
|
||||
export * from "./types"
|
||||
|
|
|
|||
|
|
@ -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<EntryState & EntryActions>(
|
|||
draft.flatMapEntries[entry.entries.id] || {},
|
||||
entry,
|
||||
)
|
||||
|
||||
const feeds = entries.map((entry) => entry.feeds)
|
||||
// Insert to feed store
|
||||
feedActions.upsertMany(feeds)
|
||||
}
|
||||
return draft
|
||||
}),
|
||||
|
|
@ -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<FeedStoreActions>
|
||||
export const useFeedStore = create<FeedStore>((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)
|
||||
|
|
@ -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<string>): FeedModel | null =>
|
||||
useFeedStore((state) => (feedId ? state.feeds[feedId] : null))
|
||||
|
||||
export const useFeedByIdSelector = <T>(
|
||||
feedId: Nullable<string>,
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./hooks"
|
||||
export * from "./store"
|
||||
export * from "./types"
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { FeedModel } from "@renderer/models"
|
||||
import { produce } from "immer"
|
||||
|
||||
import { createZustandStore, getStoreActions } from "../utils/helper"
|
||||
import type { FeedActions, FeedState } from "./types"
|
||||
|
||||
export const useFeedStore = createZustandStore<FeedState & FeedActions>(
|
||||
"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): Nullable<FeedModel> => useFeedStore.getState().feeds[feedId]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { FeedModel } from "@renderer/models"
|
||||
|
||||
type FeedId = string
|
||||
|
||||
export interface FeedState {
|
||||
feeds: Record<FeedId, FeedModel>
|
||||
}
|
||||
|
||||
export interface FeedActions {
|
||||
upsertMany: (feeds: FeedModel[]) => void
|
||||
optimisticUpdate: (feedId: FeedId, changed: Partial<FeedModel>) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
|
@ -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<SubscriptionModel, "feeds">
|
||||
interface SubscriptionState {
|
||||
data: Record<FeedId, SubscriptionModel>
|
||||
data: Record<FeedId, SubscriptionPlainModel>
|
||||
dataIdByView: Record<FeedViewType, FeedId[]>
|
||||
}
|
||||
|
||||
interface SubscriptionActions {
|
||||
upsert: (feedId: FeedId, subscription: SubscriptionModel) => void
|
||||
fetchByView: (view?: FeedViewType) => Promise<SubscriptionModel[]>
|
||||
upsertMany: (subscription: SubscriptionPlainModel[]) => void
|
||||
fetchByView: (view?: FeedViewType) => Promise<SubscriptionPlainModel[]>
|
||||
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
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { entryActions } from "../entry/entry"
|
||||
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()
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue