Merge pull request #4267 from omnivore-app/fix/web-infinite-scroll

More debugging on infinite scroll
This commit is contained in:
Jackson Harper
2024-08-19 16:48:09 +08:00
committed by GitHub
12 changed files with 206 additions and 90 deletions

View File

@ -211,6 +211,7 @@ export function HomeContainer(): JSX.Element {
const shouldFallback =
homeData.error || (!homeData.isValidating && !hasTopPicks(homeData))
const searchData = useGetLibraryItems(
'home',
undefined,
{
limit: 10,

View File

@ -89,7 +89,7 @@ export function DiscoverContainer(): JSX.Element {
}
setPage(page + 1)
}, [page, isLoading])
useFetchMore(handleFetchMore)
// useFetchMore(handleFetchMore)
const handleSaveDiscover = async (
discoverArticleId: string,

View File

@ -54,8 +54,8 @@ import { theme } from '../../tokens/stitches.config'
import { emptyTrashMutation } from '../../../lib/networking/mutations/emptyTrashMutation'
import { State } from '../../../lib/networking/fragments/articleFragment'
import { useHandleAddUrl } from '../../../lib/hooks/useHandleAddUrl'
import { QueryClient, useQueryClient } from '@tanstack/react-query'
import { useGetViewer } from '../../../lib/networking/viewer/useGetViewer'
import { Spinner } from '@phosphor-icons/react/dist/ssr'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
@ -117,11 +117,14 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
const {
data: itemsPages,
isLoading,
isFetchingNextPage,
isFetching,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
error: fetchItemsError,
} = useGetLibraryItems(props.folder, queryInputs)
} = useGetLibraryItems(props.folder ?? 'home', props.folder, queryInputs)
useEffect(() => {
if (queryValue.startsWith('#')) {
@ -157,6 +160,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
}, [router.asPath])
const libraryItems = useMemo(() => {
console.log('library items: ', itemsPages)
const items =
itemsPages?.pages
.flatMap((ad: LibraryItems) => {
@ -184,16 +188,16 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
.map((li) => li.node.id)
}, [libraryItems])
const refreshProcessingItems = useRefreshProcessingItems()
// const refreshProcessingItems = useRefreshProcessingItems()
useEffect(() => {
if (processingItems.length) {
refreshProcessingItems.mutateAsync({
attempt: 0,
itemIds: processingItems,
})
}
}, [processingItems])
// useEffect(() => {
// if (processingItems.length) {
// refreshProcessingItems.mutateAsync({
// attempt: 0,
// itemIds: processingItems,
// })
// }
// }, [processingItems])
const focusFirstItem = useCallback(() => {
if (libraryItems.length < 1) {
@ -295,6 +299,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
}, [libraryItems, activeCardId])
useEffect(() => {
console.log('active card id: ', activeCardId)
if (activeCardId && !alreadyScrolled.current) {
scrollToActiveCard(activeCardId)
alreadyScrolled.current = true
@ -789,6 +794,33 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
[itemsPages, multiSelectMode, checkedItems]
)
// return (
// <InfiniteScroll
// dataLength={libraryItems.length}
// next={fetchNextPage}
// hasMore={hasNextPage}
// loader={<h4>Loading...</h4>}
// endMessage={
// <p style={{ textAlign: 'center' }}>
// <b>Yay! You have seen it all</b>
// </p>
// }
// >
// {libraryItems.map((item) => {
// return (
// <Box
// key={item.node.id}
// onClick={() => {
// router.push(`/${viewerData?.profile.username}/${item.node.slug}`)
// }}
// >
// {item.cursor}: {item.node.title}
// </Box>
// )
// })}
// </InfiniteScroll>
// )
return (
<HomeFeedGrid
folder={props.folder}
@ -821,7 +853,7 @@ export function LibraryContainer(props: LibraryContainerProps): JSX.Element {
loadMore={fetchNextPage}
hasMore={hasNextPage ?? false}
hasData={!!itemsPages}
isValidating={isLoading}
isValidating={isLoading || isFetchingNextPage}
fetchItemsError={!!fetchItemsError}
labelsTarget={labelsTarget}
setLabelsTarget={setLabelsTarget}
@ -1150,16 +1182,20 @@ export function LibraryItemsLayout(
css={{ width: '100%', mt: '$2', mb: '$4' }}
>
{props.hasMore ? (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'Load More'}
</Button>
props.isValidating ? (
<Spinner />
) : (
<Button
style="ctaGray"
css={{
cursor: props.isValidating ? 'not-allowed' : 'pointer',
}}
onClick={props.loadMore}
disabled={props.isValidating}
>
{props.isValidating ? 'Loading' : 'More search results'}
</Button>
)
) : (
<StyledText style="caption"></StyledText>
)}

View File

@ -1,28 +1,24 @@
import { useEffect, useRef, useState } from 'react'
export const useFetchMore = (callback: () => void, delay = 500): void => {
export const useFetchMore = (fetchNextPage: () => void, delay = 500): void => {
const [first, setFirst] = useState(true)
const [lastScrollTop, setLastScrollTop] = useState(0)
const throttleTimeout = useRef<NodeJS.Timeout | undefined>(undefined)
useEffect(() => {
if (typeof window === 'undefined') {
return
}
const callbackInternal = (): void => {
const { scrollTop, scrollHeight, clientHeight } =
window.document.documentElement
const direction = scrollTop > lastScrollTop ? 'down' : 'up'
setLastScrollTop(scrollTop)
if (scrollTop + clientHeight >= scrollHeight - scrollHeight / 3) {
console.log(
'calling fetchMore: scrollTop + clientHeight >= scrollHeight - scrollHeight / 3',
scrollTop,
clientHeight,
scrollHeight,
scrollHeight / 3
)
callback()
if (
direction == 'down' &&
scrollTop + clientHeight >= scrollHeight - scrollHeight / 3
) {
fetchNextPage()
}
throttleTimeout.current = undefined
}
@ -42,5 +38,5 @@ export const useFetchMore = (callback: () => void, delay = 500): void => {
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [callback, delay, first, setFirst])
}, [fetchNextPage, delay, first, setFirst])
}

View File

@ -23,6 +23,7 @@ import {
GQL_SET_LINK_ARCHIVED,
GQL_UPDATE_LIBRARY_ITEM,
} from './gql'
import { useState } from 'react'
function gqlFetcher(
query: string,
@ -173,11 +174,8 @@ export const insertItemInCache = (
const keys = queryClient
.getQueryCache()
.findAll({ queryKey: ['libraryItems'] })
console.log('keys: ', keys)
keys.forEach((query) => {
queryClient.setQueryData(query.queryKey, (data: any) => {
console.log('data, data.pages', data)
if (!data) return data
if (data.pages.length > 0) {
const firstPage = data.pages[0] as LibraryItems
@ -202,43 +200,139 @@ export const insertItemInCache = (
},
]
data.pages[0] = firstPage
console.log('data: ', data)
return data
}
})
})
}
// const useOptimizedPageFetcher = (
// section: string,
// folder: string | undefined,
// { limit, searchQuery, includeCount }: LibraryItemsQueryInput,
// enabled = true
// ) => {
// const [pages, setPages] = useState([])
// const queryClient = useQueryClient()
// const fullQuery = folder
// ? (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
// : searchQuery ?? ''
// }
interface CachedPagesData {
pageParams: string[]
pages: LibraryItems[]
}
export function useGetLibraryItems(
section: string,
folder: string | undefined,
{ limit, searchQuery, includeCount }: LibraryItemsQueryInput,
enabled = true
) {
const queryClient = useQueryClient()
const INITIAL_INDEX = '0'
const fullQuery = folder
? (`in:${folder} use:folders ` + (searchQuery ?? '')).trim()
: searchQuery ?? ''
const queryKey = ['libraryItems', section, fullQuery]
return useInfiniteQuery({
queryKey: ['libraryItems', fullQuery],
queryFn: async ({ pageParam }) => {
// If no folder is specified cache this as `home`
queryKey,
queryFn: async ({ queryKey, pageParam, meta }) => {
console.log('pageParam and limit', Number(pageParam), limit)
const cached = queryClient.getQueryData(queryKey) as CachedPagesData
if (pageParam !== INITIAL_INDEX) {
// check in the query cache, if there is an item for this page
// in the query page, check if pageIndex - 1 was unchanged since
// the last query, this will determine if we should refetch this
// page and subsequent pages.
if (cached) {
const idx = cached.pageParams.indexOf(pageParam)
// First check if the previous page had detected a modification
// if it had we keep fetching until we find a
if (
idx > 0 &&
idx < cached.pages.length &&
cached.pages[idx - 1].pageInfo.wasUnchanged
) {
const cachedResult = cached.pages[idx]
console.log('found cached page result: ', cachedResult)
return {
edges: cachedResult.edges,
pageInfo: {
...cachedResult.pageInfo,
wasUnchanged: true,
},
}
}
}
}
const response = (await gqlFetcher(gqlSearchQuery(includeCount), {
after: pageParam,
first: limit,
query: fullQuery,
includeContent: false,
})) as LibraryItemsData
return response.search
let wasUnchanged = false
if (cached && cached.pageParams.indexOf(pageParam) > -1) {
const idx = cached.pageParams.indexOf(pageParam)
// // if there is a cache, check to see if the page is already in it
// // and mark whether or not the page has changed
try {
const cachedIds = cached.pages[idx].edges.map((m) => m.node.id)
const resultIds = response.search.edges.map((m) => m.node.id)
const compareFunc = (a: string[], b: string[]) =>
a.length === b.length &&
a.every((element, index) => element === b[index])
wasUnchanged = compareFunc(cachedIds, resultIds)
console.log('previous unchanged', wasUnchanged, cachedIds, resultIds)
} catch (err) {
console.log('error: ', err)
}
}
return {
edges: response.search.edges,
pageInfo: {
...response.search.pageInfo,
wasUnchanged,
lastUpdated: new Date(),
},
}
},
enabled,
initialPageParam: '0',
refetchOnMount: false,
refetchOnWindowFocus: false,
staleTime: 10 * 60 * 1000,
getNextPageParam: (lastPage: LibraryItems) => {
initialPageParam: INITIAL_INDEX,
getNextPageParam: (lastPage: LibraryItems, pages) => {
return lastPage.pageInfo.hasNextPage
? lastPage?.pageInfo?.endCursor
: undefined
},
select: (data) => {
const now = new Date()
// Filter pages based on the lastUpdated condition
const filteredPages = data.pages.slice(0, 5).concat(
data.pages.slice(5).filter((page, index) => {
if (page.pageInfo?.lastUpdated) {
const lastUpdatedDate = new Date(page.pageInfo.lastUpdated)
const diffMinutes =
(now.getTime() - lastUpdatedDate.getTime()) / (1000 * 60)
console.log(`page: ${index} age: ${diffMinutes}`)
return diffMinutes <= 10
}
return true
})
)
console.log('setting filteredPages: ', filteredPages)
return {
...data,
pages: filteredPages,
}
},
})
}
@ -531,7 +625,9 @@ export function useRefreshProcessingItems() {
attempt: number
itemIds: string[]
}) => {
const fullQuery = `in:all includes:${variables.itemIds.join(',')}`
const fullQuery = `in:all includes:${variables.itemIds
.slice(0, 5)
.join(',')}`
const result = (await gqlFetcher(gqlSearchQuery(), {
first: 10,
query: fullQuery,
@ -1029,6 +1125,10 @@ export type PageInfo = {
startCursor: string
endCursor: string
totalCount: number
// used internally for some cache handling
lastUpdated?: Date
wasUnchanged?: boolean
}
type SetLinkArchivedInput = {

View File

@ -114,8 +114,6 @@ const showToastWithAction = (
action: () => Promise<void>,
options?: ToastOptions
) => {
console.trace('show success: ', message)
return toast(
({ id }) => (
<FullWidthContainer alignment="center">

View File

@ -305,7 +305,7 @@ export default function Reader(): JSX.Element {
perform: () => {
const navReturn = window.localStorage.getItem('nav-return')
if (navReturn) {
router.push(navReturn)
router.push(navReturn, navReturn, { scroll: false })
return
}
const query = window.sessionStorage.getItem('q')

View File

@ -33,7 +33,7 @@ import React from 'react'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 48, // 48 hours
gcTime: 1000 * 60 * 60 * 4, // 4hrs
},
},
})

View File

@ -45,6 +45,7 @@ export default function Home(): JSX.Element {
// return <HomeContainer />
return (
<LibraryContainer
key={name}
folder={undefined}
filterFunc={(item) => {
return (
@ -59,6 +60,7 @@ export default function Home(): JSX.Element {
case 'library':
return (
<LibraryContainer
key={name}
folder="inbox"
filterFunc={(item) => {
return (
@ -73,6 +75,7 @@ export default function Home(): JSX.Element {
case 'subscriptions':
return (
<LibraryContainer
key={name}
folder="following"
filterFunc={(item) => {
return (
@ -87,6 +90,7 @@ export default function Home(): JSX.Element {
case 'search':
return (
<LibraryContainer
key={name}
folder={undefined}
filterFunc={(item) => {
console.log('item: ', item)
@ -98,6 +102,7 @@ export default function Home(): JSX.Element {
case 'archive':
return (
<LibraryContainer
key={name}
folder="archive"
filterFunc={(item) => {
return item.state == 'ARCHIVED'
@ -108,6 +113,7 @@ export default function Home(): JSX.Element {
case 'trash':
return (
<LibraryContainer
key={name}
folder="trash"
filterFunc={(item) => {
return item.state == 'DELETED'

View File

@ -92,7 +92,7 @@ export default function Account(): JSX.Element {
isUsernameValidationLoading,
])
const { data: itemsPages, isLoading } = useGetLibraryItems('all', {
const { data: itemsPages, isLoading } = useGetLibraryItems('search', 'all', {
limit: 0,
searchQuery: '',
sortDescending: false,

View File

@ -33,12 +33,16 @@ export default function BulkPerformer(): JSX.Element {
const [runningState, setRunningState] = useState<RunningState>('none')
const bulkAction = useBulkActions()
const { data: itemsPages, isLoading } = useGetLibraryItems(undefined, {
searchQuery: query,
limit: 1,
sortDescending: false,
includeCount: true,
})
const { data: itemsPages, isLoading } = useGetLibraryItems(
'search',
undefined,
{
searchQuery: query,
limit: 1,
sortDescending: false,
includeCount: true,
}
)
useEffect(() => {
setExpectedCount(itemsPages?.pages.find(() => true)?.pageInfo.totalCount)

View File

@ -29428,7 +29428,7 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -29454,15 +29454,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
@ -29617,7 +29608,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -29652,13 +29643,6 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@ -32358,7 +32342,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -32384,15 +32368,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"