Fix efficiency problems with visibility checking code (#730)

* Fix problems with visibility checking code

* Tear out old contract tracking stuff per James

* Use `useEvent` in VisibilityObserver per James suggestion
This commit is contained in:
Marshall Polaris 2022-08-09 15:28:27 -07:00 committed by GitHub
parent 5715b0e44a
commit 847d3d0f27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 43 additions and 108 deletions

View File

@ -345,7 +345,6 @@ export function ContractSearch(props: {
<ContractsGrid <ContractsGrid
contracts={hitsByPage[0] === undefined ? undefined : contracts} contracts={hitsByPage[0] === undefined ? undefined : contracts}
loadMore={loadMore} loadMore={loadMore}
hasMore={true}
showTime={showTime} showTime={showTime}
onContractClick={onContractClick} onContractClick={onContractClick}
overrideGridClassName={overrideGridClassName} overrideGridClassName={overrideGridClassName}

View File

@ -5,10 +5,10 @@ import { SiteLink } from '../site-link'
import { ContractCard } from './contract-card' import { ContractCard } from './contract-card'
import { ShowTime } from './contract-details' import { ShowTime } from './contract-details'
import { ContractSearch } from '../contract-search' import { ContractSearch } from '../contract-search'
import { useIsVisible } from 'web/hooks/use-is-visible' import { useCallback } from 'react'
import { useEffect, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
import { LoadingIndicator } from '../loading-indicator' import { LoadingIndicator } from '../loading-indicator'
import { VisibilityObserver } from '../visibility-observer'
export type ContractHighlightOptions = { export type ContractHighlightOptions = {
contractIds?: string[] contractIds?: string[]
@ -17,8 +17,7 @@ export type ContractHighlightOptions = {
export function ContractsGrid(props: { export function ContractsGrid(props: {
contracts: Contract[] | undefined contracts: Contract[] | undefined
loadMore: () => void loadMore?: () => void
hasMore: boolean
showTime?: ShowTime showTime?: ShowTime
onContractClick?: (contract: Contract) => void onContractClick?: (contract: Contract) => void
overrideGridClassName?: string overrideGridClassName?: string
@ -31,7 +30,6 @@ export function ContractsGrid(props: {
const { const {
contracts, contracts,
showTime, showTime,
hasMore,
loadMore, loadMore,
onContractClick, onContractClick,
overrideGridClassName, overrideGridClassName,
@ -39,16 +37,15 @@ export function ContractsGrid(props: {
highlightOptions, highlightOptions,
} = props } = props
const { hideQuickBet, hideGroupLink } = cardHideOptions || {} const { hideQuickBet, hideGroupLink } = cardHideOptions || {}
const { contractIds, highlightClassName } = highlightOptions || {} const { contractIds, highlightClassName } = highlightOptions || {}
const [elem, setElem] = useState<HTMLElement | null>(null) const onVisibilityUpdated = useCallback(
const isBottomVisible = useIsVisible(elem) (visible) => {
if (visible && loadMore) {
useEffect(() => {
if (isBottomVisible && hasMore) {
loadMore() loadMore()
} }
}, [isBottomVisible, hasMore, loadMore]) },
[loadMore]
)
if (contracts === undefined) { if (contracts === undefined) {
return <LoadingIndicator /> return <LoadingIndicator />
@ -92,7 +89,10 @@ export function ContractsGrid(props: {
/> />
))} ))}
</ul> </ul>
<div ref={setElem} className="relative -top-96 h-1" /> <VisibilityObserver
onVisibilityUpdated={onVisibilityUpdated}
className="relative -top-96 h-1"
/>
</Col> </Col>
) )
} }

View File

@ -1,5 +1,5 @@
// From https://tailwindui.com/components/application-ui/lists/feeds // From https://tailwindui.com/components/application-ui/lists/feeds
import React, { useState } from 'react' import React from 'react'
import { import {
BanIcon, BanIcon,
CheckIcon, CheckIcon,
@ -22,7 +22,6 @@ import { UserLink } from '../user-page'
import BetRow from '../bet-row' import BetRow from '../bet-row'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { ActivityItem } from './activity-items' import { ActivityItem } from './activity-items'
import { useSaveSeenContract } from 'web/hooks/use-seen-contracts'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { trackClick } from 'web/lib/firebase/tracking' import { trackClick } from 'web/lib/firebase/tracking'
import { DAY_MS } from 'common/util/time' import { DAY_MS } from 'common/util/time'
@ -50,11 +49,8 @@ export function FeedItems(props: {
const { contract, items, className, betRowClassName, user } = props const { contract, items, className, betRowClassName, user } = props
const { outcomeType } = contract const { outcomeType } = contract
const [elem, setElem] = useState<HTMLElement | null>(null)
useSaveSeenContract(elem, contract)
return ( return (
<div className={clsx('flow-root', className)} ref={setElem}> <div className={clsx('flow-root', className)}>
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}> <div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
{items.map((item, activityItemIdx) => ( {items.map((item, activityItemIdx) => (
<div key={item.id} className={'relative pb-4'}> <div key={item.id} className={'relative pb-4'}>

View File

@ -59,11 +59,7 @@ export function LandingPagePanel(props: { hotContracts: Contract[] }) {
<SparklesIcon className="inline h-5 w-5" aria-hidden="true" /> <SparklesIcon className="inline h-5 w-5" aria-hidden="true" />
Trending markets Trending markets
</Row> </Row>
<ContractsGrid <ContractsGrid contracts={hotContracts?.slice(0, 10) || []} />
contracts={hotContracts?.slice(0, 10) || []}
loadMore={() => {}}
hasMore={false}
/>
</> </>
) )
} }

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'
import { useEvent } from '../hooks/use-event'
export function VisibilityObserver(props: {
className?: string
onVisibilityUpdated: (visible: boolean) => void
}) {
const { className } = props
const [elem, setElem] = useState<HTMLElement | null>(null)
const onVisibilityUpdated = useEvent(props.onVisibilityUpdated)
useEffect(() => {
const hasIOSupport = !!window.IntersectionObserver
if (!hasIOSupport || !elem) return
const observer = new IntersectionObserver(([entry]) => {
onVisibilityUpdated(entry.isIntersecting)
}, {})
observer.observe(elem)
return () => observer.disconnect()
}, [elem, onVisibilityUpdated])
return <div ref={setElem} className={className}></div>
}

View File

@ -1,28 +0,0 @@
import { useEffect, useState } from 'react'
export function useIsVisible(element: HTMLElement | null) {
return !!useIntersectionObserver(element)?.isIntersecting
}
function useIntersectionObserver(
elem: HTMLElement | null
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>()
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
setEntry(entry)
}
useEffect(() => {
const hasIOSupport = !!window.IntersectionObserver
if (!hasIOSupport || !elem) return
const observer = new IntersectionObserver(updateEntry, {})
observer.observe(elem)
return () => observer.disconnect()
}, [elem])
return entry
}

View File

@ -1,47 +0,0 @@
import { mapValues } from 'lodash'
import { useEffect, useState } from 'react'
import { Contract } from 'common/contract'
import { trackView } from 'web/lib/firebase/tracking'
import { useIsVisible } from './use-is-visible'
import { useUser } from './use-user'
export const useSeenContracts = () => {
const [seenContracts, setSeenContracts] = useState<{
[contractId: string]: number
}>({})
useEffect(() => {
setSeenContracts(getSeenContracts())
}, [])
return seenContracts
}
export const useSaveSeenContract = (
elem: HTMLElement | null,
contract: Contract
) => {
const isVisible = useIsVisible(elem)
const user = useUser()
useEffect(() => {
if (isVisible && user) {
const newSeenContracts = {
...getSeenContracts(),
[contract.id]: Date.now(),
}
localStorage.setItem(key, JSON.stringify(newSeenContracts))
trackView(user.id, contract.id)
}
}, [isVisible, user, contract])
}
const key = 'feed-seen-contracts'
const getSeenContracts = () => {
return mapValues(
JSON.parse(localStorage.getItem(key) ?? '{}'),
(time) => +time
)
}

View File

@ -110,12 +110,7 @@ export default function ContractSearchFirestore(props: {
<option value="close-date">Closing soon</option> <option value="close-date">Closing soon</option>
</select> </select>
</div> </div>
<ContractsGrid <ContractsGrid contracts={matches} showTime={showTime} />
contracts={matches}
loadMore={() => {}}
hasMore={false}
showTime={showTime}
/>
</div> </div>
) )
} }