mirror of
https://github.com/RSSNext/Folo.git
synced 2026-10-29 03:46:58 +08:00
fix(mobile): keep marking entries read after the first one scrolls past
Marking entries read on scroll stopped after the first entry. Once the scrolled-out entries were handed off, the list kept for them was stored again as a new array, an empty one when all of them were handed off. That ran the marking effect again, and since marking goes through the transaction queue, marking nothing resolves at once, so the effect ran every few milliseconds without end. It kept the JS thread busy: no later entry was reported as scrolled out, and the queue's flush timer never fired, so even the first entry only reached the server on the next launch. Store the same list when nothing changed, and none when nothing is left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
306460b755
commit
4e489f4e5e
@@ -8,7 +8,10 @@ import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"
|
||||
|
||||
import { useGeneralSettingKey } from "@/src/atoms/settings/general"
|
||||
|
||||
import { shouldCollectViewableItemsForMarkRead } from "./viewable-mark-read"
|
||||
import {
|
||||
getPendingMarkReadItems,
|
||||
shouldCollectViewableItemsForMarkRead,
|
||||
} from "./viewable-mark-read"
|
||||
|
||||
const defaultIdExtractor = (item: ViewToken<string>) => item.key
|
||||
export function useOnViewableItemsChanged({
|
||||
@@ -95,10 +98,7 @@ export function useOnViewableItemsChanged({
|
||||
const entryIds = lastRemovedItems.map((item) => stableIdExtractor(item))
|
||||
const entryIdSet = new Set(entryIds)
|
||||
void unreadSyncService.queueEntriesAsRead(entryIds).then(() => {
|
||||
setLastRemovedItems((prev) => {
|
||||
if (!prev) return null
|
||||
return prev.filter((prevItem) => !entryIdSet.has(stableIdExtractor(prevItem)))
|
||||
})
|
||||
setLastRemovedItems((prev) => getPendingMarkReadItems(prev, entryIdSet, stableIdExtractor))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { shouldCollectViewableItemsForMarkRead } from "./viewable-mark-read"
|
||||
import {
|
||||
getPendingMarkReadItems,
|
||||
shouldCollectViewableItemsForMarkRead,
|
||||
} from "./viewable-mark-read"
|
||||
|
||||
describe("shouldCollectViewableItemsForMarkRead", () => {
|
||||
test("does not collect viewable items while disabled", () => {
|
||||
@@ -37,3 +40,30 @@ describe("shouldCollectViewableItemsForMarkRead", () => {
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getPendingMarkReadItems", () => {
|
||||
const getId = (item: { key: string }) => item.key
|
||||
|
||||
test("keeps the items that were not handed off yet", () => {
|
||||
const pending = { key: "b" }
|
||||
|
||||
expect(
|
||||
getPendingMarkReadItems([{ key: "a" }, pending, { key: "c" }], new Set(["a", "c"]), getId),
|
||||
).toEqual([pending])
|
||||
})
|
||||
|
||||
// The hook marks the stored items whenever they change, and marking entries that are already
|
||||
// read resolves at once, so a new array for an unchanged or empty list would mark forever
|
||||
test("is null once every handed-off item was marked", () => {
|
||||
expect(getPendingMarkReadItems([{ key: "a" }], new Set(["a"]), getId)).toBeNull()
|
||||
expect(getPendingMarkReadItems(null, new Set(["a"]), getId)).toBeNull()
|
||||
})
|
||||
|
||||
test("returns the same list when none of its items was handed off", () => {
|
||||
const items = [{ key: "a" }]
|
||||
const empty: { key: string }[] = []
|
||||
|
||||
expect(getPendingMarkReadItems(items, new Set(["b"]), getId)).toBe(items)
|
||||
expect(getPendingMarkReadItems(empty, new Set(["a"]), getId)).toBe(empty)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,3 +7,21 @@ export const shouldCollectViewableItemsForMarkRead = ({
|
||||
isScrollingDown: boolean
|
||||
offset: number
|
||||
}) => !disabled && isScrollingDown && offset > 0
|
||||
|
||||
/**
|
||||
* The scrolled-out items still waiting to be marked read once `markedIds` were handed off.
|
||||
* The result is stored in state that triggers the marking, so it keeps the same array when
|
||||
* nothing changed and is null when nothing is left. Any new array would mark again, which
|
||||
* resolves at once for entries that are already read, and so would loop without end.
|
||||
*/
|
||||
export const getPendingMarkReadItems = <T>(
|
||||
items: T[] | null,
|
||||
markedIds: ReadonlySet<string>,
|
||||
getId: (item: T) => string,
|
||||
): T[] | null => {
|
||||
if (!items) return null
|
||||
|
||||
const pending = items.filter((item) => !markedIds.has(getId(item)))
|
||||
if (pending.length === items.length) return items
|
||||
return pending.length > 0 ? pending : null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user