From addc8834409651555329225e6f39cece7da6ff01 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 17 Mar 2022 02:29:08 -0500 Subject: [PATCH] Track seen contracts in feed. Order feed by seen time priority. --- web/components/feed/feed-items.tsx | 10 +++-- web/components/feed/find-active-contracts.ts | 34 +++++++++++++++- web/hooks/use-find-active-contracts.ts | 6 ++- web/hooks/use-is-visible.ts | 29 ++++++++++++++ web/hooks/use-seen-contracts.ts | 42 ++++++++++++++++++++ web/pages/fold/[...slugs]/index.tsx | 2 +- 6 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 web/hooks/use-is-visible.ts create mode 100644 web/hooks/use-seen-contracts.ts diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 10c6f310..befb7c06 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -1,5 +1,5 @@ // From https://tailwindui.com/components/application-ui/lists/feeds -import { Fragment, useState } from 'react' +import { Fragment, useRef, useState } from 'react' import * as _ from 'lodash' import { BanIcon, @@ -44,10 +44,9 @@ import { Answer } from '../../../common/answer' import { ActivityItem } from './activity-items' import { FreeResponse, FullContract } from '../../../common/contract' import { BuyButton } from '../yes-no-selector' -import { AnswerItem } from '../answers/answer-item' import { getDpmOutcomeProbability } from '../../../common/calculate-dpm' -import { BetPanel } from '../bet-panel' import { AnswerBetPanel } from '../answers/answer-bet-panel' +import { useSaveSeenContract } from '../../hooks/use-seen-contracts' export function FeedItems(props: { contract: Contract @@ -57,8 +56,11 @@ export function FeedItems(props: { const { contract, items, betRowClassName } = props const { outcomeType } = contract + const ref = useRef(null) + useSaveSeenContract(ref, contract) + return ( -
+
{items.map((item, activityItemIdx) => (
diff --git a/web/components/feed/find-active-contracts.ts b/web/components/feed/find-active-contracts.ts index 92e9a88a..7048f299 100644 --- a/web/components/feed/find-active-contracts.ts +++ b/web/components/feed/find-active-contracts.ts @@ -24,7 +24,8 @@ function lastActivityTime(contract: Contract) { export function findActiveContracts( allContracts: Contract[], recentComments: Comment[], - recentBets: Bet[] + recentBets: Bet[], + seenContracts: { [contractId: string]: number } ) { const idToActivityTime = new Map() function record(contractId: string, time: number) { @@ -64,5 +65,34 @@ export function findActiveContracts( activeContracts, (c) => -(idToActivityTime.get(c.id) ?? 0) ) - return activeContracts.slice(0, MAX_ACTIVE_CONTRACTS) + + const contractComments = _.groupBy( + recentComments, + (comment) => comment.contractId + ) + const contractMostRecentComment = _.mapValues( + contractComments, + (comments) => _.maxBy(comments, (c) => c.createdTime) as Comment + ) + + const prioritizedContracts = _.sortBy(activeContracts, (c) => { + const seenTime = seenContracts[c.id] + if (!seenTime) { + return 0 + } + + const lastCommentTime = contractMostRecentComment[c.id]?.createdTime + if (lastCommentTime && lastCommentTime > seenTime) { + return 1 + } + + const lastBetTime = contractMostRecentBet[c.id]?.createdTime + if (lastBetTime && lastBetTime > seenTime) { + return 2 + } + + return seenTime + }) + + return prioritizedContracts.slice(0, MAX_ACTIVE_CONTRACTS) } diff --git a/web/hooks/use-find-active-contracts.ts b/web/hooks/use-find-active-contracts.ts index dd3068ed..b177f93e 100644 --- a/web/hooks/use-find-active-contracts.ts +++ b/web/hooks/use-find-active-contracts.ts @@ -11,6 +11,7 @@ import { Contract, getActiveContracts } from '../lib/firebase/contracts' import { listAllFolds } from '../lib/firebase/folds' import { useInactiveContracts } from './use-contracts' import { useFollowedFolds } from './use-fold' +import { useSeenContracts } from './use-seen-contracts' import { useUserBetContracts } from './use-user-bets' // used in static props @@ -95,10 +96,13 @@ export const useFindActiveContracts = (props: { }) => { const { contracts, recentBets, recentComments } = props + const seenContracts = useSeenContracts() + const activeContracts = findActiveContracts( contracts, recentComments, - recentBets + recentBets, + seenContracts ) const betsByContract = _.groupBy(recentBets, (bet) => bet.contractId) diff --git a/web/hooks/use-is-visible.ts b/web/hooks/use-is-visible.ts new file mode 100644 index 00000000..baf26afa --- /dev/null +++ b/web/hooks/use-is-visible.ts @@ -0,0 +1,29 @@ +import { RefObject, useEffect, useState } from 'react' + +export function useIsVisible(elementRef: RefObject) { + return !!useIntersectionObserver(elementRef)?.isIntersecting +} + +function useIntersectionObserver( + elementRef: RefObject +): IntersectionObserverEntry | undefined { + const [entry, setEntry] = useState() + + const updateEntry = ([entry]: IntersectionObserverEntry[]): void => { + setEntry(entry) + } + + useEffect(() => { + const node = elementRef?.current + const hasIOSupport = !!window.IntersectionObserver + + if (!hasIOSupport || !node) return + + const observer = new IntersectionObserver(updateEntry, {}) + observer.observe(node) + + return () => observer.disconnect() + }, [elementRef]) + + return entry +} diff --git a/web/hooks/use-seen-contracts.ts b/web/hooks/use-seen-contracts.ts new file mode 100644 index 00000000..0cda7a67 --- /dev/null +++ b/web/hooks/use-seen-contracts.ts @@ -0,0 +1,42 @@ +import _ from 'lodash' +import { useEffect, RefObject, useState } from 'react' +import { Contract } from '../../common/contract' +import { useIsVisible } from './use-is-visible' + +export const useSeenContracts = () => { + const [seenContracts, setSeenContracts] = useState<{ + [contractId: string]: number + }>({}) + + useEffect(() => { + setSeenContracts(getSeenContracts()) + }, []) + + return seenContracts +} + +export const useSaveSeenContract = ( + ref: RefObject, + contract: Contract +) => { + const isVisible = useIsVisible(ref) + + useEffect(() => { + if (isVisible) { + const newSeenContracts = { + ...getSeenContracts(), + [contract.id]: Date.now(), + } + localStorage.setItem(key, JSON.stringify(newSeenContracts)) + } + }, [isVisible, contract]) +} + +const key = 'feed-seen-contracts' + +const getSeenContracts = () => { + return _.mapValues( + JSON.parse(localStorage.getItem(key) ?? '{}'), + (time) => +time + ) +} diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index 55e2de4c..fb39b326 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -55,7 +55,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { contracts.map((contract) => listAllBets(contract.id)) ) - let activeContracts = findActiveContracts(contracts, [], _.flatten(bets)) + let activeContracts = findActiveContracts(contracts, [], _.flatten(bets), {}) const [resolved, unresolved] = _.partition( activeContracts, ({ isResolved }) => isResolved