fix: retry scroll mark-read after revisiting entries
This commit is contained in:
parent
a52ff66fb3
commit
dc43e39711
|
|
@ -0,0 +1,18 @@
|
|||
import { unreadSyncService } from "@follow/store/unread/store"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { batchMarkRead } from "./useEntryMarkReadHandler"
|
||||
|
||||
vi.mock("@follow/store/unread/store", () => ({
|
||||
unreadSyncService: {
|
||||
queueEntriesAsRead: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe("batchMarkRead", () => {
|
||||
it("queues ids without requiring entries to exist in the local store", () => {
|
||||
batchMarkRead(["entry-1", "entry-2"])
|
||||
|
||||
expect(unreadSyncService.queueEntriesAsRead).toHaveBeenCalledWith(["entry-1", "entry-2"])
|
||||
})
|
||||
})
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { getView } from "@follow/constants"
|
||||
import { entryActions } from "@follow/store/entry/store"
|
||||
import { unreadSyncService } from "@follow/store/unread/store"
|
||||
import type { Range } from "@tanstack/react-virtual"
|
||||
import { useEffect, useMemo, useRef } from "react"
|
||||
import { useMemo } from "react"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
|
|
@ -18,26 +17,13 @@ export const useEntryMarkReadHandler = (
|
|||
const scrollMarkUnread = useGeneralSettingKey("scrollMarkUnread")
|
||||
const feedView = useRouteParamsSelector((params) => params.view)
|
||||
|
||||
const processedEntryIds = useRef(new Set<string>())
|
||||
|
||||
useEffect(() => {
|
||||
processedEntryIds.current.clear()
|
||||
}, [entriesIds])
|
||||
|
||||
const handleRangeMarkRead = useEventCallback(
|
||||
({ startIndex, endIndex }: Range, enabled?: boolean) => {
|
||||
if (!enabled) return
|
||||
const idSlice = entriesIds?.slice(startIndex, endIndex)
|
||||
if (!idSlice) return
|
||||
if (!idSlice?.length) return
|
||||
|
||||
// Filter out entries that have already been processed
|
||||
const newEntries = idSlice.filter((id) => !processedEntryIds.current.has(id))
|
||||
if (newEntries.length === 0) return
|
||||
|
||||
// Mark these entries as processed to avoid duplicate processing
|
||||
newEntries.forEach((id) => processedEntryIds.current.add(id))
|
||||
|
||||
batchMarkRead(newEntries)
|
||||
batchMarkRead(idSlice)
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -71,19 +57,6 @@ export const useEntryMarkReadHandler = (
|
|||
}
|
||||
|
||||
export function batchMarkRead(ids: string[]) {
|
||||
const batchLikeIds = [] as string[]
|
||||
const entriesId2Map = entryActions.getFlattenMapEntries()
|
||||
for (const id of ids) {
|
||||
const entry = entriesId2Map[id]
|
||||
|
||||
if (!entry) continue
|
||||
const isRead = entry.read
|
||||
if (!isRead && entry.feedId) {
|
||||
batchLikeIds.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
if (batchLikeIds.length > 0) {
|
||||
void unreadSyncService.queueEntriesAsRead(batchLikeIds)
|
||||
}
|
||||
if (ids.length === 0) return
|
||||
void unreadSyncService.queueEntriesAsRead(ids)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { FeedViewType, getView } from "@follow/constants"
|
||||
import { useScrollMarkReadGracePeriod, useTitle } from "@follow/hooks"
|
||||
import { getScrollMarkReadRange } from "@follow/shared/scroll-mark-read"
|
||||
import { getScrollMarkReadRangeState } from "@follow/shared/scroll-mark-read"
|
||||
import { useEntry } from "@follow/store/entry/hooks"
|
||||
import { useFeedById } from "@follow/store/feed/hooks"
|
||||
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
|
||||
|
|
@ -43,11 +43,11 @@ function EntryColumnContent() {
|
|||
const state = useEntriesState()
|
||||
|
||||
const isInteracted = useRef(false)
|
||||
const scrollMarkReadEndIndexRef = useRef<number | null>(null)
|
||||
const scrollMarkReadAnchorIndexRef = useRef<number | null>(null)
|
||||
const latestRangeStartIndexRef = useRef<number | null>(null)
|
||||
const resetScrollInteractionState = useCallback(() => {
|
||||
isInteracted.current = false
|
||||
scrollMarkReadEndIndexRef.current = null
|
||||
scrollMarkReadAnchorIndexRef.current = null
|
||||
latestRangeStartIndexRef.current = null
|
||||
}, [])
|
||||
|
||||
|
|
@ -133,19 +133,14 @@ function EntryColumnContent() {
|
|||
(currentStartIndex: number) => {
|
||||
if (!routeFeedId) return
|
||||
|
||||
const range = getScrollMarkReadRange({
|
||||
previousEndIndex: scrollMarkReadEndIndexRef.current,
|
||||
const { nextAnchorIndex, range } = getScrollMarkReadRangeState({
|
||||
anchorIndex: scrollMarkReadAnchorIndexRef.current,
|
||||
currentStartIndex,
|
||||
})
|
||||
scrollMarkReadAnchorIndexRef.current = nextAnchorIndex
|
||||
|
||||
if (range) {
|
||||
handleScrollMarkRead?.(range as Range, isInteracted.current)
|
||||
scrollMarkReadEndIndexRef.current = currentStartIndex
|
||||
return
|
||||
}
|
||||
|
||||
if (scrollMarkReadEndIndexRef.current === null) {
|
||||
scrollMarkReadEndIndexRef.current = currentStartIndex
|
||||
}
|
||||
},
|
||||
[handleScrollMarkRead, routeFeedId],
|
||||
|
|
@ -182,8 +177,8 @@ function EntryColumnContent() {
|
|||
}
|
||||
|
||||
latestRangeStartIndexRef.current = e.startIndex
|
||||
if (scrollMarkReadEndIndexRef.current === null) {
|
||||
scrollMarkReadEndIndexRef.current = e.startIndex
|
||||
if (scrollMarkReadAnchorIndexRef.current === null) {
|
||||
scrollMarkReadAnchorIndexRef.current = e.startIndex
|
||||
} else if (isInteracted.current) {
|
||||
flushScrollMarkRead(e.startIndex)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
getScrollMarkReadEndPadding,
|
||||
getScrollMarkReadExitedSliceEnd,
|
||||
getScrollMarkReadRange,
|
||||
getScrollMarkReadRangeState,
|
||||
MIN_SCROLL_MARK_READ_END_PADDING,
|
||||
SCROLL_MARK_READ_END_INDICATOR_HEIGHT,
|
||||
shouldRenderScrollMarkReadEndSpacer,
|
||||
|
|
@ -63,4 +64,26 @@ describe("scroll mark-read range", () => {
|
|||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it("moves the anchor backward while scrolling up so entries can be retried", () => {
|
||||
expect(
|
||||
getScrollMarkReadRangeState({
|
||||
anchorIndex: 12,
|
||||
currentStartIndex: 8,
|
||||
}),
|
||||
).toEqual({
|
||||
nextAnchorIndex: 8,
|
||||
range: null,
|
||||
})
|
||||
|
||||
expect(
|
||||
getScrollMarkReadRangeState({
|
||||
anchorIndex: 8,
|
||||
currentStartIndex: 12,
|
||||
}),
|
||||
).toEqual({
|
||||
nextAnchorIndex: 12,
|
||||
range: { startIndex: 8, endIndex: 12 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -46,6 +46,36 @@ export const getScrollMarkReadRange = ({
|
|||
}
|
||||
}
|
||||
|
||||
export const getScrollMarkReadRangeState = ({
|
||||
anchorIndex,
|
||||
currentStartIndex,
|
||||
}: {
|
||||
anchorIndex: number | null | undefined
|
||||
currentStartIndex: number | null | undefined
|
||||
}) => {
|
||||
if (
|
||||
typeof currentStartIndex !== "number" ||
|
||||
!Number.isInteger(currentStartIndex) ||
|
||||
currentStartIndex < 0
|
||||
) {
|
||||
return {
|
||||
nextAnchorIndex:
|
||||
typeof anchorIndex === "number" && Number.isInteger(anchorIndex) && anchorIndex >= 0
|
||||
? anchorIndex
|
||||
: null,
|
||||
range: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nextAnchorIndex: currentStartIndex,
|
||||
range: getScrollMarkReadRange({
|
||||
previousEndIndex: anchorIndex,
|
||||
currentStartIndex,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const getScrollMarkReadExitedSliceEnd = ({
|
||||
indexes,
|
||||
renderedEndIndex,
|
||||
|
|
|
|||
Loading…
Reference in New Issue