Rich text to plaintext descriptions, other ui changes
This commit is contained in:
parent
72f497b969
commit
6c4e870d5d
151
common/contract-details.ts
Normal file
151
common/contract-details.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import { Challenge } from './challenge'
|
||||||
|
import { BinaryContract, Contract } from './contract'
|
||||||
|
import { getFormattedMappedValue } from './pseudo-numeric'
|
||||||
|
import { getProbability } from './calculate'
|
||||||
|
import { richTextToString } from './util/parse'
|
||||||
|
import { getCpmmProbability } from './calculate-cpmm'
|
||||||
|
import { getDpmProbability } from './calculate-dpm'
|
||||||
|
import { formatMoney, formatPercent } from './util/format'
|
||||||
|
|
||||||
|
export function contractMetrics(contract: Contract) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const dayjs = require('dayjs')
|
||||||
|
const { createdTime, resolutionTime, isResolved } = contract
|
||||||
|
|
||||||
|
const createdDate = dayjs(createdTime).format('MMM D')
|
||||||
|
|
||||||
|
const resolvedDate = isResolved
|
||||||
|
? dayjs(resolutionTime).format('MMM D')
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const volumeLabel = `${formatMoney(contract.volume)} bet`
|
||||||
|
|
||||||
|
return { volumeLabel, createdDate, resolvedDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
// String version of the above, to send to the OpenGraph image generator
|
||||||
|
export function contractTextDetails(contract: Contract) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const dayjs = require('dayjs')
|
||||||
|
const { closeTime, tags } = contract
|
||||||
|
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
||||||
|
|
||||||
|
const hashtags = tags.map((tag) => `#${tag}`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
||||||
|
(closeTime
|
||||||
|
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
||||||
|
closeTime
|
||||||
|
).format('MMM D, h:mma')}`
|
||||||
|
: '') +
|
||||||
|
` • ${volumeLabel}` +
|
||||||
|
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBinaryProb(contract: BinaryContract) {
|
||||||
|
const { pool, resolutionProbability, mechanism } = contract
|
||||||
|
|
||||||
|
return (
|
||||||
|
resolutionProbability ??
|
||||||
|
(mechanism === 'cpmm-1'
|
||||||
|
? getCpmmProbability(pool, contract.p)
|
||||||
|
: getDpmProbability(contract.totalShares))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOpenGraphProps = (contract: Contract) => {
|
||||||
|
const {
|
||||||
|
resolution,
|
||||||
|
question,
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
outcomeType,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
description: desc,
|
||||||
|
} = contract
|
||||||
|
const probPercent =
|
||||||
|
outcomeType === 'BINARY'
|
||||||
|
? formatPercent(getBinaryProb(contract))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const numericValue =
|
||||||
|
outcomeType === 'PSEUDO_NUMERIC'
|
||||||
|
? getFormattedMappedValue(contract)(getProbability(contract))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
|
||||||
|
|
||||||
|
const description = resolution
|
||||||
|
? `Resolved ${resolution}. ${stringDesc}`
|
||||||
|
: probPercent
|
||||||
|
? `${probPercent} chance. ${stringDesc}`
|
||||||
|
: stringDesc
|
||||||
|
|
||||||
|
return {
|
||||||
|
question,
|
||||||
|
probability: probPercent,
|
||||||
|
metadata: contractTextDetails(contract),
|
||||||
|
creatorName,
|
||||||
|
creatorUsername,
|
||||||
|
creatorAvatarUrl,
|
||||||
|
description,
|
||||||
|
numericValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OgCardProps = {
|
||||||
|
question: string
|
||||||
|
probability?: string
|
||||||
|
metadata: string
|
||||||
|
creatorName: string
|
||||||
|
creatorUsername: string
|
||||||
|
creatorAvatarUrl?: string
|
||||||
|
numericValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
||||||
|
const {
|
||||||
|
creatorAmount,
|
||||||
|
acceptances,
|
||||||
|
acceptorAmount,
|
||||||
|
creatorOutcome,
|
||||||
|
acceptorOutcome,
|
||||||
|
} = challenge || {}
|
||||||
|
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
|
||||||
|
|
||||||
|
const probabilityParam =
|
||||||
|
props.probability === undefined
|
||||||
|
? ''
|
||||||
|
: `&probability=${encodeURIComponent(props.probability ?? '')}`
|
||||||
|
|
||||||
|
const numericValueParam =
|
||||||
|
props.numericValue === undefined
|
||||||
|
? ''
|
||||||
|
: `&numericValue=${encodeURIComponent(props.numericValue ?? '')}`
|
||||||
|
|
||||||
|
const creatorAvatarUrlParam =
|
||||||
|
props.creatorAvatarUrl === undefined
|
||||||
|
? ''
|
||||||
|
: `&creatorAvatarUrl=${encodeURIComponent(props.creatorAvatarUrl ?? '')}`
|
||||||
|
|
||||||
|
const challengeUrlParams = challenge
|
||||||
|
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
|
||||||
|
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
|
||||||
|
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
// URL encode each of the props, then add them as query params
|
||||||
|
return (
|
||||||
|
`https://manifold-og-image.vercel.app/m.png` +
|
||||||
|
`?question=${encodeURIComponent(props.question)}` +
|
||||||
|
probabilityParam +
|
||||||
|
numericValueParam +
|
||||||
|
`&metadata=${encodeURIComponent(props.metadata)}` +
|
||||||
|
`&creatorName=${encodeURIComponent(props.creatorName)}` +
|
||||||
|
creatorAvatarUrlParam +
|
||||||
|
`&creatorUsername=${encodeURIComponent(props.creatorUsername)}` +
|
||||||
|
challengeUrlParams
|
||||||
|
)
|
||||||
|
}
|
|
@ -63,7 +63,7 @@ service cloud.firestore {
|
||||||
allow read: if userId == request.auth.uid || isAdmin();
|
allow read: if userId == request.auth.uid || isAdmin();
|
||||||
allow update: if (userId == request.auth.uid || isAdmin())
|
allow update: if (userId == request.auth.uid || isAdmin())
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]);
|
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails' ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
match /private-users/{userId}/views/{viewId} {
|
match /private-users/{userId}/views/{viewId} {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { sendTemplateEmail } from './send-email'
|
||||||
import { getPrivateUser, getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
import { getFunctionUrl } from '../../common/api'
|
import { getFunctionUrl } from '../../common/api'
|
||||||
import { richTextToString } from '../../common/util/parse'
|
import { richTextToString } from '../../common/util/parse'
|
||||||
|
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||||
|
|
||||||
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
|
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe')
|
||||||
|
|
||||||
|
@ -390,3 +391,53 @@ export const sendNewAnswerEmail = async (
|
||||||
{ from }
|
{ from }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendThreeContractsEmail = async (
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
contractsToSend: Contract[]
|
||||||
|
) => {
|
||||||
|
const emailType = 'weekly-trending'
|
||||||
|
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${privateUser.id}&type=${emailType}`
|
||||||
|
if (!privateUser || !privateUser.email) return
|
||||||
|
await sendTemplateEmail(
|
||||||
|
privateUser.email,
|
||||||
|
contractsToSend[0].question + ' and 2 more questions for you.',
|
||||||
|
'3-trending-markets',
|
||||||
|
{
|
||||||
|
question1Title: contractsToSend[0].question,
|
||||||
|
question1Description: getTextDescription(contractsToSend[0]),
|
||||||
|
question1Link: contractUrl(contractsToSend[0]),
|
||||||
|
question1ImgSrc: imageSourceUrl(contractsToSend[0]),
|
||||||
|
question2Title: contractsToSend[1].question,
|
||||||
|
question2Description: getTextDescription(contractsToSend[1]),
|
||||||
|
question2Link: contractUrl(contractsToSend[1]),
|
||||||
|
question2ImgSrc: imageSourceUrl(contractsToSend[1]),
|
||||||
|
question3Title: contractsToSend[2].question,
|
||||||
|
question3Description: getTextDescription(contractsToSend[2]),
|
||||||
|
question3Link: contractUrl(contractsToSend[2]),
|
||||||
|
question3ImgSrc: imageSourceUrl(contractsToSend[2]),
|
||||||
|
|
||||||
|
unsubscribeLink: unsubscribeUrl,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextDescription(contract: Contract) {
|
||||||
|
const { description } = contract
|
||||||
|
let text = ''
|
||||||
|
if (typeof description === 'string') text = description
|
||||||
|
else text = richTextToString(description)
|
||||||
|
|
||||||
|
if (text.length > 300) {
|
||||||
|
return text.substring(0, 300) + '...'
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function contractUrl(contract: Contract) {
|
||||||
|
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function imageSourceUrl(contract: Contract) {
|
||||||
|
return buildCardUrl(getOpenGraphProps(contract))
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export * from './on-create-comment-on-group'
|
||||||
export * from './on-create-txn'
|
export * from './on-create-txn'
|
||||||
export * from './on-delete-group'
|
export * from './on-delete-group'
|
||||||
export * from './score-contracts'
|
export * from './score-contracts'
|
||||||
|
export * from './weekly-markets-emails'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
'market-comment',
|
'market-comment',
|
||||||
'market-answer',
|
'market-answer',
|
||||||
'generic',
|
'generic',
|
||||||
|
'weekly-trending',
|
||||||
].includes(type)
|
].includes(type)
|
||||||
) {
|
) {
|
||||||
res.status(400).send('Invalid type parameter.')
|
res.status(400).send('Invalid type parameter.')
|
||||||
|
@ -49,6 +50,9 @@ export const unsubscribe: EndpointDefinition = {
|
||||||
...(type === 'generic' && {
|
...(type === 'generic' && {
|
||||||
unsubscribedFromGenericEmails: true,
|
unsubscribedFromGenericEmails: true,
|
||||||
}),
|
}),
|
||||||
|
...(type === 'weekly-trending' && {
|
||||||
|
unsubscribedFromWeeklyTrendingEmails: true,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(id).update(update)
|
await firestore.collection('private-users').doc(id).update(update)
|
||||||
|
|
|
@ -2,13 +2,20 @@ import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { getAllPrivateUsers, getPrivateUser, getValues, log } from './utils'
|
import {
|
||||||
import { sendTemplateEmail } from './send-email'
|
getAllPrivateUsers,
|
||||||
import { createRNG, shuffle } from '../../common/util/random'
|
getPrivateUser,
|
||||||
|
getValues,
|
||||||
|
isProd,
|
||||||
|
log,
|
||||||
|
} from './utils'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
import { sendThreeContractsEmail } from './emails'
|
||||||
|
import { createRNG, shuffle } from '../../common/util/random'
|
||||||
|
|
||||||
export const weeklyMarketsEmails = functions.pubsub
|
export const weeklyMarketsEmails = functions
|
||||||
.schedule('every 1 minutes')
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
.pubsub.schedule('every 1 minutes')
|
||||||
.onRun(async () => {
|
.onRun(async () => {
|
||||||
await sendTrendingMarketsEmailsToAllUsers()
|
await sendTrendingMarketsEmailsToAllUsers()
|
||||||
})
|
})
|
||||||
|
@ -27,10 +34,11 @@ async function getTrendingContracts() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendTrendingMarketsEmailsToAllUsers() {
|
async function sendTrendingMarketsEmailsToAllUsers() {
|
||||||
const numMarketsToSend = 3
|
|
||||||
// const privateUsers = await getAllPrivateUsers()
|
// const privateUsers = await getAllPrivateUsers()
|
||||||
// uses dev ian's private user for testing
|
// uses dev ian's private user for testing
|
||||||
const privateUser = await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')
|
const privateUser = await getPrivateUser(
|
||||||
|
isProd() ? 'AJwLWoo3xue32XIiAVrL5SyR1WB2' : '6hHpzvRG0pMq8PNJs7RZj2qlZGn2'
|
||||||
|
)
|
||||||
const privateUsers = filterDefined([privateUser])
|
const privateUsers = filterDefined([privateUser])
|
||||||
// get all users that haven't unsubscribed from weekly emails
|
// get all users that haven't unsubscribed from weekly emails
|
||||||
const privateUsersToSendEmailsTo = privateUsers.filter((user) => {
|
const privateUsersToSendEmailsTo = privateUsers.filter((user) => {
|
||||||
|
@ -45,46 +53,17 @@ async function sendTrendingMarketsEmailsToAllUsers() {
|
||||||
const contractsAvailableToSend = trendingContracts.filter((contract) => {
|
const contractsAvailableToSend = trendingContracts.filter((contract) => {
|
||||||
return !contract.uniqueBettorIds?.includes(privateUser.id)
|
return !contract.uniqueBettorIds?.includes(privateUser.id)
|
||||||
})
|
})
|
||||||
if (contractsAvailableToSend.length < numMarketsToSend) {
|
if (contractsAvailableToSend.length < 3) {
|
||||||
log('not enough new, unbet-on contracts to send to user', privateUser.id)
|
log('not enough new, unbet-on contracts to send to user', privateUser.id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// choose random subset of contracts to send to user
|
// choose random subset of contracts to send to user
|
||||||
const contractsToSend = chooseRandomSubset(
|
const contractsToSend = chooseRandomSubset(contractsAvailableToSend, 3)
|
||||||
contractsAvailableToSend,
|
|
||||||
numMarketsToSend
|
|
||||||
)
|
|
||||||
|
|
||||||
await sendTemplateEmail(
|
await sendThreeContractsEmail(privateUser, contractsToSend)
|
||||||
privateUser.email,
|
|
||||||
contractsToSend[0].question,
|
|
||||||
'3-trending-markets',
|
|
||||||
{
|
|
||||||
question1title: contractsToSend[0].question,
|
|
||||||
question1Description: getTextDescription(contractsToSend[0]),
|
|
||||||
question1link: contractUrl(contractsToSend[0]),
|
|
||||||
question2title: contractsToSend[1].question,
|
|
||||||
question2Description: getTextDescription(contractsToSend[1]),
|
|
||||||
question2link: contractUrl(contractsToSend[1]),
|
|
||||||
question3title: contractsToSend[2].question,
|
|
||||||
question3Description: getTextDescription(contractsToSend[2]),
|
|
||||||
question3link: contractUrl(contractsToSend[2]),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextDescription(contract: Contract) {
|
|
||||||
// if the contract.description is of type string, return it, otherwise return the text of the json content
|
|
||||||
return typeof contract.description === 'string'
|
|
||||||
? contract.description
|
|
||||||
: contract.description.text ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function contractUrl(contract: Contract) {
|
|
||||||
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseRandomSubset(contracts: Contract[], count: number) {
|
function chooseRandomSubset(contracts: Contract[], count: number) {
|
||||||
const fiveMinutes = 5 * 60 * 1000
|
const fiveMinutes = 5 * 60 * 1000
|
||||||
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
||||||
|
|
|
@ -1,61 +1,7 @@
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { Challenge } from 'common/challenge'
|
import { Challenge } from 'common/challenge'
|
||||||
|
import { buildCardUrl, OgCardProps } from 'common/contract-details'
|
||||||
export type OgCardProps = {
|
|
||||||
question: string
|
|
||||||
probability?: string
|
|
||||||
metadata: string
|
|
||||||
creatorName: string
|
|
||||||
creatorUsername: string
|
|
||||||
creatorAvatarUrl?: string
|
|
||||||
numericValue?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
|
|
||||||
const {
|
|
||||||
creatorAmount,
|
|
||||||
acceptances,
|
|
||||||
acceptorAmount,
|
|
||||||
creatorOutcome,
|
|
||||||
acceptorOutcome,
|
|
||||||
} = challenge || {}
|
|
||||||
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
|
|
||||||
|
|
||||||
const probabilityParam =
|
|
||||||
props.probability === undefined
|
|
||||||
? ''
|
|
||||||
: `&probability=${encodeURIComponent(props.probability ?? '')}`
|
|
||||||
|
|
||||||
const numericValueParam =
|
|
||||||
props.numericValue === undefined
|
|
||||||
? ''
|
|
||||||
: `&numericValue=${encodeURIComponent(props.numericValue ?? '')}`
|
|
||||||
|
|
||||||
const creatorAvatarUrlParam =
|
|
||||||
props.creatorAvatarUrl === undefined
|
|
||||||
? ''
|
|
||||||
: `&creatorAvatarUrl=${encodeURIComponent(props.creatorAvatarUrl ?? '')}`
|
|
||||||
|
|
||||||
const challengeUrlParams = challenge
|
|
||||||
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
|
|
||||||
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
|
|
||||||
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// URL encode each of the props, then add them as query params
|
|
||||||
return (
|
|
||||||
`https://manifold-og-image.vercel.app/m.png` +
|
|
||||||
`?question=${encodeURIComponent(props.question)}` +
|
|
||||||
probabilityParam +
|
|
||||||
numericValueParam +
|
|
||||||
`&metadata=${encodeURIComponent(props.metadata)}` +
|
|
||||||
`&creatorName=${encodeURIComponent(props.creatorName)}` +
|
|
||||||
creatorAvatarUrlParam +
|
|
||||||
`&creatorUsername=${encodeURIComponent(props.creatorUsername)}` +
|
|
||||||
challengeUrlParams
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SEO(props: {
|
export function SEO(props: {
|
||||||
title: string
|
title: string
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
import { getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
|
||||||
import { richTextToString } from 'common/util/parse'
|
|
||||||
import { contractTextDetails } from 'web/components/contract/contract-details'
|
|
||||||
import { getFormattedMappedValue } from 'common/pseudo-numeric'
|
|
||||||
import { getProbability } from 'common/calculate'
|
|
||||||
|
|
||||||
export const getOpenGraphProps = (contract: Contract) => {
|
|
||||||
const {
|
|
||||||
resolution,
|
|
||||||
question,
|
|
||||||
creatorName,
|
|
||||||
creatorUsername,
|
|
||||||
outcomeType,
|
|
||||||
creatorAvatarUrl,
|
|
||||||
description: desc,
|
|
||||||
} = contract
|
|
||||||
const probPercent =
|
|
||||||
outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined
|
|
||||||
|
|
||||||
const numericValue =
|
|
||||||
outcomeType === 'PSEUDO_NUMERIC'
|
|
||||||
? getFormattedMappedValue(contract)(getProbability(contract))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
|
|
||||||
|
|
||||||
const description = resolution
|
|
||||||
? `Resolved ${resolution}. ${stringDesc}`
|
|
||||||
: probPercent
|
|
||||||
? `${probPercent} chance. ${stringDesc}`
|
|
||||||
: stringDesc
|
|
||||||
|
|
||||||
return {
|
|
||||||
question,
|
|
||||||
probability: probPercent,
|
|
||||||
metadata: contractTextDetails(contract),
|
|
||||||
creatorName,
|
|
||||||
creatorUsername,
|
|
||||||
creatorAvatarUrl,
|
|
||||||
description,
|
|
||||||
numericValue,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,11 +9,7 @@ import {
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { UserLink } from '../user-page'
|
import { UserLink } from '../user-page'
|
||||||
import {
|
import { Contract, updateContract } from 'web/lib/firebase/contracts'
|
||||||
Contract,
|
|
||||||
contractMetrics,
|
|
||||||
updateContract,
|
|
||||||
} from 'web/lib/firebase/contracts'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
|
@ -35,6 +31,7 @@ import { SiteLink } from 'web/components/site-link'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { insertContent } from '../editor/utils'
|
import { insertContent } from '../editor/utils'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { contractMetrics } from 'common/contract-details'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -245,25 +242,6 @@ export function ContractDetails(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String version of the above, to send to the OpenGraph image generator
|
|
||||||
export function contractTextDetails(contract: Contract) {
|
|
||||||
const { closeTime, tags } = contract
|
|
||||||
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
|
|
||||||
|
|
||||||
const hashtags = tags.map((tag) => `#${tag}`)
|
|
||||||
|
|
||||||
return (
|
|
||||||
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
|
|
||||||
(closeTime
|
|
||||||
? ` • ${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
|
|
||||||
closeTime
|
|
||||||
).format('MMM D, h:mma')}`
|
|
||||||
: '') +
|
|
||||||
` • ${volumeLabel}` +
|
|
||||||
(hashtags.length > 0 ? ` • ${hashtags.join(' ')}` : '')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditableCloseDate(props: {
|
function EditableCloseDate(props: {
|
||||||
closeTime: number
|
closeTime: number
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { useState } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
import { useUserContractBets } from 'web/hooks/use-user-bets'
|
||||||
import { placeBet } from 'web/lib/firebase/api'
|
import { placeBet } from 'web/lib/firebase/api'
|
||||||
import { getBinaryProb, getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
import { getBinaryProbPercent } from 'web/lib/firebase/contracts'
|
||||||
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon'
|
||||||
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
import TriangleFillIcon from 'web/lib/icons/triangle-fill-icon'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
|
@ -34,6 +34,7 @@ import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { formatNumericProbability } from 'common/pseudo-numeric'
|
import { formatNumericProbability } from 'common/pseudo-numeric'
|
||||||
import { useUnfilledBets } from 'web/hooks/use-bets'
|
import { useUnfilledBets } from 'web/hooks/use-bets'
|
||||||
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
|
||||||
const BET_SIZE = 10
|
const BET_SIZE = 10
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ import clsx from 'clsx'
|
||||||
import { OutcomeLabel } from '../outcome-label'
|
import { OutcomeLabel } from '../outcome-label'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
contractMetrics,
|
|
||||||
contractPath,
|
contractPath,
|
||||||
tradingAllowed,
|
tradingAllowed,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
|
@ -38,6 +37,7 @@ import { FeedLiquidity } from './feed-liquidity'
|
||||||
import { SignUpPrompt } from '../sign-up-prompt'
|
import { SignUpPrompt } from '../sign-up-prompt'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
||||||
|
import { contractMetrics } from 'common/contract-details'
|
||||||
|
|
||||||
export function FeedItems(props: {
|
export function FeedItems(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { MAX_FEED_CONTRACTS } from 'common/recommended-contracts'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Comment } from 'common/comment'
|
import { Comment } from 'common/comment'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { getBinaryProb } from 'common/contract-details'
|
||||||
|
|
||||||
export const contracts = coll<Contract>('contracts')
|
export const contracts = coll<Contract>('contracts')
|
||||||
|
|
||||||
|
@ -50,20 +51,6 @@ export function contractUrl(contract: Contract) {
|
||||||
return `https://${ENV_CONFIG.domain}${contractPath(contract)}`
|
return `https://${ENV_CONFIG.domain}${contractPath(contract)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contractMetrics(contract: Contract) {
|
|
||||||
const { createdTime, resolutionTime, isResolved } = contract
|
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
|
||||||
|
|
||||||
const resolvedDate = isResolved
|
|
||||||
? dayjs(resolutionTime).format('MMM D')
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const volumeLabel = `${formatMoney(contract.volume)} bet`
|
|
||||||
|
|
||||||
return { volumeLabel, createdDate, resolvedDate }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function contractPool(contract: Contract) {
|
export function contractPool(contract: Contract) {
|
||||||
return contract.mechanism === 'cpmm-1'
|
return contract.mechanism === 'cpmm-1'
|
||||||
? formatMoney(contract.totalLiquidity)
|
? formatMoney(contract.totalLiquidity)
|
||||||
|
@ -72,17 +59,6 @@ export function contractPool(contract: Contract) {
|
||||||
: 'Empty pool'
|
: 'Empty pool'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBinaryProb(contract: BinaryContract) {
|
|
||||||
const { pool, resolutionProbability, mechanism } = contract
|
|
||||||
|
|
||||||
return (
|
|
||||||
resolutionProbability ??
|
|
||||||
(mechanism === 'cpmm-1'
|
|
||||||
? getCpmmProbability(pool, contract.p)
|
|
||||||
: getDpmProbability(contract.totalShares))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBinaryProbPercent(contract: BinaryContract) {
|
export function getBinaryProbPercent(contract: BinaryContract) {
|
||||||
return formatPercent(getBinaryProb(contract))
|
return formatPercent(getBinaryProb(contract))
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,12 +36,12 @@ import { AlertBox } from 'web/components/alert-box'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
|
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { listUsers } from 'web/lib/firebase/users'
|
import { listUsers } from 'web/lib/firebase/users'
|
||||||
import { FeedComment } from 'web/components/feed/feed-comments'
|
import { FeedComment } from 'web/components/feed/feed-comments'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { FeedBet } from 'web/components/feed/feed-bets'
|
import { FeedBet } from 'web/components/feed/feed-bets'
|
||||||
|
import { getOpenGraphProps } from 'common/contract-details'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: {
|
export async function getStaticPropz(props: {
|
||||||
|
|
|
@ -28,11 +28,11 @@ import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||||
import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
import { Bet, listAllBets } from 'web/lib/firebase/bets'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
import { getOpenGraphProps } from 'web/components/contract/contract-card-preview'
|
|
||||||
import Custom404 from 'web/pages/404'
|
import Custom404 from 'web/pages/404'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { BinaryContract } from 'common/contract'
|
import { BinaryContract } from 'common/contract'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
|
import { getOpenGraphProps } from 'common/contract-details'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user