fix: keep hovered unread entries visible (#4948)

This commit is contained in:
DIYgod 2026-04-02 15:28:30 +08:00 committed by GitHub
parent d27261d76e
commit 03ccfa8c56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 107 additions and 11 deletions

View File

@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest"
import { getVisibleLocalEntryIds } from "./filter-local-entry-ids"
describe("getVisibleLocalEntryIds", () => {
it("keeps previously visible unread entries when they turn read locally", () => {
expect(
getVisibleLocalEntryIds({
sourceIds: ["entry-1", "entry-2"],
entries: {
"entry-1": { id: "entry-1", read: true },
"entry-2": { id: "entry-2", read: false },
},
stickyVisibleIds: new Set(["entry-1", "entry-2"]),
unreadOnly: true,
}),
).toEqual(["entry-1", "entry-2"])
})
it("filters read entries that were not previously visible", () => {
expect(
getVisibleLocalEntryIds({
sourceIds: ["entry-1", "entry-2"],
entries: {
"entry-1": { id: "entry-1", read: true },
"entry-2": { id: "entry-2", read: false },
},
stickyVisibleIds: new Set<string>(),
unreadOnly: true,
}),
).toEqual(["entry-2"])
})
it("removes sticky entries once they leave the source list", () => {
expect(
getVisibleLocalEntryIds({
sourceIds: ["entry-2"],
entries: {
"entry-1": { id: "entry-1", read: true },
"entry-2": { id: "entry-2", read: false },
},
stickyVisibleIds: new Set(["entry-1", "entry-2"]),
unreadOnly: true,
}),
).toEqual(["entry-2"])
})
})

View File

@ -0,0 +1,27 @@
type LocalEntryVisibility = {
id: string
read?: boolean | null
}
export const getVisibleLocalEntryIds = <TEntry extends LocalEntryVisibility>({
sourceIds,
entries,
stickyVisibleIds,
unreadOnly,
}: {
sourceIds: string[]
entries: Record<string, TEntry | null | undefined>
stickyVisibleIds?: ReadonlySet<string>
unreadOnly: boolean
}) => {
return sourceIds.filter((id) => {
const entry = entries[id]
if (!entry) return false
if (unreadOnly && !!entry.read && !stickyVisibleIds?.has(entry.id)) {
return false
}
return true
})
}

View File

@ -19,7 +19,7 @@ import { isBizId } from "@follow/utils/utils"
import { useMutation } from "@tanstack/react-query"
import { debounce } from "es-toolkit/compat"
import { useAtomValue } from "jotai"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useGeneralSettingKey } from "~/atoms/settings/general"
import { ROUTE_FEED_PENDING } from "~/constants/app"
@ -27,6 +27,7 @@ import { useFeature } from "~/hooks/biz/useFeature"
import { useRouteParams } from "~/hooks/biz/useRouteParams"
import { aiTimelineEnabledAtom } from "../atoms/ai-timeline"
import { getVisibleLocalEntryIds } from "./filter-local-entry-ids"
import { useIsPreviewFeed } from "./useIsPreviewFeed"
const useRemoteEntries = (): UseEntriesReturn => {
@ -138,6 +139,18 @@ const useLocalEntries = (): UseEntriesReturn => {
!inboxId &&
!listId
const localQueryKey = useMemo(
() => [feedId || "", view, inboxId || "", listId || "", isCollection ? "1" : "0"].join(":"),
[feedId, inboxId, isCollection, listId, view],
)
const stickyVisibleStateRef = useRef<{
queryKey: string
ids: Set<string>
}>({
queryKey: localQueryKey,
ids: new Set<string>(),
})
const allEntries = useEntryStore(
useCallback(
(state) => {
@ -152,16 +165,17 @@ const useLocalEntries = (): UseEntriesReturn => {
entryIdsByInboxId,
) ?? [])
return ids
.map((id) => {
const entry = state.data[id]
if (!entry) return null
if (unreadOnly && entry.read) {
return null
}
return entry.id
})
.filter((id) => typeof id === "string")
const stickyVisibleIds =
unreadOnly && stickyVisibleStateRef.current.queryKey === localQueryKey
? stickyVisibleStateRef.current.ids
: undefined
return getVisibleLocalEntryIds({
sourceIds: ids,
entries: state.data,
stickyVisibleIds,
unreadOnly,
})
},
[
entryIdsByCategory,
@ -171,12 +185,20 @@ const useLocalEntries = (): UseEntriesReturn => {
entryIdsByListId,
entryIdsByView,
isCollection,
localQueryKey,
showEntriesByView,
unreadOnly,
],
),
)
useEffect(() => {
stickyVisibleStateRef.current = {
queryKey: localQueryKey,
ids: unreadOnly ? new Set(allEntries) : new Set<string>(),
}
}, [allEntries, localQueryKey, unreadOnly])
const [page, setPage] = useState(0)
const pageSize = 30
const totalPage = useMemo(