From 7b70b9b3bd0d4820959ad1031d9964e399381bc6 Mon Sep 17 00:00:00 2001 From: Boa Date: Thu, 21 Apr 2022 11:09:06 -0600 Subject: [PATCH] Free comments (#88) * Allow free comments with optional bets * Send emails for comments without bets * Refactor to share logic * No free comments on free response questions * Minor fixes * Condense line --- common/comment.ts | 2 +- functions/.gitignore | 3 +- functions/src/emails.ts | 20 +++-- functions/src/on-create-comment.ts | 27 +++--- web/components/feed/activity-items.ts | 84 ++++++++++++++----- web/components/feed/feed-items.tsx | 107 +++++++++++++++++++++--- web/lib/firebase/comments.ts | 18 ++-- web/pages/[username]/[contractSlug].tsx | 5 +- 8 files changed, 207 insertions(+), 59 deletions(-) diff --git a/common/comment.ts b/common/comment.ts index cf78da4b..5daeb37e 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -3,7 +3,7 @@ export type Comment = { id: string contractId: string - betId: string + betId?: string userId: string text: string diff --git a/functions/.gitignore b/functions/.gitignore index 8b54e3dc..f6db6f5f 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -1,5 +1,6 @@ # Secrets .env* +.runtimeconfig.json # Compiled JavaScript files lib/**/*.js @@ -13,4 +14,4 @@ node_modules/ package-lock.json ui-debug.log -firebase-debug.log \ No newline at end of file +firebase-debug.log diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 143e938d..290aaecb 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -167,7 +167,7 @@ export const sendNewCommentEmail = async ( commentCreator: User, contract: Contract, comment: Comment, - bet: Bet, + bet?: Bet, answer?: Answer ) => { const privateUser = await getPrivateUser(userId) @@ -186,8 +186,11 @@ export const sendNewCommentEmail = async ( const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator const { text } = comment - const { amount, sale, outcome } = bet - let betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}` + let betDescription = '' + if (bet) { + const { amount, sale } = bet + betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}` + } const subject = `Comment on ${question}` const from = `${commentorName} ` @@ -213,11 +216,12 @@ export const sendNewCommentEmail = async ( { from } ) } else { - betDescription = `${betDescription} of ${toDisplayResolution( - contract, - outcome - )}` - + if (bet) { + betDescription = `${betDescription} of ${toDisplayResolution( + contract, + bet.outcome + )}` + } await sendTemplateEmail( privateUser.email, subject, diff --git a/functions/src/on-create-comment.ts b/functions/src/on-create-comment.ts index e48d6039..02ade1fe 100644 --- a/functions/src/on-create-comment.ts +++ b/functions/src/on-create-comment.ts @@ -6,6 +6,7 @@ import { getContract, getUser, getValues } from './utils' import { Comment } from '../../common/comment' import { sendNewCommentEmail } from './emails' import { Bet } from '../../common/bet' +import { Answer } from '../../common/answer' const firestore = admin.firestore() @@ -24,18 +25,22 @@ export const onCreateComment = functions.firestore const commentCreator = await getUser(comment.userId) if (!commentCreator) return - const betSnapshot = await firestore - .collection('contracts') - .doc(contractId) - .collection('bets') - .doc(comment.betId) - .get() - const bet = betSnapshot.data() as Bet + let bet: Bet | undefined + let answer: Answer | undefined + if (comment.betId) { + const betSnapshot = await firestore + .collection('contracts') + .doc(contractId) + .collection('bets') + .doc(comment.betId) + .get() + bet = betSnapshot.data() as Bet - const answer = - contract.outcomeType === 'FREE_RESPONSE' && contract.answers - ? contract.answers.find((answer) => answer.id === bet.outcome) - : undefined + answer = + contract.outcomeType === 'FREE_RESPONSE' && contract.answers + ? contract.answers.find((answer) => answer.id === bet?.outcome) + : undefined + } const comments = await getValues( firestore.collection('contracts').doc(contractId).collection('comments') diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index fb5effd6..1e8cd71b 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -22,12 +22,19 @@ export type ActivityItem = | AnswerGroupItem | CloseItem | ResolveItem + | CommentInputItem type BaseActivityItem = { id: string contract: Contract } +export type CommentInputItem = BaseActivityItem & { + type: 'commentInput' + bets: Bet[] + commentsByBetId: Record +} + export type DescriptionItem = BaseActivityItem & { type: 'description' } @@ -48,7 +55,7 @@ export type BetItem = BaseActivityItem & { export type CommentItem = BaseActivityItem & { type: 'comment' comment: Comment - bet: Bet + bet: Bet | undefined hideOutcome: boolean truncate: boolean smallAvatar: boolean @@ -279,26 +286,54 @@ export function getAllContractActivityItems( ] : [{ type: 'description', id: '0', contract }] - items.push( - ...(outcomeType === 'FREE_RESPONSE' - ? getAnswerGroups( - contract as FullContract, - bets, - comments, - user, - { - sortByProb: true, - abbreviated, - reversed, - } - ) - : groupBets(bets, comments, contract, user?.id, { - hideOutcome: false, + if (outcomeType === 'FREE_RESPONSE') { + items.push( + ...getAnswerGroups( + contract as FullContract, + bets, + comments, + user, + { + sortByProb: true, abbreviated, - smallAvatar: false, - reversed: false, - })) - ) + reversed, + } + ) + ) + } else { + const commentsWithoutBets = comments + .filter((comment) => !comment.betId) + .map((comment) => ({ + type: 'comment' as const, + id: comment.id, + contract: contract, + comment, + bet: undefined, + truncate: false, + hideOutcome: true, + smallAvatar: false, + })) + + const groupedBets = groupBets(bets, comments, contract, user?.id, { + hideOutcome: false, + abbreviated, + smallAvatar: false, + reversed: false, + }) + + // iterate through the bets and comment activity items and add them to the items in order of comment creation time: + const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets] + const sortedBetsAndComments = _.sortBy(unorderedBetsAndComments, (item) => { + if (item.type === 'comment') { + return item.comment.createdTime + } else if (item.type === 'bet') { + return item.bet.createdTime + } else if (item.type === 'betgroup') { + return item.bets[0].createdTime + } + }) + items.push(...sortedBetsAndComments) + } if (contract.closeTime && contract.closeTime <= Date.now()) { items.push({ type: 'close', id: `${contract.closeTime}`, contract }) @@ -307,6 +342,15 @@ export function getAllContractActivityItems( items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract }) } + const commentsByBetId = mapCommentsByBetId(comments) + items.push({ + type: 'commentInput', + id: 'commentInput', + bets, + commentsByBetId, + contract, + }) + if (reversed) items.reverse() return items diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 33fb0912..c865b617 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -47,6 +47,7 @@ import { useSaveSeenContract } from '../../hooks/use-seen-contracts' import { User } from '../../../common/user' import { Modal } from '../layout/modal' import { trackClick } from '../../lib/firebase/tracking' +import { firebaseLogin } from '../../lib/firebase/users' import { DAY_MS } from '../../../common/util/time' import NewContractBadge from '../new-contract-badge' @@ -107,24 +108,30 @@ function FeedItem(props: { item: ActivityItem }) { return case 'resolve': return + case 'commentInput': + return } } export function FeedComment(props: { contract: Contract comment: Comment - bet: Bet + bet: Bet | undefined hideOutcome: boolean truncate: boolean smallAvatar: boolean }) { const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props - const { amount, outcome } = bet + let money: string | undefined + let outcome: string | undefined + let bought: string | undefined + if (bet) { + outcome = bet.outcome + bought = bet.amount >= 0 ? 'bought' : 'sold' + money = formatMoney(Math.abs(bet.amount)) + } const { text, userUsername, userName, userAvatarUrl, createdTime } = comment - const bought = amount >= 0 ? 'bought' : 'sold' - const money = formatMoney(Math.abs(amount)) - return ( <> @@ -177,6 +184,78 @@ function RelativeTimestamp(props: { time: number }) { ) } +export function CommentInput(props: { + contract: Contract + commentsByBetId: Record + bets: Bet[] +}) { + // see if we can comment input on any bet: + const { contract, bets, commentsByBetId } = props + const { outcomeType } = contract + const user = useUser() + const [comment, setComment] = useState('') + + if (outcomeType === 'FREE_RESPONSE') { + return
+ } + + let canCommentOnABet = false + bets.some((bet) => { + // make sure there is not already a comment with a matching bet id: + const matchingComment = commentsByBetId[bet.id] + if (matchingComment) { + return false + } + const { createdTime, userId } = bet + canCommentOnABet = canCommentOnBet(userId, createdTime, user) + return canCommentOnABet + }) + + if (canCommentOnABet) return
+ + async function submitComment() { + if (!comment) return + if (!user) { + return await firebaseLogin() + } + await createComment(contract.id, comment, user) + setComment('') + } + + return ( + <> +
+ +
+
+
+
+