From 60c79141aad8e2a4625a77df6e287bc4a27ba08d Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 20 Sep 2022 15:25:58 -0700 Subject: [PATCH] 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 --- firestore.indexes.json | 14 +++ .../src/on-create-comment-on-contract.ts | 110 +++++++++++++----- web/components/comment-input.tsx | 39 ++----- web/components/contract/contract-tabs.tsx | 21 +--- web/components/feed/contract-activity.tsx | 26 +---- .../feed/feed-answer-comment-group.tsx | 18 +-- web/components/feed/feed-comments.tsx | 69 +---------- web/lib/firebase/comments.ts | 6 +- 8 files changed, 119 insertions(+), 184 deletions(-) diff --git a/firestore.indexes.json b/firestore.indexes.json index bcee41d5..2b7ef839 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -100,6 +100,20 @@ } ] }, + { + "collectionGroup": "comments", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "userId", + "order": "ASCENDING" + }, + { + "fieldPath": "createdTime", + "order": "ASCENDING" + } + ] + }, { "collectionGroup": "comments", "queryScope": "COLLECTION_GROUP", diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 101b085c..d1f0a503 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -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( diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index ca1f4a96..bf3730f3 100644 --- a/web/components/comment-input.tsx +++ b/web/components/comment-input.tsx @@ -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} /> @@ -77,25 +70,17 @@ export function CommentInputTextArea(props: { replyToUser?: { id: string; username: string } editor: Editor | null upload: Parameters[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 && ( - + )} {!user && ( diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index c1ff2186..e3153dbb 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -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: { <> b.userId === user.id) : [] - } comments={comments} tips={tips} - user={user} /> - -
General Comments
-
+ +
General Comments
+
b.userId === user.id) : [] - } comments={generalComments} tips={tips} - user={user} /> ) : ( b.userId === user.id) : [] - } comments={comments} tips={tips} - user={user} /> ) @@ -120,7 +107,7 @@ export function ContractTabs(props: { return ( 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 ( <> - + {topLevelComments.map((parent) => ( 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) => ( -
+
))} diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 51f1bbc2..84f1e8c5 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -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 }) { - 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: { /> setShowReply(false)} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 0eca8915..027b377f 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -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 }) { - 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: { /> { 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 ( ) } - -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 -} diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index e00d7397..db4e8ede 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -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(