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",
"queryScope": "COLLECTION_GROUP",

View File

@ -22,6 +22,60 @@ import { addUserToContractFollowers } from './follow-market'
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
.runWith({ secrets: ['MAILGUN_KEY'] })
.firestore.document('contracts/{contractId}/comments/{commentId}')
@ -55,17 +109,33 @@ 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)
const priorBets = await getPriorContractBets(
contractId,
comment.createdTime
)
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) {
const fields: { [k: string]: unknown } = {
commenterPositionShares: position.shares,
@ -73,7 +143,7 @@ export const onCreateCommentOnContract = functions
}
const previousProb =
contract.outcomeType === 'BINARY'
? maxBy(previousBets, (bet) => bet.createdTime)?.probAfter
? maxBy(priorBets, (bet) => bet.createdTime)?.probAfter
: undefined
if (previousProb != null) {
fields.commenterPositionProb = previousProb
@ -81,7 +151,6 @@ export const onCreateCommentOnContract = functions
await change.ref.update(fields)
}
let bet: Bet | undefined
let answer: Answer | undefined
if (comment.answerOutcome) {
answer =
@ -90,23 +159,6 @@ export const onCreateCommentOnContract = functions
(answer) => answer.id === comment.answerOutcome
)
: 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>(

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,6 @@
import { Bet } from 'common/bet'
import { ContractComment } from 'common/comment'
import { User } from 'common/user'
import { Contract } from 'common/contract'
import React, { useEffect, useState } from 'react'
import { Dictionary } from 'lodash'
import { useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format'
import { useRouter } from 'next/router'
@ -24,23 +21,12 @@ import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input'
export function FeedCommentThread(props: {
user: User | null | undefined
contract: Contract
threadComments: ContractComment[]
tips: CommentTipMap
parentComment: ContractComment
betsByCurrentUser: Bet[]
commentsByUserId: Dictionary<ContractComment[]>
}) {
const {
user,
contract,
threadComments,
commentsByUserId,
betsByCurrentUser,
tips,
parentComment,
} = props
const { contract, threadComments, tips, parentComment } = props
const [showReply, setShowReply] = useState(false)
const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
@ -73,11 +59,8 @@ export function FeedCommentThread(props: {
/>
<ContractCommentInput
contract={contract}
betsByCurrentUser={(user && betsByCurrentUser) ?? []}
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
parentCommentId={parentComment.id}
replyToUser={replyTo}
parentAnswerOutcome={parentComment.answerOutcome}
onSubmitComment={() => {
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: {
contract: Contract
outcome: string
@ -247,8 +202,6 @@ function CommentStatus(props: {
export function ContractCommentInput(props: {
contract: Contract
betsByCurrentUser: Bet[]
commentsByCurrentUser: ContractComment[]
className?: string
parentAnswerOutcome?: string | undefined
replyToUser?: { id: string; username: string }
@ -256,7 +209,7 @@ export function ContractCommentInput(props: {
onSubmitComment?: () => void
}) {
const user = useUser()
async function onSubmitComment(editor: Editor, betId: string | undefined) {
async function onSubmitComment(editor: Editor) {
if (!user) {
track('sign in to comment')
return await firebaseLogin()
@ -265,22 +218,12 @@ export function ContractCommentInput(props: {
props.contract.id,
editor.getJSON(),
user,
betId,
props.parentAnswerOutcome,
props.parentCommentId
)
props.onSubmitComment?.()
}
const mostRecentCommentableBet = getMostRecentCommentableBet(
props.betsByCurrentUser,
props.commentsByCurrentUser,
user,
props.parentAnswerOutcome
)
const { id } = mostRecentCommentableBet || { id: undefined }
return (
<CommentInput
replyToUser={props.replyToUser}
@ -288,14 +231,6 @@ export function ContractCommentInput(props: {
parentCommentId={props.parentCommentId}
onSubmitComment={onSubmitComment}
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,
content: JSONContent,
user: User,
betId?: string,
answerOutcome?: string,
replyToCommentId?: string
) {
const ref = betId
? doc(getCommentsCollection(contractId), betId)
: doc(getCommentsCollection(contractId))
const ref = doc(getCommentsCollection(contractId))
const onContract = {
commentType: 'contract',
contractId,
betId,
answerOutcome,
} as OnContract
return await createComment(