Hook up client to use getFeed
This commit is contained in:
parent
db42761143
commit
50e77d5899
|
@ -8,31 +8,27 @@ import { useUser } from '../../hooks/use-user'
|
||||||
import { ContractActivity } from './contract-activity'
|
import { ContractActivity } from './contract-activity'
|
||||||
|
|
||||||
export function ActivityFeed(props: {
|
export function ActivityFeed(props: {
|
||||||
contracts: Contract[]
|
feed: {
|
||||||
recentBets: Bet[]
|
contract: Contract
|
||||||
recentComments: Comment[]
|
recentBets: Bet[]
|
||||||
|
recentComments: Comment[]
|
||||||
|
}[]
|
||||||
mode: 'only-recent' | 'abbreviated' | 'all'
|
mode: 'only-recent' | 'abbreviated' | 'all'
|
||||||
getContractPath?: (contract: Contract) => string
|
getContractPath?: (contract: Contract) => string
|
||||||
}) {
|
}) {
|
||||||
const { contracts, recentBets, recentComments, mode, getContractPath } = props
|
const { feed, mode, getContractPath } = props
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const groupedBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
|
||||||
const groupedComments = _.groupBy(
|
|
||||||
recentComments,
|
|
||||||
(comment) => comment.contractId
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeedContainer
|
<FeedContainer
|
||||||
contracts={contracts}
|
feed={feed}
|
||||||
renderContract={(contract) => (
|
renderItem={({ contract, recentBets, recentComments }) => (
|
||||||
<ContractActivity
|
<ContractActivity
|
||||||
user={user}
|
user={user}
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={groupedBets[contract.id] ?? []}
|
bets={recentBets}
|
||||||
comments={groupedComments[contract.id] ?? []}
|
comments={recentComments}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
contractPath={getContractPath ? getContractPath(contract) : undefined}
|
contractPath={getContractPath ? getContractPath(contract) : undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -42,18 +38,26 @@ export function ActivityFeed(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeedContainer(props: {
|
function FeedContainer(props: {
|
||||||
contracts: Contract[]
|
feed: {
|
||||||
renderContract: (contract: Contract) => any
|
contract: Contract
|
||||||
|
recentBets: Bet[]
|
||||||
|
recentComments: Comment[]
|
||||||
|
}[]
|
||||||
|
renderItem: (item: {
|
||||||
|
contract: Contract
|
||||||
|
recentBets: Bet[]
|
||||||
|
recentComments: Comment[]
|
||||||
|
}) => any
|
||||||
}) {
|
}) {
|
||||||
const { contracts, renderContract } = props
|
const { feed, renderItem } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className="items-center">
|
<Col className="items-center">
|
||||||
<Col className="w-full max-w-3xl">
|
<Col className="w-full max-w-3xl">
|
||||||
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
<Col className="w-full divide-y divide-gray-300 self-center bg-white">
|
||||||
{contracts.map((contract) => (
|
{feed.map((item) => (
|
||||||
<div key={contract.id} className="py-6 px-2 sm:px-4">
|
<div key={item.contract.id} className="py-6 px-2 sm:px-4">
|
||||||
{renderContract(contract)}
|
{renderItem(item)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -1,205 +1,32 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { Comment } from '../../common/comment'
|
import { Comment } from '../../common/comment'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
|
||||||
import { logInterpolation } from '../../common/util/math'
|
|
||||||
import { getRecommendedContracts } from '../../common/recommended-contracts'
|
|
||||||
import { useSeenContracts } from './use-seen-contracts'
|
|
||||||
import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets'
|
|
||||||
import { DAY_MS } from '../../common/util/time'
|
|
||||||
import {
|
|
||||||
getProbability,
|
|
||||||
getOutcomeProbability,
|
|
||||||
getTopAnswer,
|
|
||||||
} from '../../common/calculate'
|
|
||||||
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
||||||
import { trackLatency } from '../lib/firebase/tracking'
|
import { trackLatency } from '../lib/firebase/tracking'
|
||||||
|
import { getFeed } from '../lib/firebase/api-call'
|
||||||
|
|
||||||
const MAX_FEED_CONTRACTS = 75
|
export const useAlgoFeed = () => {
|
||||||
|
const [feed, setFeed] = useState<
|
||||||
export const useAlgoFeed = (
|
{
|
||||||
user: User | null | undefined,
|
contract: Contract
|
||||||
contracts: Contract[] | undefined,
|
recentBets: Bet[]
|
||||||
recentBets: Bet[] | undefined,
|
recentComments: Comment[]
|
||||||
recentComments: Comment[] | undefined
|
}[]
|
||||||
) => {
|
>()
|
||||||
const initialContracts = useMemo(() => contracts, [!!contracts])
|
|
||||||
const initialBets = useMemo(() => recentBets, [!!recentBets])
|
|
||||||
const initialComments = useMemo(() => recentComments, [!!recentComments])
|
|
||||||
|
|
||||||
const yourBetContractIds = useGetUserBetContractIds(user?.id)
|
|
||||||
// Update user bet contracts in local storage.
|
|
||||||
useUserBetContracts(user?.id)
|
|
||||||
|
|
||||||
const seenContracts = useSeenContracts()
|
|
||||||
|
|
||||||
const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
|
|
||||||
|
|
||||||
const getTime = useTimeSinceFirstRender()
|
const getTime = useTimeSinceFirstRender()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
getFeed().then(({ data }) => {
|
||||||
initialContracts &&
|
console.log('got data', data)
|
||||||
initialBets &&
|
setFeed(data.feed)
|
||||||
initialComments &&
|
|
||||||
yourBetContractIds
|
|
||||||
) {
|
|
||||||
const eligibleContracts = initialContracts.filter(
|
|
||||||
(c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
|
|
||||||
)
|
|
||||||
const contracts = getAlgoFeed(
|
|
||||||
eligibleContracts,
|
|
||||||
initialBets,
|
|
||||||
initialComments,
|
|
||||||
yourBetContractIds,
|
|
||||||
seenContracts
|
|
||||||
)
|
|
||||||
setAlgoFeed(contracts)
|
|
||||||
trackLatency('feed', getTime())
|
trackLatency('feed', getTime())
|
||||||
}
|
console.log('feed load time', getTime())
|
||||||
}, [
|
|
||||||
initialBets,
|
|
||||||
initialComments,
|
|
||||||
initialContracts,
|
|
||||||
seenContracts,
|
|
||||||
yourBetContractIds,
|
|
||||||
getTime,
|
|
||||||
])
|
|
||||||
|
|
||||||
return algoFeed
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAlgoFeed = (
|
|
||||||
contracts: Contract[],
|
|
||||||
recentBets: Bet[],
|
|
||||||
recentComments: Comment[],
|
|
||||||
yourBetContractIds: string[],
|
|
||||||
seenContracts: { [contractId: string]: number }
|
|
||||||
) => {
|
|
||||||
const contractsById = _.keyBy(contracts, (c) => c.id)
|
|
||||||
|
|
||||||
const recommended = getRecommendedContracts(contractsById, yourBetContractIds)
|
|
||||||
const confidence = logInterpolation(0, 100, yourBetContractIds.length)
|
|
||||||
const recommendedScores = _.fromPairs(
|
|
||||||
recommended.map((c, index) => {
|
|
||||||
const score = 1 - index / recommended.length
|
|
||||||
const withConfidence = score * confidence + (1 - confidence)
|
|
||||||
return [c.id, withConfidence] as [string, number]
|
|
||||||
})
|
})
|
||||||
)
|
}, [getTime])
|
||||||
|
|
||||||
const seenScores = _.fromPairs(
|
return feed
|
||||||
contracts.map(
|
|
||||||
(c) => [c.id, getSeenContractsScore(c, seenContracts)] as [string, number]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const activityScores = getContractsActivityScores(
|
|
||||||
contracts,
|
|
||||||
recentComments,
|
|
||||||
recentBets,
|
|
||||||
seenContracts
|
|
||||||
)
|
|
||||||
|
|
||||||
const combinedScores = contracts.map((contract) => {
|
|
||||||
const score =
|
|
||||||
(recommendedScores[contract.id] ?? 0) *
|
|
||||||
(seenScores[contract.id] ?? 0) *
|
|
||||||
(activityScores[contract.id] ?? 0)
|
|
||||||
return { contract, score }
|
|
||||||
})
|
|
||||||
|
|
||||||
const sorted = _.sortBy(combinedScores, (c) => -c.score)
|
|
||||||
return sorted.map((c) => c.contract).slice(0, MAX_FEED_CONTRACTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContractsActivityScores(
|
|
||||||
contracts: Contract[],
|
|
||||||
recentComments: Comment[],
|
|
||||||
recentBets: Bet[],
|
|
||||||
seenContracts: { [contractId: string]: number }
|
|
||||||
) {
|
|
||||||
const contractBets = _.groupBy(recentBets, (bet) => bet.contractId)
|
|
||||||
const contractMostRecentBet = _.mapValues(
|
|
||||||
contractBets,
|
|
||||||
(bets) => _.maxBy(bets, (bet) => bet.createdTime) as Bet
|
|
||||||
)
|
|
||||||
|
|
||||||
const contractComments = _.groupBy(
|
|
||||||
recentComments,
|
|
||||||
(comment) => comment.contractId
|
|
||||||
)
|
|
||||||
const contractMostRecentComment = _.mapValues(
|
|
||||||
contractComments,
|
|
||||||
(comments) => _.maxBy(comments, (c) => c.createdTime) as Comment
|
|
||||||
)
|
|
||||||
|
|
||||||
const scoredContracts = contracts.map((contract) => {
|
|
||||||
const { outcomeType } = contract
|
|
||||||
|
|
||||||
const seenTime = seenContracts[contract.id]
|
|
||||||
const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
|
|
||||||
const hasNewComments =
|
|
||||||
!seenTime || (lastCommentTime && lastCommentTime > seenTime)
|
|
||||||
const newCommentScore = hasNewComments ? 1 : 0.5
|
|
||||||
|
|
||||||
const commentCount = contractComments[contract.id]?.length ?? 0
|
|
||||||
const betCount = contractBets[contract.id]?.length ?? 0
|
|
||||||
const activtyCount = betCount + commentCount * 5
|
|
||||||
const activityCountScore =
|
|
||||||
0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
|
|
||||||
|
|
||||||
const lastBetTime =
|
|
||||||
contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
|
|
||||||
const timeSinceLastBet = Date.now() - lastBetTime
|
|
||||||
const daysAgo = timeSinceLastBet / DAY_MS
|
|
||||||
const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo)
|
|
||||||
|
|
||||||
let prob = 0.5
|
|
||||||
if (outcomeType === 'BINARY') {
|
|
||||||
prob = getProbability(contract)
|
|
||||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
||||||
const topAnswer = getTopAnswer(contract)
|
|
||||||
if (topAnswer)
|
|
||||||
prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id))
|
|
||||||
}
|
|
||||||
const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25
|
|
||||||
const probScore = 0.5 + frac * 0.5
|
|
||||||
|
|
||||||
const score =
|
|
||||||
newCommentScore * activityCountScore * timeAgoScore * probScore
|
|
||||||
|
|
||||||
// Map score to [0.5, 1] since no recent activty is not a deal breaker.
|
|
||||||
const mappedScore = 0.5 + score / 2
|
|
||||||
const newMappedScore = 0.75 + score / 4
|
|
||||||
|
|
||||||
const isNew = Date.now() < contract.createdTime + DAY_MS
|
|
||||||
const activityScore = isNew ? newMappedScore : mappedScore
|
|
||||||
|
|
||||||
return [contract.id, activityScore] as [string, number]
|
|
||||||
})
|
|
||||||
|
|
||||||
return _.fromPairs(scoredContracts)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSeenContractsScore(
|
|
||||||
contract: Contract,
|
|
||||||
seenContracts: { [contractId: string]: number }
|
|
||||||
) {
|
|
||||||
const lastSeen = seenContracts[contract.id]
|
|
||||||
if (lastSeen === undefined) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const daysAgo = (Date.now() - lastSeen) / DAY_MS
|
|
||||||
|
|
||||||
if (daysAgo < 0.5) {
|
|
||||||
const frac = logInterpolation(0, 0.5, daysAgo)
|
|
||||||
return 0.5 * frac
|
|
||||||
}
|
|
||||||
|
|
||||||
const frac = logInterpolation(0.5, 14, daysAgo)
|
|
||||||
return 0.5 + 0.5 * frac
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { httpsCallable } from 'firebase/functions'
|
import { httpsCallable } from 'firebase/functions'
|
||||||
|
import { Bet } from '../../../common/bet'
|
||||||
|
import { Comment } from '../../../common/comment'
|
||||||
|
import { Contract } from '../../../common/contract'
|
||||||
import { Fold } from '../../../common/fold'
|
import { Fold } from '../../../common/fold'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
import { randomString } from '../../../common/util/random'
|
import { randomString } from '../../../common/util/random'
|
||||||
|
@ -71,3 +74,15 @@ export const addLiquidity = (data: { amount: number; contractId: string }) => {
|
||||||
.then((r) => r.data as { status: string })
|
.then((r) => r.data as { status: string })
|
||||||
.catch((e) => ({ status: 'error', message: e.message }))
|
.catch((e) => ({ status: 'error', message: e.message }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getFeed = cloudFunction<
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
status: 'error' | 'success'
|
||||||
|
feed: {
|
||||||
|
contract: Contract
|
||||||
|
recentBets: Bet[]
|
||||||
|
recentComments: Comment[]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
>('getFeed')
|
||||||
|
|
|
@ -9,30 +9,19 @@ import { Spacer } from '../components/layout/spacer'
|
||||||
import { Col } from '../components/layout/col'
|
import { Col } from '../components/layout/col'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { LoadingIndicator } from '../components/loading-indicator'
|
import { LoadingIndicator } from '../components/loading-indicator'
|
||||||
import { useRecentBets } from '../hooks/use-bets'
|
|
||||||
import { useActiveContracts } from '../hooks/use-contracts'
|
|
||||||
import { useRecentComments } from '../hooks/use-comments'
|
|
||||||
import { useAlgoFeed } from '../hooks/use-algo-feed'
|
import { useAlgoFeed } from '../hooks/use-algo-feed'
|
||||||
import { ContractPageContent } from './[username]/[contractSlug]'
|
import { ContractPageContent } from './[username]/[contractSlug]'
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
const contracts = useActiveContracts()
|
const feed = useAlgoFeed()
|
||||||
const contractsDict = _.keyBy(contracts, 'id')
|
|
||||||
|
|
||||||
const recentBets = useRecentBets()
|
|
||||||
const recentComments = useRecentComments()
|
|
||||||
|
|
||||||
const feedContracts = useAlgoFeed(user, contracts, recentBets, recentComments)
|
|
||||||
|
|
||||||
const updatedContracts = feedContracts.map(
|
|
||||||
(contract) => contractsDict[contract.id] ?? contract
|
|
||||||
)
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { u: username, s: slug } = router.query
|
const { u: username, s: slug } = router.query
|
||||||
const contract = feedContracts.find((c) => c.slug === slug)
|
const contract = feed?.find(
|
||||||
|
({ contract }) => contract.slug === slug
|
||||||
|
)?.contract
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the page initially loads with query params, redirect to the contract page.
|
// If the page initially loads with query params, redirect to the contract page.
|
||||||
|
@ -54,11 +43,9 @@ const Home = () => {
|
||||||
<Col className="w-full max-w-[700px]">
|
<Col className="w-full max-w-[700px]">
|
||||||
<FeedCreate user={user ?? undefined} />
|
<FeedCreate user={user ?? undefined} />
|
||||||
<Spacer h={10} />
|
<Spacer h={10} />
|
||||||
{contracts && recentBets && recentComments ? (
|
{feed ? (
|
||||||
<ActivityFeed
|
<ActivityFeed
|
||||||
contracts={updatedContracts}
|
feed={feed}
|
||||||
recentBets={recentBets}
|
|
||||||
recentComments={recentComments}
|
|
||||||
mode="only-recent"
|
mode="only-recent"
|
||||||
getContractPath={(c) =>
|
getContractPath={(c) =>
|
||||||
`home?u=${c.creatorUsername}&s=${c.slug}`
|
`home?u=${c.creatorUsername}&s=${c.slug}`
|
||||||
|
|
Loading…
Reference in New Issue
Block a user