Merge branch 'main' into add-liquidity
This commit is contained in:
		
						commit
						d4add3125c
					
				| 
						 | 
					@ -18,7 +18,14 @@ import {
 | 
				
			||||||
  getDpmProbabilityAfterSale,
 | 
					  getDpmProbabilityAfterSale,
 | 
				
			||||||
} from './calculate-dpm'
 | 
					} from './calculate-dpm'
 | 
				
			||||||
import { calculateFixedPayout } from './calculate-fixed-payouts'
 | 
					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<DPM | CPMM, Binary>) {
 | 
					export function getProbability(contract: FullContract<DPM | CPMM, Binary>) {
 | 
				
			||||||
  return contract.mechanism === 'cpmm-1'
 | 
					  return contract.mechanism === 'cpmm-1'
 | 
				
			||||||
| 
						 | 
					@ -170,3 +177,15 @@ export function getContractBetNullMetrics() {
 | 
				
			||||||
    profitPercent: 0,
 | 
					    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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
export type Comment = {
 | 
					export type Comment = {
 | 
				
			||||||
  id: string
 | 
					  id: string
 | 
				
			||||||
  contractId: string
 | 
					  contractId: string
 | 
				
			||||||
  betId: string
 | 
					  betId?: string
 | 
				
			||||||
  userId: string
 | 
					  userId: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  text: string
 | 
					  text: string
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,3 +10,9 @@ export type ClickEvent = {
 | 
				
			||||||
  contractId: string
 | 
					  contractId: string
 | 
				
			||||||
  timestamp: number
 | 
					  timestamp: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LatencyEvent = {
 | 
				
			||||||
 | 
					  type: 'feed' | 'portfolio'
 | 
				
			||||||
 | 
					  latency: number
 | 
				
			||||||
 | 
					  timestamp: number
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,10 @@ service cloud.firestore {
 | 
				
			||||||
      allow create: if userId == request.auth.uid;
 | 
					      allow create: if userId == request.auth.uid;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match /private-users/{userId}/latency/{loadTimeId} {
 | 
				
			||||||
 | 
					      allow create: if userId == request.auth.uid;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match /contracts/{contractId} {
 | 
					    match /contracts/{contractId} {
 | 
				
			||||||
      allow read;
 | 
					      allow read;
 | 
				
			||||||
      allow update: if request.resource.data.diff(resource.data).affectedKeys()
 | 
					      allow update: if request.resource.data.diff(resource.data).affectedKeys()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								functions/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								functions/.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
# Secrets
 | 
					# Secrets
 | 
				
			||||||
.env*
 | 
					.env*
 | 
				
			||||||
 | 
					.runtimeconfig.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Compiled JavaScript files
 | 
					# Compiled JavaScript files
 | 
				
			||||||
lib/**/*.js
 | 
					lib/**/*.js
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -167,7 +167,7 @@ export const sendNewCommentEmail = async (
 | 
				
			||||||
  commentCreator: User,
 | 
					  commentCreator: User,
 | 
				
			||||||
  contract: Contract,
 | 
					  contract: Contract,
 | 
				
			||||||
  comment: Comment,
 | 
					  comment: Comment,
 | 
				
			||||||
  bet: Bet,
 | 
					  bet?: Bet,
 | 
				
			||||||
  answer?: Answer
 | 
					  answer?: Answer
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const privateUser = await getPrivateUser(userId)
 | 
					  const privateUser = await getPrivateUser(userId)
 | 
				
			||||||
| 
						 | 
					@ -186,8 +186,11 @@ export const sendNewCommentEmail = async (
 | 
				
			||||||
  const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
 | 
					  const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
 | 
				
			||||||
  const { text } = comment
 | 
					  const { text } = comment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { amount, sale, outcome } = bet
 | 
					  let betDescription = ''
 | 
				
			||||||
  let betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}`
 | 
					  if (bet) {
 | 
				
			||||||
 | 
					    const { amount, sale } = bet
 | 
				
			||||||
 | 
					    betDescription = `${sale ? 'sold' : 'bought'} M$ ${Math.round(amount)}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const subject = `Comment on ${question}`
 | 
					  const subject = `Comment on ${question}`
 | 
				
			||||||
  const from = `${commentorName} <info@manifold.markets>`
 | 
					  const from = `${commentorName} <info@manifold.markets>`
 | 
				
			||||||
| 
						 | 
					@ -213,11 +216,12 @@ export const sendNewCommentEmail = async (
 | 
				
			||||||
      { from }
 | 
					      { from }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
 | 
					    if (bet) {
 | 
				
			||||||
      betDescription = `${betDescription} of ${toDisplayResolution(
 | 
					      betDescription = `${betDescription} of ${toDisplayResolution(
 | 
				
			||||||
        contract,
 | 
					        contract,
 | 
				
			||||||
      outcome
 | 
					        bet.outcome
 | 
				
			||||||
      )}`
 | 
					      )}`
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    await sendTemplateEmail(
 | 
					    await sendTemplateEmail(
 | 
				
			||||||
      privateUser.email,
 | 
					      privateUser.email,
 | 
				
			||||||
      subject,
 | 
					      subject,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import { getContract, getUser, getValues } from './utils'
 | 
				
			||||||
import { Comment } from '../../common/comment'
 | 
					import { Comment } from '../../common/comment'
 | 
				
			||||||
import { sendNewCommentEmail } from './emails'
 | 
					import { sendNewCommentEmail } from './emails'
 | 
				
			||||||
import { Bet } from '../../common/bet'
 | 
					import { Bet } from '../../common/bet'
 | 
				
			||||||
 | 
					import { Answer } from '../../common/answer'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const firestore = admin.firestore()
 | 
					const firestore = admin.firestore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,18 +25,22 @@ export const onCreateComment = functions.firestore
 | 
				
			||||||
    const commentCreator = await getUser(comment.userId)
 | 
					    const commentCreator = await getUser(comment.userId)
 | 
				
			||||||
    if (!commentCreator) return
 | 
					    if (!commentCreator) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let bet: Bet | undefined
 | 
				
			||||||
 | 
					    let answer: Answer | undefined
 | 
				
			||||||
 | 
					    if (comment.betId) {
 | 
				
			||||||
      const betSnapshot = await firestore
 | 
					      const betSnapshot = await firestore
 | 
				
			||||||
        .collection('contracts')
 | 
					        .collection('contracts')
 | 
				
			||||||
        .doc(contractId)
 | 
					        .doc(contractId)
 | 
				
			||||||
        .collection('bets')
 | 
					        .collection('bets')
 | 
				
			||||||
        .doc(comment.betId)
 | 
					        .doc(comment.betId)
 | 
				
			||||||
        .get()
 | 
					        .get()
 | 
				
			||||||
    const bet = betSnapshot.data() as Bet
 | 
					      bet = betSnapshot.data() as Bet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const answer =
 | 
					      answer =
 | 
				
			||||||
        contract.outcomeType === 'FREE_RESPONSE' && contract.answers
 | 
					        contract.outcomeType === 'FREE_RESPONSE' && contract.answers
 | 
				
			||||||
        ? contract.answers.find((answer) => answer.id === bet.outcome)
 | 
					          ? contract.answers.find((answer) => answer.id === bet?.outcome)
 | 
				
			||||||
          : undefined
 | 
					          : undefined
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const comments = await getValues<Comment>(
 | 
					    const comments = await getValues<Comment>(
 | 
				
			||||||
      firestore.collection('contracts').doc(contractId).collection('comments')
 | 
					      firestore.collection('contracts').doc(contractId).collection('comments')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,8 @@ import {
 | 
				
			||||||
  resolvedPayout,
 | 
					  resolvedPayout,
 | 
				
			||||||
  getContractBetNullMetrics,
 | 
					  getContractBetNullMetrics,
 | 
				
			||||||
} from '../../common/calculate'
 | 
					} 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 BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
 | 
				
			||||||
type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
 | 
					type BetFilter = 'open' | 'closed' | 'resolved' | 'all'
 | 
				
			||||||
| 
						 | 
					@ -67,6 +69,14 @@ export function BetsList(props: { user: User }) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [bets])
 | 
					  }, [bets])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getTime = useTimeSinceFirstRender()
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (bets && contracts) {
 | 
				
			||||||
 | 
					      trackLatency('portfolio', getTime())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, [!!bets, !!contracts])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!bets || !contracts) {
 | 
					  if (!bets || !contracts) {
 | 
				
			||||||
    return <LoadingIndicator />
 | 
					    return <LoadingIndicator />
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ import {
 | 
				
			||||||
  BinaryContractOutcomeLabel,
 | 
					  BinaryContractOutcomeLabel,
 | 
				
			||||||
  FreeResponseOutcomeLabel,
 | 
					  FreeResponseOutcomeLabel,
 | 
				
			||||||
} from '../outcome-label'
 | 
					} from '../outcome-label'
 | 
				
			||||||
import { getOutcomeProbability } from '../../../common/calculate'
 | 
					import { getOutcomeProbability, getTopAnswer } from '../../../common/calculate'
 | 
				
			||||||
import { AbbrContractDetails } from './contract-details'
 | 
					import { AbbrContractDetails } from './contract-details'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ContractCard(props: {
 | 
					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: {
 | 
					export function FreeResponseResolutionOrChance(props: {
 | 
				
			||||||
  contract: FreeResponseContract
 | 
					  contract: FreeResponseContract
 | 
				
			||||||
  truncate: 'short' | 'long' | 'none'
 | 
					  truncate: 'short' | 'long' | 'none'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ import { Avatar } from '../avatar'
 | 
				
			||||||
import { useState } from 'react'
 | 
					import { useState } from 'react'
 | 
				
			||||||
import { ContractInfoDialog } from './contract-info-dialog'
 | 
					import { ContractInfoDialog } from './contract-info-dialog'
 | 
				
			||||||
import { Bet } from '../../../common/bet'
 | 
					import { Bet } from '../../../common/bet'
 | 
				
			||||||
 | 
					import NewContractBadge from '../new-contract-badge'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AbbrContractDetails(props: {
 | 
					export function AbbrContractDetails(props: {
 | 
				
			||||||
  contract: Contract
 | 
					  contract: Contract
 | 
				
			||||||
| 
						 | 
					@ -25,7 +26,8 @@ export function AbbrContractDetails(props: {
 | 
				
			||||||
  showCloseTime?: boolean
 | 
					  showCloseTime?: boolean
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { contract, showHotVolume, showCloseTime } = props
 | 
					  const { contract, showHotVolume, showCloseTime } = props
 | 
				
			||||||
  const { volume24Hours, creatorName, creatorUsername, closeTime } = contract
 | 
					  const { volume, volume24Hours, creatorName, creatorUsername, closeTime } =
 | 
				
			||||||
 | 
					    contract
 | 
				
			||||||
  const { volumeLabel } = contractMetrics(contract)
 | 
					  const { volumeLabel } = contractMetrics(contract)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
| 
						 | 
					@ -54,11 +56,10 @@ export function AbbrContractDetails(props: {
 | 
				
			||||||
            {(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
 | 
					            {(closeTime || 0) < Date.now() ? 'Closed' : 'Closes'}{' '}
 | 
				
			||||||
            {fromNow(closeTime || 0)}
 | 
					            {fromNow(closeTime || 0)}
 | 
				
			||||||
          </Row>
 | 
					          </Row>
 | 
				
			||||||
 | 
					        ) : volume > 0 ? (
 | 
				
			||||||
 | 
					          <Row>{volumeLabel}</Row>
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
          <Row className="gap-1">
 | 
					          <NewContractBadge />
 | 
				
			||||||
            {/* <DatabaseIcon className="h-5 w-5" /> */}
 | 
					 | 
				
			||||||
            {volumeLabel}
 | 
					 | 
				
			||||||
          </Row>
 | 
					 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </Row>
 | 
					      </Row>
 | 
				
			||||||
    </Col>
 | 
					    </Col>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,12 +22,19 @@ export type ActivityItem =
 | 
				
			||||||
  | AnswerGroupItem
 | 
					  | AnswerGroupItem
 | 
				
			||||||
  | CloseItem
 | 
					  | CloseItem
 | 
				
			||||||
  | ResolveItem
 | 
					  | ResolveItem
 | 
				
			||||||
 | 
					  | CommentInputItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type BaseActivityItem = {
 | 
					type BaseActivityItem = {
 | 
				
			||||||
  id: string
 | 
					  id: string
 | 
				
			||||||
  contract: Contract
 | 
					  contract: Contract
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CommentInputItem = BaseActivityItem & {
 | 
				
			||||||
 | 
					  type: 'commentInput'
 | 
				
			||||||
 | 
					  bets: Bet[]
 | 
				
			||||||
 | 
					  commentsByBetId: Record<string, Comment>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DescriptionItem = BaseActivityItem & {
 | 
					export type DescriptionItem = BaseActivityItem & {
 | 
				
			||||||
  type: 'description'
 | 
					  type: 'description'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -48,7 +55,7 @@ export type BetItem = BaseActivityItem & {
 | 
				
			||||||
export type CommentItem = BaseActivityItem & {
 | 
					export type CommentItem = BaseActivityItem & {
 | 
				
			||||||
  type: 'comment'
 | 
					  type: 'comment'
 | 
				
			||||||
  comment: Comment
 | 
					  comment: Comment
 | 
				
			||||||
  bet: Bet
 | 
					  bet: Bet | undefined
 | 
				
			||||||
  hideOutcome: boolean
 | 
					  hideOutcome: boolean
 | 
				
			||||||
  truncate: boolean
 | 
					  truncate: boolean
 | 
				
			||||||
  smallAvatar: boolean
 | 
					  smallAvatar: boolean
 | 
				
			||||||
| 
						 | 
					@ -279,9 +286,9 @@ export function getAllContractActivityItems(
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    : [{ type: 'description', id: '0', contract }]
 | 
					    : [{ type: 'description', id: '0', contract }]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (outcomeType === 'FREE_RESPONSE') {
 | 
				
			||||||
    items.push(
 | 
					    items.push(
 | 
				
			||||||
    ...(outcomeType === 'FREE_RESPONSE'
 | 
					      ...getAnswerGroups(
 | 
				
			||||||
      ? getAnswerGroups(
 | 
					 | 
				
			||||||
        contract as FullContract<DPM, FreeResponse>,
 | 
					        contract as FullContract<DPM, FreeResponse>,
 | 
				
			||||||
        bets,
 | 
					        bets,
 | 
				
			||||||
        comments,
 | 
					        comments,
 | 
				
			||||||
| 
						 | 
					@ -292,13 +299,41 @@ export function getAllContractActivityItems(
 | 
				
			||||||
          reversed,
 | 
					          reversed,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      : groupBets(bets, comments, contract, user?.id, {
 | 
					    )
 | 
				
			||||||
 | 
					  } 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,
 | 
					      hideOutcome: false,
 | 
				
			||||||
      abbreviated,
 | 
					      abbreviated,
 | 
				
			||||||
      smallAvatar: false,
 | 
					      smallAvatar: false,
 | 
				
			||||||
      reversed: 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()) {
 | 
					  if (contract.closeTime && contract.closeTime <= Date.now()) {
 | 
				
			||||||
    items.push({ type: 'close', id: `${contract.closeTime}`, contract })
 | 
					    items.push({ type: 'close', id: `${contract.closeTime}`, contract })
 | 
				
			||||||
| 
						 | 
					@ -307,6 +342,15 @@ export function getAllContractActivityItems(
 | 
				
			||||||
    items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
 | 
					    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()
 | 
					  if (reversed) items.reverse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return items
 | 
					  return items
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import {
 | 
				
			||||||
  UserIcon,
 | 
					  UserIcon,
 | 
				
			||||||
  UsersIcon,
 | 
					  UsersIcon,
 | 
				
			||||||
  XIcon,
 | 
					  XIcon,
 | 
				
			||||||
 | 
					  SparklesIcon,
 | 
				
			||||||
} from '@heroicons/react/solid'
 | 
					} from '@heroicons/react/solid'
 | 
				
			||||||
import clsx from 'clsx'
 | 
					import clsx from 'clsx'
 | 
				
			||||||
import Textarea from 'react-expanding-textarea'
 | 
					import Textarea from 'react-expanding-textarea'
 | 
				
			||||||
| 
						 | 
					@ -46,6 +47,9 @@ import { useSaveSeenContract } from '../../hooks/use-seen-contracts'
 | 
				
			||||||
import { User } from '../../../common/user'
 | 
					import { User } from '../../../common/user'
 | 
				
			||||||
import { Modal } from '../layout/modal'
 | 
					import { Modal } from '../layout/modal'
 | 
				
			||||||
import { trackClick } from '../../lib/firebase/tracking'
 | 
					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'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function FeedItems(props: {
 | 
					export function FeedItems(props: {
 | 
				
			||||||
  contract: Contract
 | 
					  contract: Contract
 | 
				
			||||||
| 
						 | 
					@ -104,24 +108,30 @@ function FeedItem(props: { item: ActivityItem }) {
 | 
				
			||||||
      return <FeedClose {...item} />
 | 
					      return <FeedClose {...item} />
 | 
				
			||||||
    case 'resolve':
 | 
					    case 'resolve':
 | 
				
			||||||
      return <FeedResolve {...item} />
 | 
					      return <FeedResolve {...item} />
 | 
				
			||||||
 | 
					    case 'commentInput':
 | 
				
			||||||
 | 
					      return <CommentInput {...item} />
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function FeedComment(props: {
 | 
					export function FeedComment(props: {
 | 
				
			||||||
  contract: Contract
 | 
					  contract: Contract
 | 
				
			||||||
  comment: Comment
 | 
					  comment: Comment
 | 
				
			||||||
  bet: Bet
 | 
					  bet: Bet | undefined
 | 
				
			||||||
  hideOutcome: boolean
 | 
					  hideOutcome: boolean
 | 
				
			||||||
  truncate: boolean
 | 
					  truncate: boolean
 | 
				
			||||||
  smallAvatar: boolean
 | 
					  smallAvatar: boolean
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props
 | 
					  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 { text, userUsername, userName, userAvatarUrl, createdTime } = comment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const bought = amount >= 0 ? 'bought' : 'sold'
 | 
					 | 
				
			||||||
  const money = formatMoney(Math.abs(amount))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Avatar
 | 
					      <Avatar
 | 
				
			||||||
| 
						 | 
					@ -144,7 +154,7 @@ export function FeedComment(props: {
 | 
				
			||||||
                {' '}
 | 
					                {' '}
 | 
				
			||||||
                of{' '}
 | 
					                of{' '}
 | 
				
			||||||
                <OutcomeLabel
 | 
					                <OutcomeLabel
 | 
				
			||||||
                  outcome={outcome}
 | 
					                  outcome={outcome ? outcome : ''}
 | 
				
			||||||
                  contract={contract}
 | 
					                  contract={contract}
 | 
				
			||||||
                  truncate="short"
 | 
					                  truncate="short"
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
| 
						 | 
					@ -174,6 +184,78 @@ function RelativeTimestamp(props: { time: number }) {
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function CommentInput(props: {
 | 
				
			||||||
 | 
					  contract: Contract
 | 
				
			||||||
 | 
					  commentsByBetId: Record<string, Comment>
 | 
				
			||||||
 | 
					  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 <div />
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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 <div />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function submitComment() {
 | 
				
			||||||
 | 
					    if (!comment) return
 | 
				
			||||||
 | 
					    if (!user) {
 | 
				
			||||||
 | 
					      return await firebaseLogin()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await createComment(contract.id, comment, user)
 | 
				
			||||||
 | 
					    setComment('')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={'min-w-0 flex-1 py-1.5'}>
 | 
				
			||||||
 | 
					        <div className="text-sm text-gray-500">
 | 
				
			||||||
 | 
					          <div className="mt-2">
 | 
				
			||||||
 | 
					            <Textarea
 | 
				
			||||||
 | 
					              value={comment}
 | 
				
			||||||
 | 
					              onChange={(e) => setComment(e.target.value)}
 | 
				
			||||||
 | 
					              className="textarea textarea-bordered w-full resize-none"
 | 
				
			||||||
 | 
					              placeholder="Add a comment..."
 | 
				
			||||||
 | 
					              rows={3}
 | 
				
			||||||
 | 
					              maxLength={MAX_COMMENT_LENGTH}
 | 
				
			||||||
 | 
					              onKeyDown={(e) => {
 | 
				
			||||||
 | 
					                if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
 | 
				
			||||||
 | 
					                  submitComment()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <button
 | 
				
			||||||
 | 
					              className="btn btn-outline btn-sm mt-1"
 | 
				
			||||||
 | 
					              onClick={submitComment}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Comment
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function FeedBet(props: {
 | 
					export function FeedBet(props: {
 | 
				
			||||||
  contract: Contract
 | 
					  contract: Contract
 | 
				
			||||||
  bet: Bet
 | 
					  bet: Bet
 | 
				
			||||||
| 
						 | 
					@ -185,14 +267,12 @@ export function FeedBet(props: {
 | 
				
			||||||
  const { id, amount, outcome, createdTime, userId } = bet
 | 
					  const { id, amount, outcome, createdTime, userId } = bet
 | 
				
			||||||
  const user = useUser()
 | 
					  const user = useUser()
 | 
				
			||||||
  const isSelf = user?.id === userId
 | 
					  const isSelf = user?.id === userId
 | 
				
			||||||
 | 
					  const canComment = canCommentOnBet(userId, createdTime, user)
 | 
				
			||||||
  // You can comment if your bet was posted in the last hour
 | 
					 | 
				
			||||||
  const canComment = isSelf && Date.now() - createdTime < 60 * 60 * 1000
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [comment, setComment] = useState('')
 | 
					  const [comment, setComment] = useState('')
 | 
				
			||||||
  async function submitComment() {
 | 
					  async function submitComment() {
 | 
				
			||||||
    if (!user || !comment || !canComment) return
 | 
					    if (!user || !comment || !canComment) return
 | 
				
			||||||
    await createComment(contract.id, id, comment, user)
 | 
					    await createComment(contract.id, comment, user, id)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const bought = amount >= 0 ? 'bought' : 'sold'
 | 
					  const bought = amount >= 0 ? 'bought' : 'sold'
 | 
				
			||||||
| 
						 | 
					@ -307,19 +387,17 @@ export function FeedQuestion(props: {
 | 
				
			||||||
  contractPath?: string
 | 
					  contractPath?: string
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const { contract, showDescription } = props
 | 
					  const { contract, showDescription } = props
 | 
				
			||||||
  const { creatorName, creatorUsername, question, resolution, outcomeType } =
 | 
					  const {
 | 
				
			||||||
    contract
 | 
					    creatorName,
 | 
				
			||||||
 | 
					    creatorUsername,
 | 
				
			||||||
 | 
					    question,
 | 
				
			||||||
 | 
					    outcomeType,
 | 
				
			||||||
 | 
					    volume,
 | 
				
			||||||
 | 
					    createdTime,
 | 
				
			||||||
 | 
					  } = contract
 | 
				
			||||||
  const { volumeLabel } = contractMetrics(contract)
 | 
					  const { volumeLabel } = contractMetrics(contract)
 | 
				
			||||||
  const isBinary = outcomeType === 'BINARY'
 | 
					  const isBinary = outcomeType === 'BINARY'
 | 
				
			||||||
 | 
					  const isNew = createdTime > Date.now() - DAY_MS
 | 
				
			||||||
  // const closeMessage =
 | 
					 | 
				
			||||||
  //   contract.isResolved || !contract.closeTime ? null : (
 | 
					 | 
				
			||||||
  //     <>
 | 
					 | 
				
			||||||
  //       <span className="mx-2">•</span>
 | 
					 | 
				
			||||||
  //       {contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
 | 
					 | 
				
			||||||
  //       <RelativeTimestamp time={contract.closeTime || 0} />
 | 
					 | 
				
			||||||
  //     </>
 | 
					 | 
				
			||||||
  //   )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
| 
						 | 
					@ -336,10 +414,15 @@ export function FeedQuestion(props: {
 | 
				
			||||||
          />{' '}
 | 
					          />{' '}
 | 
				
			||||||
          asked
 | 
					          asked
 | 
				
			||||||
          {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
 | 
					          {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */}
 | 
				
			||||||
          <span className="float-right hidden text-gray-400 sm:inline">
 | 
					          <div className="relative -top-2 float-right ">
 | 
				
			||||||
 | 
					            {isNew || volume === 0 ? (
 | 
				
			||||||
 | 
					              <NewContractBadge />
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					              <span className="hidden text-gray-400 sm:inline">
 | 
				
			||||||
                {volumeLabel}
 | 
					                {volumeLabel}
 | 
				
			||||||
            {/* {closeMessage} */}
 | 
					 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
 | 
					        <Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
 | 
				
			||||||
          <Col>
 | 
					          <Col>
 | 
				
			||||||
| 
						 | 
					@ -372,6 +455,16 @@ export function FeedQuestion(props: {
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function canCommentOnBet(
 | 
				
			||||||
 | 
					  userId: string,
 | 
				
			||||||
 | 
					  createdTime: number,
 | 
				
			||||||
 | 
					  user?: User | null
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const isSelf = user?.id === userId
 | 
				
			||||||
 | 
					  // You can comment if your bet was posted in the last hour
 | 
				
			||||||
 | 
					  return isSelf && Date.now() - createdTime < 60 * 60 * 1000
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function FeedDescription(props: { contract: Contract }) {
 | 
					function FeedDescription(props: { contract: Contract }) {
 | 
				
			||||||
  const { contract } = props
 | 
					  const { contract } = props
 | 
				
			||||||
  const { creatorName, creatorUsername } = contract
 | 
					  const { creatorName, creatorUsername } = contract
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								web/components/new-contract-badge.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/components/new-contract-badge.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { SparklesIcon } from '@heroicons/react/solid'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function NewContractBadge() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <span className="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3  py-0.5 text-sm font-medium text-blue-800">
 | 
				
			||||||
 | 
					      <SparklesIcon className="h-4 w-4" aria-hidden="true" /> New
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -88,7 +88,10 @@ export function UserPage(props: { user: User; currentUser?: User }) {
 | 
				
			||||||
        <Col className="sm:flex-row sm:gap-4">
 | 
					        <Col className="sm:flex-row sm:gap-4">
 | 
				
			||||||
          {user.website && (
 | 
					          {user.website && (
 | 
				
			||||||
            <SiteLink
 | 
					            <SiteLink
 | 
				
			||||||
              href={user.website.replace('https://manifold.markets/', '')}
 | 
					              href={
 | 
				
			||||||
 | 
					                'https://' +
 | 
				
			||||||
 | 
					                user.website.replace('http://', '').replace('https://', '')
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <Row className="items-center gap-1">
 | 
					              <Row className="items-center gap-1">
 | 
				
			||||||
                <LinkIcon className="h-4 w-4" />
 | 
					                <LinkIcon className="h-4 w-4" />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,14 @@ import { logInterpolation } from '../../common/util/math'
 | 
				
			||||||
import { getRecommendedContracts } from '../../common/recommended-contracts'
 | 
					import { getRecommendedContracts } from '../../common/recommended-contracts'
 | 
				
			||||||
import { useSeenContracts } from './use-seen-contracts'
 | 
					import { useSeenContracts } from './use-seen-contracts'
 | 
				
			||||||
import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets'
 | 
					import { useGetUserBetContractIds, useUserBetContracts } from './use-user-bets'
 | 
				
			||||||
 | 
					import { DAY_MS } from '../../common/util/time'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getProbability,
 | 
				
			||||||
 | 
					  getOutcomeProbability,
 | 
				
			||||||
 | 
					  getTopAnswer,
 | 
				
			||||||
 | 
					} from '../../common/calculate'
 | 
				
			||||||
 | 
					import { useTimeSinceFirstRender } from './use-time-since-first-render'
 | 
				
			||||||
 | 
					import { trackLatency } from '../lib/firebase/tracking'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MAX_FEED_CONTRACTS = 75
 | 
					const MAX_FEED_CONTRACTS = 75
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,8 +37,15 @@ export const useAlgoFeed = (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
 | 
					  const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getTime = useTimeSinceFirstRender()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (initialContracts && initialBets && initialComments) {
 | 
					    if (
 | 
				
			||||||
 | 
					      initialContracts &&
 | 
				
			||||||
 | 
					      initialBets &&
 | 
				
			||||||
 | 
					      initialComments &&
 | 
				
			||||||
 | 
					      yourBetContractIds
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
      const eligibleContracts = initialContracts.filter(
 | 
					      const eligibleContracts = initialContracts.filter(
 | 
				
			||||||
        (c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
 | 
					        (c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
| 
						 | 
					@ -42,6 +57,7 @@ export const useAlgoFeed = (
 | 
				
			||||||
        seenContracts
 | 
					        seenContracts
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      setAlgoFeed(contracts)
 | 
					      setAlgoFeed(contracts)
 | 
				
			||||||
 | 
					      trackLatency('feed', getTime())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [
 | 
					  }, [
 | 
				
			||||||
    initialBets,
 | 
					    initialBets,
 | 
				
			||||||
| 
						 | 
					@ -49,6 +65,7 @@ export const useAlgoFeed = (
 | 
				
			||||||
    initialContracts,
 | 
					    initialContracts,
 | 
				
			||||||
    seenContracts,
 | 
					    seenContracts,
 | 
				
			||||||
    yourBetContractIds,
 | 
					    yourBetContractIds,
 | 
				
			||||||
 | 
					    getTime,
 | 
				
			||||||
  ])
 | 
					  ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return algoFeed
 | 
					  return algoFeed
 | 
				
			||||||
| 
						 | 
					@ -120,11 +137,13 @@ function getContractsActivityScores(
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const scoredContracts = contracts.map((contract) => {
 | 
					  const scoredContracts = contracts.map((contract) => {
 | 
				
			||||||
 | 
					    const { outcomeType } = contract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const seenTime = seenContracts[contract.id]
 | 
					    const seenTime = seenContracts[contract.id]
 | 
				
			||||||
    const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
 | 
					    const lastCommentTime = contractMostRecentComment[contract.id]?.createdTime
 | 
				
			||||||
    const hasNewComments =
 | 
					    const hasNewComments =
 | 
				
			||||||
      !seenTime || (lastCommentTime && lastCommentTime > seenTime)
 | 
					      !seenTime || (lastCommentTime && lastCommentTime > seenTime)
 | 
				
			||||||
    const newCommentScore = hasNewComments ? 1 : 0.75
 | 
					    const newCommentScore = hasNewComments ? 1 : 0.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const commentCount = contractComments[contract.id]?.length ?? 0
 | 
					    const commentCount = contractComments[contract.id]?.length ?? 0
 | 
				
			||||||
    const betCount = contractBets[contract.id]?.length ?? 0
 | 
					    const betCount = contractBets[contract.id]?.length ?? 0
 | 
				
			||||||
| 
						 | 
					@ -132,25 +151,39 @@ function getContractsActivityScores(
 | 
				
			||||||
    const activityCountScore =
 | 
					    const activityCountScore =
 | 
				
			||||||
      0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
 | 
					      0.5 + 0.5 * logInterpolation(0, 200, activtyCount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const lastBetTime = contractMostRecentBet[contract.id]?.createdTime
 | 
					    const lastBetTime =
 | 
				
			||||||
    const timeSinceLastBet = !lastBetTime
 | 
					      contractMostRecentBet[contract.id]?.createdTime ?? contract.createdTime
 | 
				
			||||||
      ? contract.createdTime
 | 
					    const timeSinceLastBet = Date.now() - lastBetTime
 | 
				
			||||||
      : Date.now() - lastBetTime
 | 
					    const daysAgo = timeSinceLastBet / DAY_MS
 | 
				
			||||||
    const daysAgo = timeSinceLastBet / oneDayMs
 | 
					 | 
				
			||||||
    const timeAgoScore = 1 - logInterpolation(0, 3, daysAgo)
 | 
					    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.
 | 
					    // Map score to [0.5, 1] since no recent activty is not a deal breaker.
 | 
				
			||||||
    const mappedScore = 0.5 + score / 2
 | 
					    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)
 | 
					  return _.fromPairs(scoredContracts)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const oneDayMs = 24 * 60 * 60 * 1000
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getSeenContractsScore(
 | 
					function getSeenContractsScore(
 | 
				
			||||||
  contract: Contract,
 | 
					  contract: Contract,
 | 
				
			||||||
  seenContracts: { [contractId: string]: number }
 | 
					  seenContracts: { [contractId: string]: number }
 | 
				
			||||||
| 
						 | 
					@ -160,7 +193,7 @@ function getSeenContractsScore(
 | 
				
			||||||
    return 1
 | 
					    return 1
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const daysAgo = (Date.now() - lastSeen) / oneDayMs
 | 
					  const daysAgo = (Date.now() - lastSeen) / DAY_MS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (daysAgo < 0.5) {
 | 
					  if (daysAgo < 0.5) {
 | 
				
			||||||
    const frac = logInterpolation(0, 0.5, daysAgo)
 | 
					    const frac = logInterpolation(0, 0.5, daysAgo)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import {
 | 
				
			||||||
  listenForContracts,
 | 
					  listenForContracts,
 | 
				
			||||||
  listenForHotContracts,
 | 
					  listenForHotContracts,
 | 
				
			||||||
  listenForInactiveContracts,
 | 
					  listenForInactiveContracts,
 | 
				
			||||||
 | 
					  listenForNewContracts,
 | 
				
			||||||
} from '../lib/firebase/contracts'
 | 
					} from '../lib/firebase/contracts'
 | 
				
			||||||
import { listenForTaggedContracts } from '../lib/firebase/folds'
 | 
					import { listenForTaggedContracts } from '../lib/firebase/folds'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,13 +21,22 @@ export const useContracts = () => {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useActiveContracts = () => {
 | 
					export const useActiveContracts = () => {
 | 
				
			||||||
  const [contracts, setContracts] = useState<Contract[] | undefined>()
 | 
					  const [activeContracts, setActiveContracts] = useState<
 | 
				
			||||||
 | 
					    Contract[] | undefined
 | 
				
			||||||
 | 
					  >()
 | 
				
			||||||
 | 
					  const [newContracts, setNewContracts] = useState<Contract[] | undefined>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    return listenForActiveContracts(setContracts)
 | 
					    return listenForActiveContracts(setActiveContracts)
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return contracts
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    return listenForNewContracts(setNewContracts)
 | 
				
			||||||
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!activeContracts || !newContracts) return undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [...activeContracts, ...newContracts]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useInactiveContracts = () => {
 | 
					export const useInactiveContracts = () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								web/hooks/use-time-since-first-render.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/hooks/use-time-since-first-render.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					  }, [])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -54,14 +54,16 @@ export const useUserBetContracts = (userId: string | undefined) => {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useGetUserBetContractIds = (userId: string | undefined) => {
 | 
					export const useGetUserBetContractIds = (userId: string | undefined) => {
 | 
				
			||||||
  const [contractIds, setContractIds] = useState<string[]>([])
 | 
					  const [contractIds, setContractIds] = useState<string[] | undefined>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (userId) {
 | 
				
			||||||
      const key = `user-bet-contractIds-${userId}`
 | 
					      const key = `user-bet-contractIds-${userId}`
 | 
				
			||||||
      const userBetContractJson = localStorage.getItem(key)
 | 
					      const userBetContractJson = localStorage.getItem(key)
 | 
				
			||||||
      if (userBetContractJson) {
 | 
					      if (userBetContractJson) {
 | 
				
			||||||
        setContractIds(JSON.parse(userBetContractJson))
 | 
					        setContractIds(JSON.parse(userBetContractJson))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }, [userId])
 | 
					  }, [userId])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return contractIds
 | 
					  return contractIds
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,16 +19,16 @@ export const MAX_COMMENT_LENGTH = 10000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function createComment(
 | 
					export async function createComment(
 | 
				
			||||||
  contractId: string,
 | 
					  contractId: string,
 | 
				
			||||||
  betId: string,
 | 
					 | 
				
			||||||
  text: string,
 | 
					  text: string,
 | 
				
			||||||
  commenter: User
 | 
					  commenter: User,
 | 
				
			||||||
 | 
					  betId?: string
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  const ref = doc(getCommentsCollection(contractId), betId)
 | 
					  const ref = betId
 | 
				
			||||||
 | 
					    ? doc(getCommentsCollection(contractId), betId)
 | 
				
			||||||
 | 
					    : doc(getCommentsCollection(contractId))
 | 
				
			||||||
  const comment: Comment = {
 | 
					  const comment: Comment = {
 | 
				
			||||||
    id: ref.id,
 | 
					    id: ref.id,
 | 
				
			||||||
    contractId,
 | 
					    contractId,
 | 
				
			||||||
    betId,
 | 
					 | 
				
			||||||
    userId: commenter.id,
 | 
					    userId: commenter.id,
 | 
				
			||||||
    text: text.slice(0, MAX_COMMENT_LENGTH),
 | 
					    text: text.slice(0, MAX_COMMENT_LENGTH),
 | 
				
			||||||
    createdTime: Date.now(),
 | 
					    createdTime: Date.now(),
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,9 @@ export async function createComment(
 | 
				
			||||||
    userUsername: commenter.username,
 | 
					    userUsername: commenter.username,
 | 
				
			||||||
    userAvatarUrl: commenter.avatarUrl,
 | 
					    userAvatarUrl: commenter.avatarUrl,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (betId) {
 | 
				
			||||||
 | 
					    comment.betId = betId
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  return await setDoc(ref, comment)
 | 
					  return await setDoc(ref, comment)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,8 +69,10 @@ export function listenForComments(
 | 
				
			||||||
export function mapCommentsByBetId(comments: Comment[]) {
 | 
					export function mapCommentsByBetId(comments: Comment[]) {
 | 
				
			||||||
  const map: Record<string, Comment> = {}
 | 
					  const map: Record<string, Comment> = {}
 | 
				
			||||||
  for (const comment of comments) {
 | 
					  for (const comment of comments) {
 | 
				
			||||||
 | 
					    if (comment.betId) {
 | 
				
			||||||
      map[comment.betId] = comment
 | 
					      map[comment.betId] = comment
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  return map
 | 
					  return map
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,7 @@ import { getDpmProbability } from '../../../common/calculate-dpm'
 | 
				
			||||||
import { createRNG, shuffle } from '../../../common/util/random'
 | 
					import { createRNG, shuffle } from '../../../common/util/random'
 | 
				
			||||||
import { getCpmmProbability } from '../../../common/calculate-cpmm'
 | 
					import { getCpmmProbability } from '../../../common/calculate-cpmm'
 | 
				
			||||||
import { formatMoney, formatPercent } from '../../../common/util/format'
 | 
					import { formatMoney, formatPercent } from '../../../common/util/format'
 | 
				
			||||||
 | 
					import { DAY_MS } from '../../../common/util/time'
 | 
				
			||||||
export type { Contract }
 | 
					export type { Contract }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function contractPath(contract: Contract) {
 | 
					export function contractPath(contract: Contract) {
 | 
				
			||||||
| 
						 | 
					@ -162,6 +163,19 @@ export function listenForInactiveContracts(
 | 
				
			||||||
  return listenForValues<Contract>(inactiveContractsQuery, setContracts)
 | 
					  return listenForValues<Contract>(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<Contract>(newContractsQuery, setContracts)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function listenForContract(
 | 
					export function listenForContract(
 | 
				
			||||||
  contractId: string,
 | 
					  contractId: string,
 | 
				
			||||||
  setContract: (contract: Contract | null) => void
 | 
					  setContract: (contract: Contract | null) => void
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import { doc, collection, setDoc } from 'firebase/firestore'
 | 
				
			||||||
import _ from 'lodash'
 | 
					import _ from 'lodash'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { db } from './init'
 | 
					import { db } from './init'
 | 
				
			||||||
import { ClickEvent, View } from '../../../common/tracking'
 | 
					import { ClickEvent, LatencyEvent, View } from '../../../common/tracking'
 | 
				
			||||||
import { listenForLogin, User } from './users'
 | 
					import { listenForLogin, User } from './users'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let user: User | null = null
 | 
					let user: User | null = null
 | 
				
			||||||
| 
						 | 
					@ -34,3 +34,19 @@ export async function trackClick(contractId: string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return await setDoc(ref, clickEvent)
 | 
					  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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -250,7 +250,10 @@ function ContractTopTrades(props: {
 | 
				
			||||||
  const topBettor = useUserById(betsById[topBetId]?.userId)
 | 
					  const topBettor = useUserById(betsById[topBetId]?.userId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // And also the commentId of the comment with the highest profit
 | 
					  // And also the commentId of the comment with the highest profit
 | 
				
			||||||
  const topCommentId = _.sortBy(comments, (c) => -profitById[c.betId])[0]?.id
 | 
					  const topCommentId = _.sortBy(
 | 
				
			||||||
 | 
					    comments,
 | 
				
			||||||
 | 
					    (c) => c.betId && -profitById[c.betId]
 | 
				
			||||||
 | 
					  )[0]?.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="mt-12 max-w-sm">
 | 
					    <div className="mt-12 max-w-sm">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user