Move comment-bet association code into comment creation trigger (#899)

* Move comment-bet association code into comment creation trigger

* Add index for new comments query
This commit is contained in:
Marshall Polaris 2022-09-20 15:25:58 -07:00 committed by GitHub
parent faaf502114
commit 60c79141aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 184 deletions

View File

@ -100,6 +100,20 @@
} }
] ]
}, },
{
"collectionGroup": "comments",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "comments", "collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP", "queryScope": "COLLECTION_GROUP",

View File

@ -22,6 +22,60 @@ import { addUserToContractFollowers } from './follow-market'
const firestore = admin.firestore() const firestore = admin.firestore()
function getMostRecentCommentableBet(
before: number,
betsByCurrentUser: Bet[],
commentsByCurrentUser: ContractComment[],
answerOutcome?: string
) {
let sortedBetsByCurrentUser = betsByCurrentUser.sort(
(a, b) => b.createdTime - a.createdTime
)
if (answerOutcome) {
sortedBetsByCurrentUser = sortedBetsByCurrentUser.slice(0, 1)
}
return sortedBetsByCurrentUser
.filter((bet) => {
const { createdTime, isRedemption } = bet
// You can comment on bets posted in the last hour
const commentable = !isRedemption && before - createdTime < 60 * 60 * 1000
const alreadyCommented = commentsByCurrentUser.some(
(comment) => comment.createdTime > bet.createdTime
)
if (commentable && !alreadyCommented) {
if (!answerOutcome) return true
return answerOutcome === bet.outcome
}
return false
})
.pop()
}
async function getPriorUserComments(
contractId: string,
userId: string,
before: number
) {
const priorCommentsQuery = await firestore
.collection('contracts')
.doc(contractId)
.collection('comments')
.where('createdTime', '<', before)
.where('userId', '==', userId)
.get()
return priorCommentsQuery.docs.map((d) => d.data() as ContractComment)
}
async function getPriorContractBets(contractId: string, before: number) {
const priorBetsQuery = await firestore
.collection('contracts')
.doc(contractId)
.collection('bets')
.where('createdTime', '<', before)
.get()
return priorBetsQuery.docs.map((d) => d.data() as Bet)
}
export const onCreateCommentOnContract = functions export const onCreateCommentOnContract = functions
.runWith({ secrets: ['MAILGUN_KEY'] }) .runWith({ secrets: ['MAILGUN_KEY'] })
.firestore.document('contracts/{contractId}/comments/{commentId}') .firestore.document('contracts/{contractId}/comments/{commentId}')
@ -55,17 +109,33 @@ export const onCreateCommentOnContract = functions
.doc(contract.id) .doc(contract.id)
.update({ lastCommentTime, lastUpdatedTime: Date.now() }) .update({ lastCommentTime, lastUpdatedTime: Date.now() })
const previousBetsQuery = await firestore const priorBets = await getPriorContractBets(
.collection('contracts') contractId,
.doc(contractId) comment.createdTime
.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)
) )
const priorUserBets = priorBets.filter(
(b) => b.userId === comment.userId && !b.isAnte
)
const priorUserComments = await getPriorUserComments(
contractId,
comment.userId,
comment.createdTime
)
const bet = getMostRecentCommentableBet(
comment.createdTime,
priorUserBets,
priorUserComments,
comment.answerOutcome
)
if (bet) {
await change.ref.update({
betId: bet.id,
betOutcome: bet.outcome,
betAmount: bet.amount,
})
}
const position = getLargestPosition(contract, priorUserBets)
if (position) { if (position) {
const fields: { [k: string]: unknown } = { const fields: { [k: string]: unknown } = {
commenterPositionShares: position.shares, commenterPositionShares: position.shares,
@ -73,7 +143,7 @@ export const onCreateCommentOnContract = functions
} }
const previousProb = const previousProb =
contract.outcomeType === 'BINARY' contract.outcomeType === 'BINARY'
? maxBy(previousBets, (bet) => bet.createdTime)?.probAfter ? maxBy(priorBets, (bet) => bet.createdTime)?.probAfter
: undefined : undefined
if (previousProb != null) { if (previousProb != null) {
fields.commenterPositionProb = previousProb fields.commenterPositionProb = previousProb
@ -81,7 +151,6 @@ export const onCreateCommentOnContract = functions
await change.ref.update(fields) await change.ref.update(fields)
} }
let bet: Bet | undefined
let answer: Answer | undefined let answer: Answer | undefined
if (comment.answerOutcome) { if (comment.answerOutcome) {
answer = answer =
@ -90,23 +159,6 @@ export const onCreateCommentOnContract = functions
(answer) => answer.id === comment.answerOutcome (answer) => answer.id === comment.answerOutcome
) )
: undefined : undefined
} else if (comment.betId) {
const betSnapshot = await firestore
.collection('contracts')
.doc(contractId)
.collection('bets')
.doc(comment.betId)
.get()
bet = betSnapshot.data() as Bet
answer =
contract.outcomeType === 'FREE_RESPONSE' && contract.answers
? contract.answers.find((answer) => answer.id === bet?.outcome)
: undefined
await change.ref.update({
betOutcome: bet.outcome,
betAmount: bet.amount,
})
} }
const comments = await getValues<ContractComment>( const comments = await getValues<ContractComment>(

View File

@ -16,17 +16,11 @@ export function CommentInput(props: {
parentAnswerOutcome?: string parentAnswerOutcome?: string
// Reply to another comment // Reply to another comment
parentCommentId?: string parentCommentId?: string
onSubmitComment?: (editor: Editor, betId: string | undefined) => void onSubmitComment?: (editor: Editor) => void
className?: string className?: string
presetId?: string
}) { }) {
const { const { parentAnswerOutcome, parentCommentId, replyToUser, onSubmitComment } =
parentAnswerOutcome, props
parentCommentId,
replyToUser,
onSubmitComment,
presetId,
} = props
const user = useUser() const user = useUser()
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
@ -40,10 +34,10 @@ export function CommentInput(props: {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
async function submitComment(betId: string | undefined) { async function submitComment() {
if (!editor || editor.isEmpty || isSubmitting) return if (!editor || editor.isEmpty || isSubmitting) return
setIsSubmitting(true) setIsSubmitting(true)
onSubmitComment?.(editor, betId) onSubmitComment?.(editor)
setIsSubmitting(false) setIsSubmitting(false)
} }
@ -65,7 +59,6 @@ export function CommentInput(props: {
user={user} user={user}
submitComment={submitComment} submitComment={submitComment}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
presetId={presetId}
/> />
</div> </div>
</Row> </Row>
@ -77,25 +70,17 @@ export function CommentInputTextArea(props: {
replyToUser?: { id: string; username: string } replyToUser?: { id: string; username: string }
editor: Editor | null editor: Editor | null
upload: Parameters<typeof TextEditor>[0]['upload'] upload: Parameters<typeof TextEditor>[0]['upload']
submitComment: (id?: string) => void submitComment: () => void
isSubmitting: boolean isSubmitting: boolean
presetId?: string
}) { }) {
const { const { user, editor, upload, submitComment, isSubmitting, replyToUser } =
user, props
editor,
upload,
submitComment,
presetId,
isSubmitting,
replyToUser,
} = props
useEffect(() => { useEffect(() => {
editor?.setEditable(!isSubmitting) editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor]) }, [isSubmitting, editor])
const submit = () => { const submit = () => {
submitComment(presetId) submitComment()
editor?.commands?.clearContent() editor?.commands?.clearContent()
} }
@ -151,14 +136,14 @@ export function CommentInputTextArea(props: {
)} )}
{isSubmitting && ( {isSubmitting && (
<LoadingIndicator spinnerClassName={'border-gray-500'} /> <LoadingIndicator spinnerClassName="border-gray-500" />
)} )}
</TextEditor> </TextEditor>
<Row> <Row>
{!user && ( {!user && (
<button <button
className={'btn btn-outline btn-sm mt-2 normal-case'} className="btn btn-outline btn-sm mt-2 normal-case"
onClick={() => submitComment(presetId)} onClick={submitComment}
> >
Add my comment Add my comment
</button> </button>

View File

@ -59,7 +59,6 @@ export function ContractTabs(props: {
/> />
) )
const generalBets = outcomeType === 'FREE_RESPONSE' ? [] : visibleBets
const generalComments = comments.filter( const generalComments = comments.filter(
(comment) => (comment) =>
comment.answerOutcome === undefined && comment.answerOutcome === undefined &&
@ -71,36 +70,24 @@ export function ContractTabs(props: {
<> <>
<FreeResponseContractCommentsActivity <FreeResponseContractCommentsActivity
contract={contract} contract={contract}
betsByCurrentUser={
user ? visibleBets.filter((b) => b.userId === user.id) : []
}
comments={comments} comments={comments}
tips={tips} tips={tips}
user={user}
/> />
<Col className={'mt-8 flex w-full '}> <Col className="mt-8 flex w-full">
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div> <div className="text-md mt-8 mb-2 text-left">General Comments</div>
<div className={'mb-4 w-full border-b border-gray-200'} /> <div className="mb-4 w-full border-b border-gray-200" />
<ContractCommentsActivity <ContractCommentsActivity
contract={contract} contract={contract}
betsByCurrentUser={
user ? generalBets.filter((b) => b.userId === user.id) : []
}
comments={generalComments} comments={generalComments}
tips={tips} tips={tips}
user={user}
/> />
</Col> </Col>
</> </>
) : ( ) : (
<ContractCommentsActivity <ContractCommentsActivity
contract={contract} contract={contract}
betsByCurrentUser={
user ? visibleBets.filter((b) => b.userId === user.id) : []
}
comments={comments} comments={comments}
tips={tips} tips={tips}
user={user}
/> />
) )
@ -120,7 +107,7 @@ export function ContractTabs(props: {
return ( return (
<Tabs <Tabs
currentPageForAnalytics={'contract'} currentPageForAnalytics="contract"
tabs={[ tabs={[
{ {
title: 'Comments', title: 'Comments',

View File

@ -8,7 +8,6 @@ import { FeedBet } from './feed-bets'
import { FeedLiquidity } from './feed-liquidity' import { FeedLiquidity } from './feed-liquidity'
import { FeedAnswerCommentGroup } from './feed-answer-comment-group' import { FeedAnswerCommentGroup } from './feed-answer-comment-group'
import { FeedCommentThread, ContractCommentInput } from './feed-comments' import { FeedCommentThread, ContractCommentInput } from './feed-comments'
import { User } from 'common/user'
import { CommentTipMap } from 'web/hooks/use-tip-txns' import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { LiquidityProvision } from 'common/liquidity-provision' import { LiquidityProvision } from 'common/liquidity-provision'
import { groupBy, sortBy } from 'lodash' import { groupBy, sortBy } from 'lodash'
@ -72,13 +71,10 @@ export function ContractBetsActivity(props: {
export function ContractCommentsActivity(props: { export function ContractCommentsActivity(props: {
contract: Contract contract: Contract
betsByCurrentUser: Bet[]
comments: ContractComment[] comments: ContractComment[]
tips: CommentTipMap tips: CommentTipMap
user: User | null | undefined
}) { }) {
const { betsByCurrentUser, contract, comments, user, tips } = props const { contract, comments, tips } = props
const commentsByUserId = groupBy(comments, (c) => c.userId)
const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_') const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_')
const topLevelComments = sortBy( const topLevelComments = sortBy(
commentsByParentId['_'] ?? [], commentsByParentId['_'] ?? [],
@ -87,16 +83,10 @@ export function ContractCommentsActivity(props: {
return ( return (
<> <>
<ContractCommentInput <ContractCommentInput className="mb-5" contract={contract} />
className="mb-5"
contract={contract}
betsByCurrentUser={betsByCurrentUser}
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
/>
{topLevelComments.map((parent) => ( {topLevelComments.map((parent) => (
<FeedCommentThread <FeedCommentThread
key={parent.id} key={parent.id}
user={user}
contract={contract} contract={contract}
parentComment={parent} parentComment={parent}
threadComments={sortBy( threadComments={sortBy(
@ -104,8 +94,6 @@ export function ContractCommentsActivity(props: {
(c) => c.createdTime (c) => c.createdTime
)} )}
tips={tips} tips={tips}
betsByCurrentUser={betsByCurrentUser}
commentsByUserId={commentsByUserId}
/> />
))} ))}
</> </>
@ -114,18 +102,15 @@ export function ContractCommentsActivity(props: {
export function FreeResponseContractCommentsActivity(props: { export function FreeResponseContractCommentsActivity(props: {
contract: FreeResponseContract contract: FreeResponseContract
betsByCurrentUser: Bet[]
comments: ContractComment[] comments: ContractComment[]
tips: CommentTipMap tips: CommentTipMap
user: User | null | undefined
}) { }) {
const { betsByCurrentUser, contract, comments, user, tips } = props const { contract, comments, tips } = props
const sortedAnswers = sortBy( const sortedAnswers = sortBy(
contract.answers, contract.answers,
(answer) => -getOutcomeProbability(contract, answer.number.toString()) (answer) => -getOutcomeProbability(contract, answer.number.toString())
) )
const commentsByUserId = groupBy(comments, (c) => c.userId)
const commentsByOutcome = groupBy( const commentsByOutcome = groupBy(
comments, comments,
(c) => c.answerOutcome ?? c.betOutcome ?? '_' (c) => c.answerOutcome ?? c.betOutcome ?? '_'
@ -134,22 +119,19 @@ export function FreeResponseContractCommentsActivity(props: {
return ( return (
<> <>
{sortedAnswers.map((answer) => ( {sortedAnswers.map((answer) => (
<div key={answer.id} className={'relative pb-4'}> <div key={answer.id} className="relative pb-4">
<span <span
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
aria-hidden="true" aria-hidden="true"
/> />
<FeedAnswerCommentGroup <FeedAnswerCommentGroup
contract={contract} contract={contract}
user={user}
answer={answer} answer={answer}
answerComments={sortBy( answerComments={sortBy(
commentsByOutcome[answer.number.toString()] ?? [], commentsByOutcome[answer.number.toString()] ?? [],
(c) => c.createdTime (c) => c.createdTime
)} )}
tips={tips} tips={tips}
betsByCurrentUser={betsByCurrentUser}
commentsByUserId={commentsByUserId}
/> />
</div> </div>
))} ))}

View File

@ -1,5 +1,4 @@
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
import { Bet } from 'common/bet'
import { FreeResponseContract } from 'common/contract' import { FreeResponseContract } from 'common/contract'
import { ContractComment } from 'common/comment' import { ContractComment } from 'common/comment'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
@ -14,7 +13,6 @@ import {
} from 'web/components/feed/feed-comments' } from 'web/components/feed/feed-comments'
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Dictionary } from 'lodash'
import { User } from 'common/user' import { User } from 'common/user'
import { useEvent } from 'web/hooks/use-event' import { useEvent } from 'web/hooks/use-event'
import { CommentTipMap } from 'web/hooks/use-tip-txns' import { CommentTipMap } from 'web/hooks/use-tip-txns'
@ -22,22 +20,11 @@ import { UserLink } from 'web/components/user-link'
export function FeedAnswerCommentGroup(props: { export function FeedAnswerCommentGroup(props: {
contract: FreeResponseContract contract: FreeResponseContract
user: User | undefined | null
answer: Answer answer: Answer
answerComments: ContractComment[] answerComments: ContractComment[]
tips: CommentTipMap tips: CommentTipMap
betsByCurrentUser: Bet[]
commentsByUserId: Dictionary<ContractComment[]>
}) { }) {
const { const { answer, contract, answerComments, tips } = props
answer,
contract,
answerComments,
tips,
betsByCurrentUser,
commentsByUserId,
user,
} = props
const { username, avatarUrl, name, text } = answer const { username, avatarUrl, name, text } = answer
const [replyToUser, setReplyToUser] = const [replyToUser, setReplyToUser] =
@ -47,7 +34,6 @@ export function FeedAnswerCommentGroup(props: {
const router = useRouter() const router = useRouter()
const answerElementId = `answer-${answer.id}` const answerElementId = `answer-${answer.id}`
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
const scrollAndOpenReplyInput = useEvent( const scrollAndOpenReplyInput = useEvent(
(comment?: ContractComment, answer?: Answer) => { (comment?: ContractComment, answer?: Answer) => {
@ -133,8 +119,6 @@ export function FeedAnswerCommentGroup(props: {
/> />
<ContractCommentInput <ContractCommentInput
contract={contract} contract={contract}
betsByCurrentUser={betsByCurrentUser}
commentsByCurrentUser={commentsByCurrentUser}
parentAnswerOutcome={answer.number.toString()} parentAnswerOutcome={answer.number.toString()}
replyToUser={replyToUser} replyToUser={replyToUser}
onSubmitComment={() => setShowReply(false)} onSubmitComment={() => setShowReply(false)}

View File

@ -1,9 +1,6 @@
import { Bet } from 'common/bet'
import { ContractComment } from 'common/comment' import { ContractComment } from 'common/comment'
import { User } from 'common/user'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Dictionary } from 'lodash'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -24,23 +21,12 @@ import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input' import { CommentInput } from '../comment-input'
export function FeedCommentThread(props: { export function FeedCommentThread(props: {
user: User | null | undefined
contract: Contract contract: Contract
threadComments: ContractComment[] threadComments: ContractComment[]
tips: CommentTipMap tips: CommentTipMap
parentComment: ContractComment parentComment: ContractComment
betsByCurrentUser: Bet[]
commentsByUserId: Dictionary<ContractComment[]>
}) { }) {
const { const { contract, threadComments, tips, parentComment } = props
user,
contract,
threadComments,
commentsByUserId,
betsByCurrentUser,
tips,
parentComment,
} = props
const [showReply, setShowReply] = useState(false) const [showReply, setShowReply] = useState(false)
const [replyTo, setReplyTo] = useState<{ id: string; username: string }>() const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
@ -73,11 +59,8 @@ export function FeedCommentThread(props: {
/> />
<ContractCommentInput <ContractCommentInput
contract={contract} contract={contract}
betsByCurrentUser={(user && betsByCurrentUser) ?? []}
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
parentCommentId={parentComment.id} parentCommentId={parentComment.id}
replyToUser={replyTo} replyToUser={replyTo}
parentAnswerOutcome={parentComment.answerOutcome}
onSubmitComment={() => { onSubmitComment={() => {
setShowReply(false) setShowReply(false)
}} }}
@ -202,34 +185,6 @@ export function FeedComment(props: {
) )
} }
export function getMostRecentCommentableBet(
betsByCurrentUser: Bet[],
commentsByCurrentUser: ContractComment[],
user?: User | null,
answerOutcome?: string
) {
let sortedBetsByCurrentUser = betsByCurrentUser.sort(
(a, b) => b.createdTime - a.createdTime
)
if (answerOutcome) {
sortedBetsByCurrentUser = sortedBetsByCurrentUser.slice(0, 1)
}
return sortedBetsByCurrentUser
.filter((bet) => {
if (
canCommentOnBet(bet, user) &&
!commentsByCurrentUser.some(
(comment) => comment.createdTime > bet.createdTime
)
) {
if (!answerOutcome) return true
return answerOutcome === bet.outcome
}
return false
})
.pop()
}
function CommentStatus(props: { function CommentStatus(props: {
contract: Contract contract: Contract
outcome: string outcome: string
@ -247,8 +202,6 @@ function CommentStatus(props: {
export function ContractCommentInput(props: { export function ContractCommentInput(props: {
contract: Contract contract: Contract
betsByCurrentUser: Bet[]
commentsByCurrentUser: ContractComment[]
className?: string className?: string
parentAnswerOutcome?: string | undefined parentAnswerOutcome?: string | undefined
replyToUser?: { id: string; username: string } replyToUser?: { id: string; username: string }
@ -256,7 +209,7 @@ export function ContractCommentInput(props: {
onSubmitComment?: () => void onSubmitComment?: () => void
}) { }) {
const user = useUser() const user = useUser()
async function onSubmitComment(editor: Editor, betId: string | undefined) { async function onSubmitComment(editor: Editor) {
if (!user) { if (!user) {
track('sign in to comment') track('sign in to comment')
return await firebaseLogin() return await firebaseLogin()
@ -265,22 +218,12 @@ export function ContractCommentInput(props: {
props.contract.id, props.contract.id,
editor.getJSON(), editor.getJSON(),
user, user,
betId,
props.parentAnswerOutcome, props.parentAnswerOutcome,
props.parentCommentId props.parentCommentId
) )
props.onSubmitComment?.() props.onSubmitComment?.()
} }
const mostRecentCommentableBet = getMostRecentCommentableBet(
props.betsByCurrentUser,
props.commentsByCurrentUser,
user,
props.parentAnswerOutcome
)
const { id } = mostRecentCommentableBet || { id: undefined }
return ( return (
<CommentInput <CommentInput
replyToUser={props.replyToUser} replyToUser={props.replyToUser}
@ -288,14 +231,6 @@ export function ContractCommentInput(props: {
parentCommentId={props.parentCommentId} parentCommentId={props.parentCommentId}
onSubmitComment={onSubmitComment} onSubmitComment={onSubmitComment}
className={props.className} className={props.className}
presetId={id}
/> />
) )
} }
function canCommentOnBet(bet: Bet, user?: User | null) {
const { userId, createdTime, isRedemption } = bet
const isSelf = user?.id === userId
// You can comment if your bet was posted in the last hour
return !isRedemption && isSelf && Date.now() - createdTime < 60 * 60 * 1000
}

View File

@ -35,17 +35,13 @@ export async function createCommentOnContract(
contractId: string, contractId: string,
content: JSONContent, content: JSONContent,
user: User, user: User,
betId?: string,
answerOutcome?: string, answerOutcome?: string,
replyToCommentId?: string replyToCommentId?: string
) { ) {
const ref = betId const ref = doc(getCommentsCollection(contractId))
? doc(getCommentsCollection(contractId), betId)
: doc(getCommentsCollection(contractId))
const onContract = { const onContract = {
commentType: 'contract', commentType: 'contract',
contractId, contractId,
betId,
answerOutcome, answerOutcome,
} as OnContract } as OnContract
return await createComment( return await createComment(