Skip to content

Commit f41a4ee

Browse files
committed
fix: pause scroll mark read after refresh
1 parent 22e0e90 commit f41a4ee

10 files changed

Lines changed: 165 additions & 28 deletions

File tree

apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useMasonryColumn } from "@follow/components/ui/masonry/hooks.js"
99
import { Masonry } from "@follow/components/ui/masonry/index.js"
1010
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
1111
import { Skeleton } from "@follow/components/ui/skeleton/index.jsx"
12-
import { useRefValue } from "@follow/hooks"
12+
import { useRefValue, useScrollMarkReadGracePeriod } from "@follow/hooks"
1313
import { getEntry } from "@follow/store/entry/getter"
1414
import { useEntryTranslation } from "@follow/store/translation/hooks"
1515
import { clsx } from "@follow/utils/utils"
@@ -36,6 +36,7 @@ import { MediaContainerWidthProvider } from "~/components/ui/media/MediaContaine
3636
import type { StoreImageType } from "~/store/image"
3737
import { imageActions } from "~/store/image"
3838

39+
import { useEntriesState } from "../context/EntriesContext"
3940
import { batchMarkRead } from "../hooks/useEntryMarkReadHandler"
4041
import { PictureWaterFallItem } from "./picture-item"
4142

@@ -48,6 +49,10 @@ const gutter = 24
4849

4950
export const PictureMasonry: FC<MasonryProps> = (props) => {
5051
const { data } = props
52+
const entriesState = useEntriesState()
53+
const pauseScrollMarkRead = useScrollMarkReadGracePeriod(
54+
entriesState.isFetching && !entriesState.isFetchingNextPage,
55+
)
5156
const cacheMap = useState(() => new Map<string, object>())[0]
5257
const [isInitDim, setIsInitDim] = useState(false)
5358
const [isInitLayout, setIsInitLayout] = useState(false)
@@ -155,6 +160,7 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
155160

156161
function scrollOutViewMarkRead(entries: IntersectionObserverEntry[]) {
157162
if (!scrollMarkRead) return
163+
if (pauseScrollMarkRead) return
158164
if (!scrollElement) return
159165
let minimumIndex = Number.MAX_SAFE_INTEGER
160166
entries.forEach((entry) => {
@@ -210,7 +216,7 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
210216
return () => {
211217
observer.disconnect()
212218
}
213-
}, [scrollElement, renderMarkRead, scrollMarkRead, dataRef])
219+
}, [dataRef, pauseScrollMarkRead, renderMarkRead, scrollElement, scrollMarkRead])
214220

215221
const [firstScreenReady, setFirstScreenReady] = useState(false)
216222
useEffect(() => {

apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntryMarkReadHandler.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,72 @@ import { getView } from "@follow/constants"
22
import { entryActions } from "@follow/store/entry/store"
33
import { unreadSyncService } from "@follow/store/unread/store"
44
import type { Range } from "@tanstack/react-virtual"
5-
import { useMemo } from "react"
5+
import { useEffect, useMemo, useRef } from "react"
66
import { useEventCallback } from "usehooks-ts"
77

88
import { useGeneralSettingKey } from "~/atoms/settings/general"
99
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
1010

11-
export const useEntryMarkReadHandler = (entriesIds: string[]) => {
11+
type EntryMarkReadHandler = (range: Range, enabled?: boolean) => void
12+
13+
export const useEntryMarkReadHandler = (
14+
entriesIds: string[],
15+
{ pauseScrollMarkRead = false }: { pauseScrollMarkRead?: boolean } = {},
16+
) => {
1217
const renderAsRead = useGeneralSettingKey("renderMarkUnread")
1318
const scrollMarkUnread = useGeneralSettingKey("scrollMarkUnread")
1419
const feedView = useRouteParamsSelector((params) => params.view)
1520

16-
const processedEntryIds = useMemo(() => new Set<string>(), [entriesIds])
21+
const processedEntryIds = useRef(new Set<string>())
22+
23+
useEffect(() => {
24+
processedEntryIds.current.clear()
25+
}, [entriesIds])
1726

18-
const handleRenderAsRead = useEventCallback(
27+
const handleRangeMarkRead = useEventCallback(
1928
({ startIndex, endIndex }: Range, enabled?: boolean) => {
2029
if (!enabled) return
2130
const idSlice = entriesIds?.slice(startIndex, endIndex)
2231
if (!idSlice) return
2332

2433
// Filter out entries that have already been processed
25-
const newEntries = idSlice.filter((id) => !processedEntryIds.has(id))
34+
const newEntries = idSlice.filter((id) => !processedEntryIds.current.has(id))
2635
if (newEntries.length === 0) return
2736

2837
// Mark these entries as processed to avoid duplicate processing
29-
newEntries.forEach((id) => processedEntryIds.add(id))
38+
newEntries.forEach((id) => processedEntryIds.current.add(id))
3039

3140
batchMarkRead(newEntries)
3241
},
3342
)
3443

35-
return useMemo(() => {
36-
if (getView(feedView)?.wideMode && renderAsRead) {
37-
return handleRenderAsRead
44+
const handleScrollMarkRead = useEventCallback((range: Range, enabled?: boolean) => {
45+
if (pauseScrollMarkRead) return
46+
handleRangeMarkRead(range, enabled)
47+
})
48+
49+
const renderMarkReadHandler = useMemo<EntryMarkReadHandler | undefined>(() => {
50+
if (!getView(feedView)?.wideMode || !renderAsRead) {
51+
return
3852
}
3953

40-
if (scrollMarkUnread) {
41-
return handleRenderAsRead
54+
return handleRangeMarkRead
55+
}, [feedView, handleRangeMarkRead, renderAsRead])
56+
57+
const scrollMarkReadHandler = useMemo<EntryMarkReadHandler | undefined>(() => {
58+
if (!scrollMarkUnread) {
59+
return
60+
}
61+
62+
return handleScrollMarkRead
63+
}, [handleScrollMarkRead, scrollMarkUnread])
64+
65+
return useMemo(() => {
66+
return {
67+
handleRenderMarkRead: renderMarkReadHandler,
68+
handleScrollMarkRead: scrollMarkReadHandler,
4269
}
43-
return
44-
}, [feedView, handleRenderAsRead, renderAsRead, scrollMarkUnread])
70+
}, [renderMarkReadHandler, scrollMarkReadHandler])
4571
}
4672

4773
export function batchMarkRead(ids: string[]) {

apps/desktop/layer/renderer/src/modules/entry-column/index.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FeedViewType, getView } from "@follow/constants"
2-
import { useTitle } from "@follow/hooks"
2+
import { useScrollMarkReadGracePeriod, useTitle } from "@follow/hooks"
33
import { useEntry } from "@follow/store/entry/hooks"
44
import { useFeedById } from "@follow/store/feed/hooks"
55
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
@@ -80,8 +80,12 @@ function EntryColumnContent() {
8080
}, [activeEntryId, entry?.feedId, isCollection, isPendingEntry, isLoggedIn])
8181

8282
const isInteracted = useRef(false)
83+
const isRefreshing = state.isFetching && !state.isFetchingNextPage
84+
const pauseScrollMarkRead = useScrollMarkReadGracePeriod(isRefreshing)
8385

84-
const handleMarkReadInRange = useEntryMarkReadHandler(entriesIds)
86+
const { handleRenderMarkRead, handleScrollMarkRead } = useEntryMarkReadHandler(entriesIds, {
87+
pauseScrollMarkRead,
88+
})
8589

8690
const handleScroll = useCallback(() => {
8791
if (!isInteracted.current) {
@@ -92,15 +96,15 @@ function EntryColumnContent() {
9296

9397
const [first, second] = rangeQueueRef.current
9498
if (first && second && second.startIndex - first.startIndex > 0) {
95-
handleMarkReadInRange?.(
99+
handleScrollMarkRead?.(
96100
{
97101
startIndex: first.startIndex,
98102
endIndex: second.startIndex,
99103
} as Range,
100104
isInteracted.current,
101105
)
102106
}
103-
}, [handleMarkReadInRange, routeFeedId])
107+
}, [handleScrollMarkRead, routeFeedId])
104108

105109
const { handleScroll: handleScrollBeyond } = useAttachScrollBeyond()
106110
const handleCombinedScroll = useCallback(
@@ -114,7 +118,6 @@ function EntryColumnContent() {
114118
const navigate = useNavigateEntry()
115119

116120
const rangeQueueRef = useRef<Range[]>([])
117-
const isRefreshing = state.isFetching && !state.isFetchingNextPage
118121
const aiTimelineEnabled = useAtomValue(aiTimelineEnabledAtom)
119122
const showAiTimelineLoading = aiTimelineEnabled && state.isLoading && !state.isFetchingNextPage
120123
const renderAsRead = useGeneralSettingKey("renderMarkUnread")
@@ -134,9 +137,9 @@ function EntryColumnContent() {
134137
return
135138
}
136139
// For gird, render as mark read logic
137-
handleMarkReadInRange?.(e, isInteracted.current)
140+
handleRenderMarkRead?.(e, isInteracted.current)
138141
},
139-
[handleMarkReadInRange, renderAsRead, view],
142+
[handleRenderMarkRead, renderAsRead, view],
140143
)
141144

142145
const fetchNextPage = useCallback(() => {

apps/mobile/src/modules/entry-list/EntryListContentArticle.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@ export const EntryListContentArticle = ({
4040
const extraData: EntryExtraData = useMemo(() => ({ entryIds }), [entryIds])
4141
const readableItemStyle = useReadableContainerStyle(860, 16)
4242

43-
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage, fetchedTime, isReady } =
44-
useEntries({ viewId: view, active })
43+
const {
44+
fetchNextPage,
45+
isFetching,
46+
isFetchingNextPage,
47+
refetch,
48+
isRefetching,
49+
hasNextPage,
50+
fetchedTime,
51+
isReady,
52+
} = useEntries({ viewId: view, active })
4553

4654
const renderItem = useCallback(
4755
({ item: id, extraData, index }: ListRenderItemInfo<string>) => (
@@ -67,6 +75,7 @@ export const EntryListContentArticle = ({
6775

6876
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
6977
disabled: active === false || isFetching,
78+
refreshing: isFetching && !isFetchingNextPage,
7079
})
7180

7281
useImperativeHandle(forwardRef, () => ref.current!)

apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,21 @@ export const EntryListContentPicture = ({
4242
const isTablet = useIsTabletLayout()
4343

4444
useImperativeHandle(forwardRef, () => ref.current!)
45-
const { fetchNextPage, refetch, isRefetching, hasNextPage, isFetching, isReady } = useEntries({
45+
const {
46+
fetchNextPage,
47+
refetch,
48+
isRefetching,
49+
hasNextPage,
50+
isFetching,
51+
isFetchingNextPage,
52+
isReady,
53+
} = useEntries({
4654
viewId: view,
4755
active,
4856
})
4957
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
5058
disabled: active === false || isFetching,
59+
refreshing: isFetching && !isFetchingNextPage,
5160
})
5261
const translation = useGeneralSettingKey("translation")
5362
const translationMode = useGeneralSettingKey("translationMode")

apps/mobile/src/modules/entry-list/EntryListContentSocial.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ export const EntryListContentSocial = ({
2525
}: { entryIds: string[] | null; active?: boolean; view: FeedViewType } & {
2626
ref?: React.Ref<ElementRef<typeof TimelineSelectorList> | null>
2727
}) => {
28-
const { fetchNextPage, isFetching, refetch, isRefetching, hasNextPage, isReady } = useEntries({
28+
const {
29+
fetchNextPage,
30+
isFetching,
31+
isFetchingNextPage,
32+
refetch,
33+
isRefetching,
34+
hasNextPage,
35+
isReady,
36+
} = useEntries({
2937
viewId: view,
3038
active,
3139
})
@@ -48,6 +56,7 @@ export const EntryListContentSocial = ({
4856

4957
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
5058
disabled: active === false || isFetching,
59+
refreshing: isFetching && !isFetchingNextPage,
5160
})
5261

5362
const translation = useGeneralSettingKey("translation")

apps/mobile/src/modules/entry-list/EntryListContentVideo.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,21 @@ export const EntryListContentVideo = ({
3434
const ref = useRef<FlashListRef<any>>(null)
3535
useImperativeHandle(forwardRef, () => ref.current!)
3636
const isTablet = useIsTabletLayout()
37-
const { fetchNextPage, refetch, isRefetching, isFetching, hasNextPage, isReady } = useEntries({
37+
const {
38+
fetchNextPage,
39+
refetch,
40+
isRefetching,
41+
isFetching,
42+
isFetchingNextPage,
43+
hasNextPage,
44+
isReady,
45+
} = useEntries({
3846
viewId: view,
3947
active,
4048
})
4149
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
4250
disabled: active === false || isFetching,
51+
refreshing: isFetching && !isFetchingNextPage,
4352
})
4453

4554
const translation = useGeneralSettingKey("translation")

apps/mobile/src/modules/entry-list/hooks.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useScrollMarkReadGracePeriod } from "@follow/hooks"
12
import { debouncedFetchEntryContentByStream } from "@follow/store/entry/store"
23
import { unreadSyncService } from "@follow/store/unread/store"
34
import { useIsLoggedIn } from "@follow/store/user/hooks"
@@ -12,17 +13,20 @@ export function useOnViewableItemsChanged({
1213
disabled,
1314
idExtractor = defaultIdExtractor,
1415
onScroll: onScrollProp,
16+
refreshing,
1517
}: {
1618
disabled?: boolean
1719
idExtractor?: (item: ViewToken<string>) => string
1820
onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void
21+
refreshing?: boolean
1922
} = {}) {
2023
const orientation = useRef<"down" | "up">("down")
2124
const lastOffset = useRef(0)
2225
const isLoggedIn = useIsLoggedIn()
2326

2427
const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread")
2528
const markAsReadWhenRendering = useGeneralSettingKey("renderMarkUnread")
29+
const pauseScrollMarkRead = useScrollMarkReadGracePeriod(refreshing ?? false)
2630
const [viewableItems, setViewableItems] = useState<ViewToken<string>[]>([])
2731
const [lastViewableItems, setLastViewableItems] = useState<ViewToken<string>[] | null>()
2832
const [lastRemovedItems, setLastRemovedItems] = useState<ViewToken<string>[] | null>(null)
@@ -42,6 +46,11 @@ export function useOnViewableItemsChanged({
4246
// This can avoid misjudgment during the rebound of the pull-to-refresh (because the offset will change from negative to zero during the rebound).
4347
if (orientation.current === "down" && lastOffset.current > 0) {
4448
setLastViewableItems(viewableItems)
49+
if (pauseScrollMarkRead) {
50+
setLastRemovedItems(null)
51+
return
52+
}
53+
4554
if (removed.length > 0) {
4655
setLastRemovedItems((prev) => {
4756
if (prev) {
@@ -57,10 +66,18 @@ export function useOnViewableItemsChanged({
5766
}
5867
})
5968

69+
useEffect(() => {
70+
if (!disabled && !pauseScrollMarkRead) {
71+
return
72+
}
73+
74+
setLastRemovedItems(null)
75+
}, [disabled, pauseScrollMarkRead])
76+
6077
useEffect(() => {
6178
if (disabled) return
6279

63-
if (isLoggedIn && markAsReadWhenScrolling && lastRemovedItems) {
80+
if (isLoggedIn && markAsReadWhenScrolling && !pauseScrollMarkRead && lastRemovedItems) {
6481
lastRemovedItems.forEach((item) => {
6582
unreadSyncService.markEntryAsRead(stableIdExtractor(item)).then(() => {
6683
setLastRemovedItems((prev) => {
@@ -86,6 +103,7 @@ export function useOnViewableItemsChanged({
86103
lastViewableItems,
87104
markAsReadWhenRendering,
88105
markAsReadWhenScrolling,
106+
pauseScrollMarkRead,
89107
stableIdExtractor,
90108
])
91109

packages/internal/hooks/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from "./useOnce"
1414
export * from "./usePageVisibility"
1515
export * from "./usePrevious"
1616
export * from "./useRefValue"
17+
export * from "./useScrollMarkReadGracePeriod"
1718
export * from "./useSetState"
1819
export * from "./useSmoothScroll"
1920
export * from "./useSyncTheme"

0 commit comments

Comments
 (0)