Precalculate and store current positions for users who make comments (#878)
This commit is contained in:
parent
e37b805b49
commit
58dcbaaf6e
|
@ -1,4 +1,4 @@
|
|||
import { maxBy, sortBy, sum, sumBy } from 'lodash'
|
||||
import { maxBy, partition, sortBy, sum, sumBy } from 'lodash'
|
||||
import { Bet, LimitBet } from './bet'
|
||||
import {
|
||||
calculateCpmmSale,
|
||||
|
@ -255,3 +255,43 @@ export function getTopAnswer(
|
|||
)
|
||||
return top?.answer
|
||||
}
|
||||
|
||||
export function getLargestPosition(contract: Contract, userBets: Bet[]) {
|
||||
let yesFloorShares = 0,
|
||||
yesShares = 0,
|
||||
noShares = 0,
|
||||
noFloorShares = 0
|
||||
|
||||
if (userBets.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
const answerCounts: { [outcome: string]: number } = {}
|
||||
for (const bet of userBets) {
|
||||
if (bet.outcome) {
|
||||
if (!answerCounts[bet.outcome]) {
|
||||
answerCounts[bet.outcome] = bet.amount
|
||||
} else {
|
||||
answerCounts[bet.outcome] += bet.amount
|
||||
}
|
||||
}
|
||||
}
|
||||
const majorityAnswer =
|
||||
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
|
||||
return {
|
||||
prob: undefined,
|
||||
shares: answerCounts[majorityAnswer] || 0,
|
||||
outcome: majorityAnswer,
|
||||
}
|
||||
}
|
||||
|
||||
const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES')
|
||||
yesShares = sumBy(yesBets, (bet) => bet.shares)
|
||||
noShares = sumBy(noBets, (bet) => bet.shares)
|
||||
yesFloorShares = Math.floor(yesShares)
|
||||
noFloorShares = Math.floor(noShares)
|
||||
|
||||
const shares = yesFloorShares || noFloorShares
|
||||
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
|
||||
return { shares, outcome }
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ export type OnContract = {
|
|||
// denormalized from bet
|
||||
betAmount?: number
|
||||
betOutcome?: string
|
||||
|
||||
// denormalized based on betting history
|
||||
commenterPositionProb?: number // binary only
|
||||
commenterPositionShares?: number
|
||||
commenterPositionOutcome?: string
|
||||
}
|
||||
|
||||
export type OnGroup = {
|
||||
|
|
|
@ -5,6 +5,8 @@ import { getContract, getUser, getValues } from './utils'
|
|||
import { ContractComment } from '../../common/comment'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getLargestPosition } from '../../common/calculate'
|
||||
import { maxBy } from 'lodash'
|
||||
import {
|
||||
createCommentOrAnswerOrUpdatedContractNotification,
|
||||
replied_users_info,
|
||||
|
@ -45,6 +47,32 @@ export const onCreateCommentOnContract = functions
|
|||
.doc(contract.id)
|
||||
.update({ lastCommentTime, lastUpdatedTime: Date.now() })
|
||||
|
||||
const previousBetsQuery = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.collection('bets')
|
||||
.where('createdTime', '<', comment.createdTime)
|
||||
.get()
|
||||
const previousBets = previousBetsQuery.docs.map((d) => d.data() as Bet)
|
||||
const position = getLargestPosition(
|
||||
contract,
|
||||
previousBets.filter((b) => b.userId === comment.userId && !b.isAnte)
|
||||
)
|
||||
if (position) {
|
||||
const fields: { [k: string]: unknown } = {
|
||||
commenterPositionShares: position.shares,
|
||||
commenterPositionOutcome: position.outcome,
|
||||
}
|
||||
const previousProb =
|
||||
contract.outcomeType === 'BINARY'
|
||||
? maxBy(previousBets, (bet) => bet.createdTime)?.probAfter
|
||||
: undefined
|
||||
if (previousProb != null) {
|
||||
fields.commenterPositionProb = previousProb
|
||||
}
|
||||
await change.ref.update(fields)
|
||||
}
|
||||
|
||||
let bet: Bet | undefined
|
||||
let answer: Answer | undefined
|
||||
if (comment.answerOutcome) {
|
||||
|
|
92
functions/src/scripts/backfill-comment-position-data.ts
Normal file
92
functions/src/scripts/backfill-comment-position-data.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Filling in historical bet positions on comments.
|
||||
|
||||
// Warning: This just recalculates all of them, rather than trying to
|
||||
// figure out which ones are out of date, since I'm using it to fill them
|
||||
// in once in the first place.
|
||||
|
||||
import { maxBy } from 'lodash'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { filterDefined } from '../../../common/util/array'
|
||||
import { Bet } from '../../../common/bet'
|
||||
import { Comment } from '../../../common/comment'
|
||||
import { Contract } from '../../../common/contract'
|
||||
import { getLargestPosition } from '../../../common/calculate'
|
||||
import { initAdmin } from './script-init'
|
||||
import { DocumentSnapshot } from 'firebase-admin/firestore'
|
||||
import { log, writeAsync } from '../utils'
|
||||
|
||||
initAdmin()
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function getContractsById() {
|
||||
const contracts = await firestore.collection('contracts').get()
|
||||
const results = Object.fromEntries(
|
||||
contracts.docs.map((doc) => [doc.id, doc.data() as Contract])
|
||||
)
|
||||
log(`Found ${contracts.size} contracts.`)
|
||||
return results
|
||||
}
|
||||
|
||||
async function getCommentsByContractId() {
|
||||
const comments = await firestore
|
||||
.collectionGroup('comments')
|
||||
.where('contractId', '!=', null)
|
||||
.get()
|
||||
const results = new Map<string, DocumentSnapshot[]>()
|
||||
comments.forEach((doc) => {
|
||||
const contractId = doc.get('contractId')
|
||||
const contractComments = results.get(contractId) || []
|
||||
contractComments.push(doc)
|
||||
results.set(contractId, contractComments)
|
||||
})
|
||||
log(`Found ${comments.size} comments on ${results.size} contracts.`)
|
||||
return results
|
||||
}
|
||||
|
||||
// not in a transaction for speed -- may need to be run more than once
|
||||
async function denormalize() {
|
||||
const contractsById = await getContractsById()
|
||||
const commentsByContractId = await getCommentsByContractId()
|
||||
for (const [contractId, comments] of commentsByContractId.entries()) {
|
||||
const betsQuery = await firestore
|
||||
.collection('contracts')
|
||||
.doc(contractId)
|
||||
.collection('bets')
|
||||
.get()
|
||||
log(`Loaded ${betsQuery.size} bets for contract ${contractId}.`)
|
||||
const bets = betsQuery.docs.map((d) => d.data() as Bet)
|
||||
const updates = comments.map((doc) => {
|
||||
const comment = doc.data() as Comment
|
||||
const contract = contractsById[contractId]
|
||||
const previousBets = bets.filter(
|
||||
(b) => b.createdTime < comment.createdTime
|
||||
)
|
||||
const position = getLargestPosition(
|
||||
contract,
|
||||
previousBets.filter((b) => b.userId === comment.userId && !b.isAnte)
|
||||
)
|
||||
if (position) {
|
||||
const fields: { [k: string]: unknown } = {
|
||||
commenterPositionShares: position.shares,
|
||||
commenterPositionOutcome: position.outcome,
|
||||
}
|
||||
const previousProb =
|
||||
contract.outcomeType === 'BINARY'
|
||||
? maxBy(previousBets, (bet) => bet.createdTime)?.probAfter
|
||||
: undefined
|
||||
if (previousProb != null) {
|
||||
fields.commenterPositionProb = previousProb
|
||||
}
|
||||
return { doc: doc.ref, fields }
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
log(`Updating ${updates.length} comments.`)
|
||||
await writeAsync(firestore, filterDefined(updates))
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
denormalize().catch((e) => console.error(e))
|
||||
}
|
|
@ -106,7 +106,6 @@ export function ContractTopTrades(props: {
|
|||
contract={contract}
|
||||
comment={commentsById[topCommentId]}
|
||||
tips={tips[topCommentId]}
|
||||
betsBySameUser={[betsById[topCommentId]]}
|
||||
/>
|
||||
</div>
|
||||
<Spacer h={16} />
|
||||
|
|
|
@ -85,7 +85,9 @@ export function ContractTabs(props: {
|
|||
<div className={'mb-4 w-full border-b border-gray-200'} />
|
||||
<ContractCommentsActivity
|
||||
contract={contract}
|
||||
bets={generalBets}
|
||||
betsByCurrentUser={
|
||||
user ? generalBets.filter((b) => b.userId === user.id) : []
|
||||
}
|
||||
comments={generalComments}
|
||||
tips={tips}
|
||||
user={user}
|
||||
|
@ -95,7 +97,9 @@ export function ContractTabs(props: {
|
|||
) : (
|
||||
<ContractCommentsActivity
|
||||
contract={contract}
|
||||
bets={visibleBets}
|
||||
betsByCurrentUser={
|
||||
user ? visibleBets.filter((b) => b.userId === user.id) : []
|
||||
}
|
||||
comments={comments}
|
||||
tips={tips}
|
||||
user={user}
|
||||
|
|
|
@ -73,13 +73,12 @@ export function ContractBetsActivity(props: {
|
|||
|
||||
export function ContractCommentsActivity(props: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
betsByCurrentUser: Bet[]
|
||||
comments: ContractComment[]
|
||||
tips: CommentTipMap
|
||||
user: User | null | undefined
|
||||
}) {
|
||||
const { bets, contract, comments, user, tips } = props
|
||||
const betsByUserId = groupBy(bets, (bet) => bet.userId)
|
||||
const { betsByCurrentUser, contract, comments, user, tips } = props
|
||||
const commentsByUserId = groupBy(comments, (c) => c.userId)
|
||||
const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_')
|
||||
const topLevelComments = sortBy(
|
||||
|
@ -92,7 +91,7 @@ export function ContractCommentsActivity(props: {
|
|||
<ContractCommentInput
|
||||
className="mb-5"
|
||||
contract={contract}
|
||||
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
||||
betsByCurrentUser={betsByCurrentUser}
|
||||
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
|
||||
/>
|
||||
{topLevelComments.map((parent) => (
|
||||
|
@ -106,8 +105,7 @@ export function ContractCommentsActivity(props: {
|
|||
(c) => c.createdTime
|
||||
)}
|
||||
tips={tips}
|
||||
bets={bets}
|
||||
betsByUserId={betsByUserId}
|
||||
betsByCurrentUser={betsByCurrentUser}
|
||||
commentsByUserId={commentsByUserId}
|
||||
/>
|
||||
))}
|
||||
|
@ -136,7 +134,9 @@ export function FreeResponseContractCommentsActivity(props: {
|
|||
})
|
||||
.filter((answer) => answer != null)
|
||||
|
||||
const betsByUserId = groupBy(bets, (bet) => bet.userId)
|
||||
const betsByCurrentUser = user
|
||||
? bets.filter((bet) => bet.userId === user.id)
|
||||
: []
|
||||
const commentsByUserId = groupBy(comments, (c) => c.userId)
|
||||
const commentsByOutcome = groupBy(comments, (c) => c.answerOutcome ?? '_')
|
||||
|
||||
|
@ -157,7 +157,7 @@ export function FreeResponseContractCommentsActivity(props: {
|
|||
(c) => c.createdTime
|
||||
)}
|
||||
tips={tips}
|
||||
betsByUserId={betsByUserId}
|
||||
betsByCurrentUser={betsByCurrentUser}
|
||||
commentsByUserId={commentsByUserId}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@ export function FeedAnswerCommentGroup(props: {
|
|||
answer: Answer
|
||||
answerComments: ContractComment[]
|
||||
tips: CommentTipMap
|
||||
betsByUserId: Dictionary<Bet[]>
|
||||
betsByCurrentUser: Bet[]
|
||||
commentsByUserId: Dictionary<ContractComment[]>
|
||||
}) {
|
||||
const {
|
||||
|
@ -35,7 +35,7 @@ export function FeedAnswerCommentGroup(props: {
|
|||
contract,
|
||||
answerComments,
|
||||
tips,
|
||||
betsByUserId,
|
||||
betsByCurrentUser,
|
||||
commentsByUserId,
|
||||
user,
|
||||
} = props
|
||||
|
@ -48,7 +48,6 @@ export function FeedAnswerCommentGroup(props: {
|
|||
const router = useRouter()
|
||||
|
||||
const answerElementId = `answer-${answer.id}`
|
||||
const betsByCurrentUser = (user && betsByUserId[user.id]) ?? []
|
||||
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
||||
const isFreeResponseContractPage = !!commentsByCurrentUser
|
||||
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
||||
|
@ -166,7 +165,6 @@ export function FeedAnswerCommentGroup(props: {
|
|||
contract={contract}
|
||||
comment={comment}
|
||||
tips={tips[comment.id]}
|
||||
betsBySameUser={betsByUserId[comment.userId] ?? []}
|
||||
onReplyClick={scrollAndOpenReplyInput}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ContractComment } from 'common/comment'
|
|||
import { User } from 'common/user'
|
||||
import { Contract } from 'common/contract'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { minBy, maxBy, partition, sumBy, Dictionary } from 'lodash'
|
||||
import { Dictionary } from 'lodash'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { useRouter } from 'next/router'
|
||||
|
@ -29,8 +29,7 @@ export function FeedCommentThread(props: {
|
|||
threadComments: ContractComment[]
|
||||
tips: CommentTipMap
|
||||
parentComment: ContractComment
|
||||
bets: Bet[]
|
||||
betsByUserId: Dictionary<Bet[]>
|
||||
betsByCurrentUser: Bet[]
|
||||
commentsByUserId: Dictionary<ContractComment[]>
|
||||
}) {
|
||||
const {
|
||||
|
@ -38,8 +37,7 @@ export function FeedCommentThread(props: {
|
|||
contract,
|
||||
threadComments,
|
||||
commentsByUserId,
|
||||
bets,
|
||||
betsByUserId,
|
||||
betsByCurrentUser,
|
||||
tips,
|
||||
parentComment,
|
||||
} = props
|
||||
|
@ -64,17 +62,7 @@ export function FeedCommentThread(props: {
|
|||
contract={contract}
|
||||
comment={comment}
|
||||
tips={tips[comment.id]}
|
||||
betsBySameUser={betsByUserId[comment.userId] ?? []}
|
||||
onReplyClick={scrollAndOpenReplyInput}
|
||||
probAtCreatedTime={
|
||||
contract.outcomeType === 'BINARY'
|
||||
? minBy(bets, (bet) => {
|
||||
return bet.createdTime < comment.createdTime
|
||||
? comment.createdTime - bet.createdTime
|
||||
: comment.createdTime
|
||||
})?.probAfter
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{showReply && (
|
||||
|
@ -85,7 +73,7 @@ export function FeedCommentThread(props: {
|
|||
/>
|
||||
<ContractCommentInput
|
||||
contract={contract}
|
||||
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
||||
betsByCurrentUser={(user && betsByCurrentUser) ?? []}
|
||||
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
|
||||
parentCommentId={parentComment.id}
|
||||
replyToUser={replyTo}
|
||||
|
@ -104,22 +92,21 @@ export function FeedComment(props: {
|
|||
contract: Contract
|
||||
comment: ContractComment
|
||||
tips: CommentTips
|
||||
betsBySameUser: Bet[]
|
||||
indent?: boolean
|
||||
probAtCreatedTime?: number
|
||||
onReplyClick?: (comment: ContractComment) => void
|
||||
}) {
|
||||
const { contract, comment, tips, indent, onReplyClick } = props
|
||||
const {
|
||||
contract,
|
||||
comment,
|
||||
tips,
|
||||
betsBySameUser,
|
||||
indent,
|
||||
probAtCreatedTime,
|
||||
onReplyClick,
|
||||
} = props
|
||||
const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
|
||||
comment
|
||||
text,
|
||||
content,
|
||||
userUsername,
|
||||
userName,
|
||||
userAvatarUrl,
|
||||
commenterPositionProb,
|
||||
commenterPositionShares,
|
||||
commenterPositionOutcome,
|
||||
createdTime,
|
||||
} = comment
|
||||
const betOutcome = comment.betOutcome
|
||||
let bought: string | undefined
|
||||
let money: string | undefined
|
||||
|
@ -136,13 +123,6 @@ export function FeedComment(props: {
|
|||
}
|
||||
}, [comment.id, router.asPath])
|
||||
|
||||
// Only calculated if they don't have a matching bet
|
||||
const { userPosition, outcome } = getBettorsLargestPositionBeforeTime(
|
||||
contract,
|
||||
comment.createdTime,
|
||||
comment.betId ? [] : betsBySameUser
|
||||
)
|
||||
|
||||
return (
|
||||
<Row
|
||||
id={comment.id}
|
||||
|
@ -167,14 +147,17 @@ export function FeedComment(props: {
|
|||
username={userUsername}
|
||||
name={userName}
|
||||
/>{' '}
|
||||
{!comment.betId != null &&
|
||||
userPosition > 0 &&
|
||||
{comment.betId == null &&
|
||||
commenterPositionProb != null &&
|
||||
commenterPositionOutcome != null &&
|
||||
commenterPositionShares != null &&
|
||||
commenterPositionShares > 0 &&
|
||||
contract.outcomeType !== 'NUMERIC' && (
|
||||
<>
|
||||
{'is '}
|
||||
<CommentStatus
|
||||
prob={probAtCreatedTime}
|
||||
outcome={outcome}
|
||||
prob={commenterPositionProb}
|
||||
outcome={commenterPositionOutcome}
|
||||
contract={contract}
|
||||
/>
|
||||
</>
|
||||
|
@ -310,56 +293,6 @@ export function ContractCommentInput(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function getBettorsLargestPositionBeforeTime(
|
||||
contract: Contract,
|
||||
createdTime: number,
|
||||
bets: Bet[]
|
||||
) {
|
||||
let yesFloorShares = 0,
|
||||
yesShares = 0,
|
||||
noShares = 0,
|
||||
noFloorShares = 0
|
||||
|
||||
const previousBets = bets.filter(
|
||||
(prevBet) => prevBet.createdTime < createdTime && !prevBet.isAnte
|
||||
)
|
||||
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||
const answerCounts: { [outcome: string]: number } = {}
|
||||
for (const bet of previousBets) {
|
||||
if (bet.outcome) {
|
||||
if (!answerCounts[bet.outcome]) {
|
||||
answerCounts[bet.outcome] = bet.amount
|
||||
} else {
|
||||
answerCounts[bet.outcome] += bet.amount
|
||||
}
|
||||
}
|
||||
}
|
||||
const majorityAnswer =
|
||||
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
|
||||
return {
|
||||
userPosition: answerCounts[majorityAnswer] || 0,
|
||||
outcome: majorityAnswer,
|
||||
}
|
||||
}
|
||||
if (bets.length === 0) {
|
||||
return { userPosition: 0, outcome: '' }
|
||||
}
|
||||
|
||||
const [yesBets, noBets] = partition(
|
||||
previousBets ?? [],
|
||||
(bet) => bet.outcome === 'YES'
|
||||
)
|
||||
yesShares = sumBy(yesBets, (bet) => bet.shares)
|
||||
noShares = sumBy(noBets, (bet) => bet.shares)
|
||||
yesFloorShares = Math.floor(yesShares)
|
||||
noFloorShares = Math.floor(noShares)
|
||||
|
||||
const userPosition = yesFloorShares || noFloorShares
|
||||
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
|
||||
return { userPosition, outcome }
|
||||
}
|
||||
|
||||
function canCommentOnBet(bet: Bet, user?: User | null) {
|
||||
const { userId, createdTime, isRedemption } = bet
|
||||
const isSelf = user?.id === userId
|
||||
|
|
Loading…
Reference in New Issue
Block a user