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:
parent
5715b0e44a
commit
847d3d0f27
|
@ -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}
|
||||||
|
|
|
@ -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(() => {
|
loadMore()
|
||||||
if (isBottomVisible && hasMore) {
|
}
|
||||||
loadMore()
|
},
|
||||||
}
|
[loadMore]
|
||||||
}, [isBottomVisible, hasMore, 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'}>
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
24
web/components/visibility-observer.tsx
Normal file
24
web/components/visibility-observer.tsx
Normal 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>
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user