Merge branch 'main' into add-liquidity
This commit is contained in:
commit
d4add3125c
|
@ -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<DPM | CPMM, Binary>) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
export type Comment = {
|
||||
id: string
|
||||
contractId: string
|
||||
betId: string
|
||||
betId?: string
|
||||
userId: string
|
||||
|
||||
text: string
|
||||
|
|
|
@ -10,3 +10,9 @@ export type ClickEvent = {
|
|||
contractId: string
|
||||
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;
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
1
functions/.gitignore
vendored
1
functions/.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
# Secrets
|
||||
.env*
|
||||
.runtimeconfig.json
|
||||
|
||||
# Compiled JavaScript files
|
||||
lib/**/*.js
|
||||
|
|
|
@ -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} <info@manifold.markets>`
|
||||
|
@ -213,11 +216,12 @@ export const sendNewCommentEmail = async (
|
|||
{ from }
|
||||
)
|
||||
} else {
|
||||
if (bet) {
|
||||
betDescription = `${betDescription} of ${toDisplayResolution(
|
||||
contract,
|
||||
outcome
|
||||
bet.outcome
|
||||
)}`
|
||||
|
||||
}
|
||||
await sendTemplateEmail(
|
||||
privateUser.email,
|
||||
subject,
|
||||
|
|
|
@ -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
|
||||
|
||||
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()
|
||||
const bet = betSnapshot.data() as Bet
|
||||
bet = betSnapshot.data() as Bet
|
||||
|
||||
const answer =
|
||||
answer =
|
||||
contract.outcomeType === 'FREE_RESPONSE' && contract.answers
|
||||
? contract.answers.find((answer) => answer.id === bet.outcome)
|
||||
? contract.answers.find((answer) => answer.id === bet?.outcome)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const comments = await getValues<Comment>(
|
||||
firestore.collection('contracts').doc(contractId).collection('comments')
|
||||
|
|
|
@ -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 <LoadingIndicator />
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)}
|
||||
</Row>
|
||||
) : volume > 0 ? (
|
||||
<Row>{volumeLabel}</Row>
|
||||
) : (
|
||||
<Row className="gap-1">
|
||||
{/* <DatabaseIcon className="h-5 w-5" /> */}
|
||||
{volumeLabel}
|
||||
</Row>
|
||||
<NewContractBadge />
|
||||
)}
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
|
@ -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<string, Comment>
|
||||
}
|
||||
|
||||
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,9 +286,9 @@ export function getAllContractActivityItems(
|
|||
]
|
||||
: [{ type: 'description', id: '0', contract }]
|
||||
|
||||
if (outcomeType === 'FREE_RESPONSE') {
|
||||
items.push(
|
||||
...(outcomeType === 'FREE_RESPONSE'
|
||||
? getAnswerGroups(
|
||||
...getAnswerGroups(
|
||||
contract as FullContract<DPM, FreeResponse>,
|
||||
bets,
|
||||
comments,
|
||||
|
@ -292,13 +299,41 @@ export function getAllContractActivityItems(
|
|||
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,
|
||||
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
|
||||
|
|
|
@ -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,9 @@ 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'
|
||||
|
||||
export function FeedItems(props: {
|
||||
contract: Contract
|
||||
|
@ -104,24 +108,30 @@ function FeedItem(props: { item: ActivityItem }) {
|
|||
return <FeedClose {...item} />
|
||||
case 'resolve':
|
||||
return <FeedResolve {...item} />
|
||||
case 'commentInput':
|
||||
return <CommentInput {...item} />
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Avatar
|
||||
|
@ -144,7 +154,7 @@ export function FeedComment(props: {
|
|||
{' '}
|
||||
of{' '}
|
||||
<OutcomeLabel
|
||||
outcome={outcome}
|
||||
outcome={outcome ? outcome : ''}
|
||||
contract={contract}
|
||||
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: {
|
||||
contract: Contract
|
||||
bet: Bet
|
||||
|
@ -185,14 +267,12 @@ export function FeedBet(props: {
|
|||
const { id, amount, outcome, createdTime, userId } = bet
|
||||
const user = useUser()
|
||||
const isSelf = user?.id === userId
|
||||
|
||||
// You can comment if your bet was posted in the last hour
|
||||
const canComment = isSelf && Date.now() - createdTime < 60 * 60 * 1000
|
||||
const canComment = canCommentOnBet(userId, createdTime, user)
|
||||
|
||||
const [comment, setComment] = useState('')
|
||||
async function submitComment() {
|
||||
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'
|
||||
|
@ -307,19 +387,17 @@ export function FeedQuestion(props: {
|
|||
contractPath?: string
|
||||
}) {
|
||||
const { contract, showDescription } = props
|
||||
const { creatorName, creatorUsername, question, resolution, outcomeType } =
|
||||
contract
|
||||
const {
|
||||
creatorName,
|
||||
creatorUsername,
|
||||
question,
|
||||
outcomeType,
|
||||
volume,
|
||||
createdTime,
|
||||
} = contract
|
||||
const { volumeLabel } = contractMetrics(contract)
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
// const closeMessage =
|
||||
// contract.isResolved || !contract.closeTime ? null : (
|
||||
// <>
|
||||
// <span className="mx-2">•</span>
|
||||
// {contract.closeTime > Date.now() ? 'Closes' : 'Closed'}
|
||||
// <RelativeTimestamp time={contract.closeTime || 0} />
|
||||
// </>
|
||||
// )
|
||||
const isNew = createdTime > Date.now() - DAY_MS
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -336,10 +414,15 @@ export function FeedQuestion(props: {
|
|||
/>{' '}
|
||||
asked
|
||||
{/* 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}
|
||||
{/* {closeMessage} */}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4">
|
||||
<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 }) {
|
||||
const { contract } = props
|
||||
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">
|
||||
{user.website && (
|
||||
<SiteLink
|
||||
href={user.website.replace('https://manifold.markets/', '')}
|
||||
href={
|
||||
'https://' +
|
||||
user.website.replace('http://', '').replace('https://', '')
|
||||
}
|
||||
>
|
||||
<Row className="items-center gap-1">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
|
|
|
@ -8,6 +8,14 @@ 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'
|
||||
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
||||
import { trackLatency } from '../lib/firebase/tracking'
|
||||
|
||||
const MAX_FEED_CONTRACTS = 75
|
||||
|
||||
|
@ -29,8 +37,15 @@ export const useAlgoFeed = (
|
|||
|
||||
const [algoFeed, setAlgoFeed] = useState<Contract[]>([])
|
||||
|
||||
const getTime = useTimeSinceFirstRender()
|
||||
|
||||
useEffect(() => {
|
||||
if (initialContracts && initialBets && initialComments) {
|
||||
if (
|
||||
initialContracts &&
|
||||
initialBets &&
|
||||
initialComments &&
|
||||
yourBetContractIds
|
||||
) {
|
||||
const eligibleContracts = initialContracts.filter(
|
||||
(c) => !c.isResolved && (c.closeTime ?? Infinity) > Date.now()
|
||||
)
|
||||
|
@ -42,6 +57,7 @@ export const useAlgoFeed = (
|
|||
seenContracts
|
||||
)
|
||||
setAlgoFeed(contracts)
|
||||
trackLatency('feed', getTime())
|
||||
}
|
||||
}, [
|
||||
initialBets,
|
||||
|
@ -49,6 +65,7 @@ export const useAlgoFeed = (
|
|||
initialContracts,
|
||||
seenContracts,
|
||||
yourBetContractIds,
|
||||
getTime,
|
||||
])
|
||||
|
||||
return algoFeed
|
||||
|
@ -120,11 +137,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 +151,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 +193,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)
|
||||
|
|
|
@ -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<Contract[] | undefined>()
|
||||
const [activeContracts, setActiveContracts] = useState<
|
||||
Contract[] | undefined
|
||||
>()
|
||||
const [newContracts, setNewContracts] = useState<Contract[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
return listenForActiveContracts(setContracts)
|
||||
return listenForActiveContracts(setActiveContracts)
|
||||
}, [])
|
||||
|
||||
return contracts
|
||||
useEffect(() => {
|
||||
return listenForNewContracts(setNewContracts)
|
||||
}, [])
|
||||
|
||||
if (!activeContracts || !newContracts) return undefined
|
||||
|
||||
return [...activeContracts, ...newContracts]
|
||||
}
|
||||
|
||||
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) => {
|
||||
const [contractIds, setContractIds] = useState<string[]>([])
|
||||
const [contractIds, setContractIds] = useState<string[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
const key = `user-bet-contractIds-${userId}`
|
||||
const userBetContractJson = localStorage.getItem(key)
|
||||
if (userBetContractJson) {
|
||||
setContractIds(JSON.parse(userBetContractJson))
|
||||
}
|
||||
}
|
||||
}, [userId])
|
||||
|
||||
return contractIds
|
||||
|
|
|
@ -19,16 +19,16 @@ export const MAX_COMMENT_LENGTH = 10000
|
|||
|
||||
export async function createComment(
|
||||
contractId: string,
|
||||
betId: 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 = {
|
||||
id: ref.id,
|
||||
contractId,
|
||||
betId,
|
||||
userId: commenter.id,
|
||||
text: text.slice(0, MAX_COMMENT_LENGTH),
|
||||
createdTime: Date.now(),
|
||||
|
@ -36,7 +36,9 @@ export async function createComment(
|
|||
userUsername: commenter.username,
|
||||
userAvatarUrl: commenter.avatarUrl,
|
||||
}
|
||||
|
||||
if (betId) {
|
||||
comment.betId = betId
|
||||
}
|
||||
return await setDoc(ref, comment)
|
||||
}
|
||||
|
||||
|
@ -67,8 +69,10 @@ export function listenForComments(
|
|||
export function mapCommentsByBetId(comments: Comment[]) {
|
||||
const map: Record<string, Comment> = {}
|
||||
for (const comment of comments) {
|
||||
if (comment.betId) {
|
||||
map[comment.betId] = comment
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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(
|
||||
contractId: string,
|
||||
setContract: (contract: Contract | null) => void
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -250,7 +250,10 @@ function ContractTopTrades(props: {
|
|||
const topBettor = useUserById(betsById[topBetId]?.userId)
|
||||
|
||||
// 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 (
|
||||
<div className="mt-12 max-w-sm">
|
||||
|
|
Loading…
Reference in New Issue
Block a user