From 0390ec0f47186bd34df4fdad9c60c3664e21e9cb Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Wed, 20 Apr 2022 10:13:17 -0600 Subject: [PATCH 1/7] Prepare user website urls for external link --- web/components/user-page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 0c77dc20..b73cffc4 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -88,7 +88,10 @@ export function UserPage(props: { user: User; currentUser?: User }) { {user.website && ( From 01e43abd17c630f62ba3f05731fac88aa23e092b Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 Apr 2022 16:36:41 -0500 Subject: [PATCH 2/7] Add factors to weight new contracts higher, contracts with unseen comments, and contracts with prob closer to 50% --- common/calculate.ts | 21 ++++++++++- web/components/contract/contract-card.tsx | 14 +------- web/hooks/use-algo-feed.ts | 44 +++++++++++++++++------ 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/common/calculate.ts b/common/calculate.ts index d6834295..cc6451c5 100644 --- a/common/calculate.ts +++ b/common/calculate.ts @@ -18,7 +18,14 @@ import { getDpmProbabilityAfterSale, } from './calculate-dpm' import { calculateFixedPayout } from './calculate-fixed-payouts' -import { Binary, Contract, CPMM, DPM, FullContract } from './contract' +import { + Binary, + Contract, + CPMM, + DPM, + FreeResponseContract, + FullContract, +} from './contract' export function getProbability(contract: FullContract) { return contract.mechanism === 'cpmm-1' @@ -170,3 +177,15 @@ export function getContractBetNullMetrics() { profitPercent: 0, } } + +export function getTopAnswer(contract: FreeResponseContract) { + const { answers } = contract + const top = _.maxBy( + answers.map((answer) => ({ + answer, + prob: getOutcomeProbability(contract, answer.id), + })), + ({ prob }) => prob + ) + return top?.answer +} diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index f415a38c..41373679 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -23,7 +23,7 @@ import { BinaryContractOutcomeLabel, FreeResponseOutcomeLabel, } from '../outcome-label' -import { getOutcomeProbability } from '../../../common/calculate' +import { getOutcomeProbability, getTopAnswer } from '../../../common/calculate' import { AbbrContractDetails } from './contract-details' export function ContractCard(props: { @@ -122,18 +122,6 @@ export function BinaryResolutionOrChance(props: { ) } -function getTopAnswer(contract: FreeResponseContract) { - const { answers } = contract - const top = _.maxBy( - answers.map((answer) => ({ - answer, - prob: getOutcomeProbability(contract, answer.id), - })), - ({ prob }) => prob - ) - return top?.answer -} - export function FreeResponseResolutionOrChance(props: { contract: FreeResponseContract truncate: 'short' | 'long' | 'none' diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index 68958821..365f4baa 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -8,6 +8,12 @@ import { logInterpolation } from '../../common/util/math' import { getRecommendedContracts } from '../../common/recommended-contracts' import { useSeenContracts } from './use-seen-contracts' import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets' +import { DAY_MS } from '../../common/util/time' +import { + getProbability, + getOutcomeProbability, + getTopAnswer, +} from '../../common/calculate' const MAX_FEED_CONTRACTS = 75 @@ -120,11 +126,13 @@ function getContractsActivityScores( ) const scoredContracts = contracts.map((contract) => { + const { outcomeType } = contract + const seenTime = seenContracts[contract.id] const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime const hasNewComments = !seenTime || (lastCommentTime && lastCommentTime > seenTime) - const newCommentScore = hasNewComments ? 1 : 0.75 + const newCommentScore = hasNewComments ? 1 : 0.5 const commentCount = contractComments[contract.id]?.length ?? 0 const betCount = contractBets[contract.id]?.length ?? 0 @@ -132,25 +140,39 @@ function getContractsActivityScores( const activityCountScore = 0.5 + 0.5 * logInterpolation(0, 200, activtyCount) - const lastBetTime = contractMostRecentBet[contract.id]?.createdTime - const timeSinceLastBet = !lastBetTime - ? contract.createdTime - : Date.now() - lastBetTime - const daysAgo = timeSinceLastBet / oneDayMs + const lastBetTime = + contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime + const timeSinceLastBet = Date.now() - lastBetTime + const daysAgo = timeSinceLastBet / DAY_MS const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo) - const score = newCommentScore * activityCountScore * timeAgoScore + let prob = 0.5 + if (outcomeType === 'BINARY') { + prob = getProbability(contract) + } else if (outcomeType === 'FREE_RESPONSE') { + const topAnswer = getTopAnswer(contract) + if (topAnswer) + prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id)) + } + const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25 + const probScore = 0.5 + frac * 0.5 + + const score = + newCommentScore * activityCountScore * timeAgoScore * probScore // Map score to [0.5, 1] since no recent activty is not a deal breaker. const mappedScore = 0.5 + score / 2 - return [contract.id, mappedScore] as [string, number] + const newMappedScore = 0.75 + score / 4 + + const isNew = Date.now() < contract.createdTime + DAY_MS + const activityScore = isNew ? newMappedScore : mappedScore + + return [contract.id, activityScore] as [string, number] }) return _.fromPairs(scoredContracts) } -const oneDayMs = 24 * 60 * 60 * 1000 - function getSeenContractsScore( contract: Contract, seenContracts: { [contractId: string]: number } @@ -160,7 +182,7 @@ function getSeenContractsScore( return 1 } - const daysAgo = (Date.now() - lastSeen) / oneDayMs + const daysAgo = (Date.now() - lastSeen) / DAY_MS if (daysAgo < 0.5) { const frac = logInterpolation(0, 0.5, daysAgo) From e1b5b595e76414d5af90e46771813231aee62664 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 Apr 2022 16:59:34 -0500 Subject: [PATCH 3/7] Also get new contracts for feed with 0 volume. --- web/hooks/use-algo-feed.ts | 7 ++++++- web/hooks/use-contracts.ts | 16 +++++++++++++--- web/hooks/use-user-bets.ts | 12 +++++++----- web/lib/firebase/contracts.ts | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index 365f4baa..f27a1870 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -36,7 +36,12 @@ export const useAlgoFeed = ( const [algoFeed, setAlgoFeed] = useState([]) useEffect(() => { - if (initialContracts && initialBets && initialComments) { + if ( + initialContracts && + initialBets && + initialComments && + yourBetContractIds + ) { const eligibleContracts = initialContracts.filter( (c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now() ) diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 26c43ed1..c6d2be0e 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -6,6 +6,7 @@ import { listenForContracts, listenForHotContracts, listenForInactiveContracts, + listenForNewContracts, } from '../lib/firebase/contracts' import { listenForTaggedContracts } from '../lib/firebase/folds' @@ -20,13 +21,22 @@ export const useContracts = () => { } export const useActiveContracts = () => { - const [contracts, setContracts] = useState() + const [activeContracts, setActiveContracts] = useState< + Contract[] | undefined + >() + const [newContracts, setNewContracts] = useState() useEffect(() => { - return listenForActiveContracts(setContracts) + return listenForActiveContracts(setActiveContracts) }, []) - return contracts + useEffect(() => { + return listenForNewContracts(setNewContracts) + }, []) + + if (!activeContracts || !newContracts) return undefined + + return [...activeContracts, ...newContracts] } export const useInactiveContracts = () => { diff --git a/web/hooks/use-user-bets.ts b/web/hooks/use-user-bets.ts index 82606933..c11af097 100644 --- a/web/hooks/use-user-bets.ts +++ b/web/hooks/use-user-bets.ts @@ -54,13 +54,15 @@ export const useUserBetContracts = (userId: string | undefined) => { } export const useGetUserBetContractIds = (userId: string | undefined) => { - const [contractIds, setContractIds] = useState([]) + const [contractIds, setContractIds] = useState() useEffect(() => { - const key = `user-bet-contractIds-${userId}` - const userBetContractJson = localStorage.getItem(key) - if (userBetContractJson) { - setContractIds(JSON.parse(userBetContractJson)) + if (userId) { + const key = `user-bet-contractIds-${userId}` + const userBetContractJson = localStorage.getItem(key) + if (userBetContractJson) { + setContractIds(JSON.parse(userBetContractJson)) + } } }, [userId]) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index cc9118f9..1646bd09 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -22,6 +22,7 @@ import { getDpmProbability } from '../../../common/calculate-dpm' import { createRNG, shuffle } from '../../../common/util/random' import { getCpmmProbability } from '../../../common/calculate-cpmm' import { formatMoney, formatPercent } from '../../../common/util/format' +import { DAY_MS } from '../../../common/util/time' export type { Contract } export function contractPath(contract: Contract) { @@ -162,6 +163,19 @@ export function listenForInactiveContracts( return listenForValues(inactiveContractsQuery, setContracts) } +const newContractsQuery = query( + contractCollection, + where('isResolved', '==', false), + where('volume7Days', '==', 0), + where('createdTime', '>', Date.now() - 7 * DAY_MS) +) + +export function listenForNewContracts( + setContracts: (contracts: Contract[]) => void +) { + return listenForValues(newContractsQuery, setContracts) +} + export function listenForContract( contractId: string, setContract: (contract: Contract | null) => void From 8c8a9be6a118cfac841286bc183c44d41e9b80f5 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 Apr 2022 17:25:29 -0500 Subject: [PATCH 4/7] Show new label instead of 0 volume --- web/components/feed/feed-items.tsx | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index e2def4d4..2200bbbb 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -9,6 +9,7 @@ import { UserIcon, UsersIcon, XIcon, + SparklesIcon, } from '@heroicons/react/solid' import clsx from 'clsx' import Textarea from 'react-expanding-textarea' @@ -46,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 { DAY_MS } from '../../../common/util/time' export function FeedItems(props: { contract: Contract @@ -307,10 +309,18 @@ export function FeedQuestion(props: { contractPath?: string }) { const { contract, showDescription } = props - const { creatorName, creatorUsername, question, resolution, outcomeType } = - contract + const { + creatorName, + creatorUsername, + question, + resolution, + outcomeType, + volume, + createdTime, + } = contract const { volumeLabel } = contractMetrics(contract) const isBinary = outcomeType === 'BINARY' + const isNew = createdTime > Date.now() - DAY_MS // const closeMessage = // contract.isResolved || !contract.closeTime ? null : ( @@ -336,10 +346,18 @@ export function FeedQuestion(props: { />{' '} asked {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} - - {volumeLabel} - {/* {closeMessage} */} - +
+ {isNew || volume === 0 ? ( + + + ) : ( + + {volumeLabel} + {/* {closeMessage} */} + + )} +
From 7847a9e78197435a665be2f5a1f4528d424dd043 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 21 Apr 2022 01:00:08 -0500 Subject: [PATCH 5/7] Track latency of feed and portfolio page. --- common/tracking.ts | 6 ++++++ firestore.rules | 4 ++++ web/components/bets-list.tsx | 10 ++++++++++ web/hooks/use-algo-feed.ts | 6 ++++++ web/hooks/use-time-since-first-render.ts | 13 +++++++++++++ web/lib/firebase/tracking.ts | 18 +++++++++++++++++- 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 web/hooks/use-time-since-first-render.ts diff --git a/common/tracking.ts b/common/tracking.ts index 29a1365c..bf06b6e3 100644 --- a/common/tracking.ts +++ b/common/tracking.ts @@ -10,3 +10,9 @@ export type ClickEvent = { contractId: string timestamp: number } + +export type LatencyEvent = { + type: 'feed' | 'portfolio' + latency: number + timestamp: number +} diff --git a/firestore.rules b/firestore.rules index 542bd5ec..09d65aac 100644 --- a/firestore.rules +++ b/firestore.rules @@ -30,6 +30,10 @@ service cloud.firestore { allow create: if userId == request.auth.uid; } + match /private-users/{userId}/latency/{loadTimeId} { + allow create: if userId == request.auth.uid; + } + match /contracts/{contractId} { allow read; allow update: if request.resource.data.diff(resource.data).affectedKeys() diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 35e0b3d9..63cbfd57 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -37,6 +37,8 @@ import { resolvedPayout, getContractBetNullMetrics, } from '../../common/calculate' +import { useTimeSinceFirstRender } from '../hooks/use-time-since-first-render' +import { trackLatency } from '../lib/firebase/tracking' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'closed' | 'resolved' | 'all' @@ -67,6 +69,14 @@ export function BetsList(props: { user: User }) { } }, [bets]) + const getTime = useTimeSinceFirstRender() + useEffect(() => { + if (bets && contracts) { + trackLatency('portfolio', getTime()) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [!!bets, !!contracts]) + if (!bets || !contracts) { return } diff --git a/web/hooks/use-algo-feed.ts b/web/hooks/use-algo-feed.ts index f27a1870..543f484b 100644 --- a/web/hooks/use-algo-feed.ts +++ b/web/hooks/use-algo-feed.ts @@ -14,6 +14,8 @@ import { getOutcomeProbability, getTopAnswer, } from '../../common/calculate' +import { useTimeSinceFirstRender } from './use-time-since-first-render' +import { trackLatency } from '../lib/firebase/tracking' const MAX_FEED_CONTRACTS = 75 @@ -35,6 +37,8 @@ export const useAlgoFeed = ( const [algoFeed, setAlgoFeed] = useState([]) + const getTime = useTimeSinceFirstRender() + useEffect(() => { if ( initialContracts && @@ -53,6 +57,7 @@ export const useAlgoFeed = ( seenContracts ) setAlgoFeed(contracts) + trackLatency('feed', getTime()) } }, [ initialBets, @@ -60,6 +65,7 @@ export const useAlgoFeed = ( initialContracts, seenContracts, yourBetContractIds, + getTime, ]) return algoFeed diff --git a/web/hooks/use-time-since-first-render.ts b/web/hooks/use-time-since-first-render.ts new file mode 100644 index 00000000..da132146 --- /dev/null +++ b/web/hooks/use-time-since-first-render.ts @@ -0,0 +1,13 @@ +import { useCallback, useEffect, useRef } from 'react' + +export function useTimeSinceFirstRender() { + const startTimeRef = useRef(0) + useEffect(() => { + startTimeRef.current = Date.now() + }, []) + + return useCallback(() => { + if (!startTimeRef.current) return 0 + return Date.now() - startTimeRef.current + }, []) +} diff --git a/web/lib/firebase/tracking.ts b/web/lib/firebase/tracking.ts index 4d609f68..034ad09d 100644 --- a/web/lib/firebase/tracking.ts +++ b/web/lib/firebase/tracking.ts @@ -2,7 +2,7 @@ import { doc, collection, setDoc } from 'firebase/firestore' import _ from 'lodash' import { db } from './init' -import { ClickEvent, View } from '../../../common/tracking' +import { ClickEvent, LatencyEvent, View } from '../../../common/tracking' import { listenForLogin, User } from './users' let user: User | null = null @@ -34,3 +34,19 @@ export async function trackClick(contractId: string) { return await setDoc(ref, clickEvent) } + +export async function trackLatency( + type: 'feed' | 'portfolio', + latency: number +) { + if (!user) return + const ref = doc(collection(db, 'private-users', user.id, 'latency')) + + const latencyEvent: LatencyEvent = { + type, + latency, + timestamp: Date.now(), + } + + return await setDoc(ref, latencyEvent) +} From 9ce82b1b6f02c76abd91e288a3ca4c3d4b7aaf3d Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Wed, 20 Apr 2022 23:03:16 -0700 Subject: [PATCH 6/7] Show "New" badge on contract cards too --- web/components/contract/contract-details.tsx | 11 ++++++----- web/components/feed/feed-items.tsx | 20 ++++---------------- web/components/new-contract-badge.tsx | 9 +++++++++ 3 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 web/components/new-contract-badge.tsx diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 83c3c25a..8cc27496 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -18,6 +18,7 @@ import { Avatar } from '../avatar' import { useState } from 'react' import { ContractInfoDialog } from './contract-info-dialog' import { Bet } from '../../../common/bet' +import NewContractBadge from '../new-contract-badge' export function AbbrContractDetails(props: { contract: Contract @@ -25,7 +26,8 @@ export function AbbrContractDetails(props: { showCloseTime?: boolean }) { const { contract, showHotVolume, showCloseTime } = props - const { volume24Hours, creatorName, creatorUsername, closeTime } = contract + const { volume, volume24Hours, creatorName, creatorUsername, closeTime } = + contract const { volumeLabel } = contractMetrics(contract) return ( @@ -54,11 +56,10 @@ export function AbbrContractDetails(props: { {(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '} {fromNow(closeTime || 0)}
+ ) : volume > 0 ? ( + {volumeLabel} ) : ( - - {/* */} - {volumeLabel} - + )} diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 2200bbbb..33fb0912 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -48,6 +48,7 @@ import { User } from '../../../common/user' import { Modal } from '../layout/modal' import { trackClick } from '../../lib/firebase/tracking' import { DAY_MS } from '../../../common/util/time' +import NewContractBadge from '../new-contract-badge' export function FeedItems(props: { contract: Contract @@ -313,7 +314,6 @@ export function FeedQuestion(props: { creatorName, creatorUsername, question, - resolution, outcomeType, volume, createdTime, @@ -322,15 +322,6 @@ export function FeedQuestion(props: { const isBinary = outcomeType === 'BINARY' const isNew = createdTime > Date.now() - DAY_MS - // const closeMessage = - // contract.isResolved || !contract.closeTime ? null : ( - // <> - // - // {contract.closeTime > Date.now() ? 'Closes' : 'Closed'} - // - // - // ) - return ( <> {' '} asked {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} -
+
{isNew || volume === 0 ? ( - - + ) : ( - + {volumeLabel} - {/* {closeMessage} */} )}
diff --git a/web/components/new-contract-badge.tsx b/web/components/new-contract-badge.tsx new file mode 100644 index 00000000..3cd47eab --- /dev/null +++ b/web/components/new-contract-badge.tsx @@ -0,0 +1,9 @@ +import { SparklesIcon } from '@heroicons/react/solid' + +export default function NewContractBadge() { + return ( + + + ) +} From 7b70b9b3bd0d4820959ad1031d9964e399381bc6 Mon Sep 17 00:00:00 2001 From: Boa Date: Thu, 21 Apr 2022 11:09:06 -0600 Subject: [PATCH 7/7] 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 ( + <> +
+ +
+
+
+
+