From 58d0e9c1902157c2e9fb1444bc1c0db0b775485b Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 26 Nov 2024 21:56:57 +0800 Subject: [PATCH] fix: mark feed unread dirty, refetch unread next time, fixed #1830 Signed-off-by: Innei --- apps/renderer/src/atoms/feed.ts | 69 +++++++++++++ .../modules/entry-column/hooks/useMarkAll.ts | 2 +- apps/renderer/src/queries/entries.ts | 14 ++- apps/renderer/src/store/entry/store.ts | 97 +++++++++++-------- apps/renderer/src/store/subscription/store.ts | 12 ++- apps/renderer/src/store/unread/index.ts | 2 + 6 files changed, 155 insertions(+), 41 deletions(-) create mode 100644 apps/renderer/src/atoms/feed.ts diff --git a/apps/renderer/src/atoms/feed.ts b/apps/renderer/src/atoms/feed.ts new file mode 100644 index 000000000..d5a5d9e87 --- /dev/null +++ b/apps/renderer/src/atoms/feed.ts @@ -0,0 +1,69 @@ +import { jotaiStore } from "@follow/utils/jotai" +import { isBizId } from "@follow/utils/utils" +import { atom, useAtomValue } from "jotai" +import { selectAtom } from "jotai/utils" +import { useMemo } from "react" + +import { + FEED_COLLECTION_LIST, + INBOX_PREFIX_ID, + ROUTE_FEED_IN_LIST, + ROUTE_FEED_PENDING, +} from "~/constants" + +const feedUnreadDirtySetAtom = atom(new Set()) + +// 1. feedId may be feedId, or `inbox-id` or `feedId, feedId,` or `list-id`, or `all`, or `collections` +export const useFeedUnreadIsDirty = (feedId: string) => { + return useAtomValue( + useMemo( + () => + selectAtom(feedUnreadDirtySetAtom, (set) => { + const isRealFeedId = isBizId(feedId) + if (isRealFeedId) return set.has(feedId) + + if (feedId.startsWith(ROUTE_FEED_IN_LIST) || feedId.startsWith(INBOX_PREFIX_ID)) { + // List/Inbox is not supported unread + return false + } + + if (feedId === ROUTE_FEED_PENDING) { + return set.size > 0 + } + + if (feedId === FEED_COLLECTION_LIST) { + // Entry in collections has not unread status + return false + } + + const splitted = feedId.split(",") + let isDirty = false + for (const feedId of splitted) { + if (isBizId(feedId)) { + isDirty = isDirty || set.has(feedId) + + if (isDirty) break + } + } + return isDirty + }), + [feedId], + ), + ) +} + +export const setFeedUnreadDirty = (feedId: string) => { + jotaiStore.set(feedUnreadDirtySetAtom, (prev) => { + const newSet = new Set(prev) + newSet.add(feedId) + return newSet + }) +} + +export const clearFeedUnreadDirty = (feedId: string) => { + jotaiStore.set(feedUnreadDirtySetAtom, (prev) => { + const newSet = new Set(prev) + newSet.delete(feedId) + return newSet + }) +} diff --git a/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts b/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts index ac81c1ffa..8289bed7c 100644 --- a/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts +++ b/apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts @@ -40,5 +40,5 @@ export const useMarkAllByRoute = (filter?: MarkAllFilter) => { filter, }) } - }, [routerParams, folderIds, view, filter]) + }, [routerParams, inboxId, folderIds, view, filter]) } diff --git a/apps/renderer/src/queries/entries.ts b/apps/renderer/src/queries/entries.ts index a602f57f8..8b9daab6c 100644 --- a/apps/renderer/src/queries/entries.ts +++ b/apps/renderer/src/queries/entries.ts @@ -1,3 +1,4 @@ +import { useFeedUnreadIsDirty } from "~/atoms/feed" import { useGeneralSettingKey } from "~/atoms/settings/general" import { useAuthInfiniteQuery, useAuthQuery } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" @@ -144,6 +145,9 @@ export const useEntries = ({ isArchived?: boolean }) => { const reduceRefetch = useGeneralSettingKey("reduceRefetch") + const fetchUnread = read === false + const feedUnreadDirty = useFeedUnreadIsDirty((feedId as string) || "") + return useAuthInfiniteQuery( entries.entries({ feedId, inboxId, listId, view, read, isArchived }), { @@ -155,8 +159,16 @@ export const useEntries = ({ initialPageParam: undefined, refetchOnWindowFocus: false, refetchOnReconnect: false, + refetchOnMount: fetchUnread && feedUnreadDirty ? "always" : false, - staleTime: reduceRefetch ? maxStaleTime : defaultStaleTime, + staleTime: + // Force refetch unread entries when feed is dirty + fetchUnread && feedUnreadDirty + ? 0 + : // Keep reduce data fetch logic + reduceRefetch + ? maxStaleTime + : defaultStaleTime, }, ) } diff --git a/apps/renderer/src/store/entry/store.ts b/apps/renderer/src/store/entry/store.ts index f9b7449cf..d5eabd377 100644 --- a/apps/renderer/src/store/entry/store.ts +++ b/apps/renderer/src/store/entry/store.ts @@ -11,6 +11,7 @@ import { omitObjectUndefinedValue } from "@follow/utils/utils" import { isNil, merge, omit } from "es-toolkit/compat" import { produce } from "immer" +import { clearFeedUnreadDirty, setFeedUnreadDirty } from "~/atoms/feed" import { runTransactionInScope } from "~/database" import { apiClient } from "~/lib/api-fetch" import { getEntriesParams } from "~/lib/utils" @@ -123,43 +124,61 @@ class EntryActions { pageParam?: string isArchived?: boolean }) { - const data = inboxId - ? await apiClient.entries.inbox - .$post({ - json: { - publishedAfter: pageParam, - limit, - inboxId: `${inboxId}`, - read, - }, - }) - .then((res) => { - return { - ...res, - data: res.data?.map(({ feeds, ...d }) => { - return { - ...d, - inboxes: feeds, - } - }), - } - }) - : await apiClient.entries.$post({ + if (inboxId) { + const data = await apiClient.entries.inbox + .$post({ json: { publishedAfter: pageParam, - read, limit, - isArchived, - // withContent: true, - ...getEntriesParams({ - feedId, - inboxId, - listId, - view, - }), + inboxId: `${inboxId}`, + read, }, }) + .then((res) => { + return { + ...res, + data: res.data?.map(({ feeds, ...d }) => { + return { + ...d, + inboxes: feeds, + } + }), + } + }) + if (data.data) { + this.upsertMany(data.data, { isArchived }) + } + return data + } + + const params = getEntriesParams({ + feedId, + inboxId, + listId, + view, + }) + const data = await apiClient.entries.$post({ + json: { + publishedAfter: pageParam, + read, + limit, + isArchived, + ...params, + }, + }) + + // Mark feed unread dirty, so re-fetch the unread data when view feed unread entires in the next time + if (read === false) { + if (params.feedId) { + clearFeedUnreadDirty(params.feedId as string) + } + if (params.feedIdList) { + params.feedIdList.forEach((feedId) => { + clearFeedUnreadDirty(feedId) + }) + } + } if (data.data) { this.upsertMany(data.data, { isArchived }) } @@ -431,7 +450,7 @@ class EntryActions { const tx = createTransaction({}) - tx.optimistic(async (_, ctx) => { + tx.optimistic((_, ctx) => { const prevUnread = feedUnreadActions.incrementByFeedId(feedId, read ? -1 : 1) ctx.prevUnread = prevUnread @@ -456,13 +475,13 @@ class EntryActions { } }) - tx.persist(async () => { + tx.persist(() => { EntryService.bulkStoreReadStatus({ [entryId]: read, }) }) - tx.rollback(async (_, ctx) => { + tx.rollback((_, ctx) => { feedUnreadActions.updateByFeedId(feedId, ctx.prevUnread) this.patch(entryId, { read: !read, @@ -470,12 +489,14 @@ class EntryActions { }) await tx.run() + + setFeedUnreadDirty(feedId) } async markStar(entryId: string, star: boolean, view?: FeedViewType) { const tx = createTransaction({}) - tx.optimistic(async (_, ctx) => { + tx.optimistic((_, ctx) => { ctx.prevIsStar = !!get().flatMapEntries[entryId]?.collections?.createdAt this.patch(entryId, { collections: star @@ -508,7 +529,7 @@ class EntryActions { } }) - tx.rollback(async (_, ctx) => { + tx.rollback((_, ctx) => { set((state) => produce(state, (state) => { ctx.prevIsStar ? state.starIds.add(entryId) : state.starIds.delete(entryId) @@ -555,7 +576,7 @@ class EntryActions { deletedIndex: -1, }) - tx.optimistic(async (entry, ctx) => { + tx.optimistic((entry, ctx) => { const { inboxId } = entry const fullInboxId = `inbox-${inboxId}` @@ -594,7 +615,7 @@ class EntryActions { await EntryService.deleteEntries([entryId]) }) - tx.rollback(async (entry, ctx) => { + tx.rollback((entry, ctx) => { set((state) => ({ ...state, entries: { diff --git a/apps/renderer/src/store/subscription/store.ts b/apps/renderer/src/store/subscription/store.ts index 215d4f08f..8b37a6e49 100644 --- a/apps/renderer/src/store/subscription/store.ts +++ b/apps/renderer/src/store/subscription/store.ts @@ -10,6 +10,7 @@ import { omit } from "es-toolkit/compat" import { produce } from "immer" import { parse } from "tldts" +import { setFeedUnreadDirty } from "~/atoms/feed" import { whoami } from "~/atoms/user" import { runTransactionInScope } from "~/database" import { apiClient } from "~/lib/api-fetch" @@ -270,6 +271,11 @@ class SubscriptionActions { await feedUnreadActions.fetchUnreadByView(view) }) await tx.run() + + const feedIdsInView = get().feedIdByView[view] + for (const feedId of feedIdsInView) { + setFeedUnreadDirty(feedId) + } } async markReadByFeedIds({ @@ -307,7 +313,7 @@ class SubscriptionActions { }, }) }) - tx.optimistic(async () => { + tx.optimistic(() => { if (listId) { feedUnreadActions.updateByFeedId(listId, 0) } else if (inboxId) { @@ -331,6 +337,10 @@ class SubscriptionActions { }) await tx.run() + + for (const feedId of stableFeedIds) { + setFeedUnreadDirty(feedId) + } } clear() { diff --git a/apps/renderer/src/store/unread/index.ts b/apps/renderer/src/store/unread/index.ts index 76051190c..a449e631e 100644 --- a/apps/renderer/src/store/unread/index.ts +++ b/apps/renderer/src/store/unread/index.ts @@ -1,5 +1,6 @@ import type { FeedViewType } from "@follow/constants" +import { setFeedUnreadDirty } from "~/atoms/feed" import { apiClient } from "~/lib/api-fetch" import { FeedUnreadService } from "~/services" @@ -78,6 +79,7 @@ class FeedUnreadActions { const nextValue = Math.max(0, (cur || 0) + inc) this.internal_setValue([[feedId, nextValue]]) + setFeedUnreadDirty(feedId) return cur }