fix: mark feed unread dirty, refetch unread next time, fixed #1830

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-11-26 21:56:57 +08:00
parent 3cb6364462
commit 58d0e9c190
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
6 changed files with 155 additions and 41 deletions

View File

@ -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<string>())
// 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
})
}

View File

@ -40,5 +40,5 @@ export const useMarkAllByRoute = (filter?: MarkAllFilter) => {
filter,
})
}
}, [routerParams, folderIds, view, filter])
}, [routerParams, inboxId, folderIds, view, filter])
}

View File

@ -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,
},
)
}

View File

@ -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<unknown, { prevUnread: number }>({})
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<unknown, { prevIsStar: boolean }>({})
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: {

View File

@ -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() {

View File

@ -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
}