Type description as JSON, fix string-based logic

- Delete make-predictions.tsx
- Delete feed logic that showed descriptions
This commit is contained in:
Sinclair Chen 2022-06-28 18:08:37 -05:00
parent 322ffd7c36
commit 5b29d32cfa
12 changed files with 31 additions and 319 deletions

View File

@ -1,5 +1,6 @@
import { Answer } from './answer'
import { Fees } from './fees'
import { JSONContent } from '@tiptap/core'
export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = Binary | FreeResponse | Numeric
@ -19,7 +20,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
creatorAvatarUrl?: string
question: string
description: string // More info about what the contract is about
description: string | JSONContent // More info about what the contract is about
tags: string[]
lowercaseTags: string[]
visibility: 'public' | 'unlisted'
@ -33,7 +34,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
isResolved: boolean
resolutionTime?: number // When the contract creator resolved the market
resolution?: string
resolutionProbability?: number,
resolutionProbability?: number
closeEmailsSent?: number

View File

@ -9,8 +9,9 @@ import {
outcomeType,
} from './contract'
import { User } from './user'
import { parseTags } from './util/parse'
import { parseTags, richTextToString } from './util/parse'
import { removeUndefinedProps } from './util/object'
import { JSONContent } from '@tiptap/core'
export function getNewContract(
id: string,
@ -18,7 +19,7 @@ export function getNewContract(
creator: User,
question: string,
outcomeType: outcomeType,
description: string,
description: JSONContent,
initialProb: number,
ante: number,
closeTime: number,
@ -30,7 +31,11 @@ export function getNewContract(
max: number
) {
const tags = parseTags(
`${question} ${description} ${extraTags.map((tag) => `#${tag}`).join(' ')}`
[
question,
richTextToString(description),
...extraTags.map((tag) => `#${tag}`),
].join(' ')
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
@ -52,7 +57,7 @@ export function getNewContract(
creatorAvatarUrl: creator.avatarUrl,
question: question.trim(),
description: description.trim(),
description,
tags,
lowercaseTags,
visibility: 'public',

View File

@ -1,4 +1,6 @@
import { MAX_TAG_LENGTH } from '../contract'
import { generateText, JSONContent } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
export function parseTags(text: string) {
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
@ -27,3 +29,7 @@ export function parseWordsAsTags(text: string) {
.join(' ')
return parseTags(taggedText)
}
export function richTextToString(text: JSONContent | string) {
return typeof text === 'string' ? text : generateText(text, [StarterKit])
}

View File

@ -2,6 +2,7 @@ import * as functions from 'firebase-functions'
import { getUser } from './utils'
import { createNotification } from './create-notification'
import { Contract } from '../../common/contract'
import { richTextToString } from 'common/util/parse'
export const onCreateContract = functions.firestore
.document('contracts/{contractId}')
@ -18,7 +19,7 @@ export const onCreateContract = functions.firestore
'created',
contractCreator,
eventId,
contract.description,
richTextToString(contract.description),
contract
)
})

View File

@ -5,12 +5,12 @@ import Textarea from 'react-expanding-textarea'
import { CATEGORY_LIST } from '../../../common/categories'
import { Contract } from 'common/contract'
import { parseTags } from 'common/util/parse'
import { parseTags, richTextToString } from 'common/util/parse'
import { useAdmin } from 'web/hooks/use-admin'
import { updateContract } from 'web/lib/firebase/contracts'
import { Row } from '../layout/row'
import { Linkify } from '../linkify'
import { TagsList } from '../tags-list'
import { Content } from '../editor'
export function ContractDescription(props: {
contract: Contract
@ -23,7 +23,9 @@ export function ContractDescription(props: {
// Append the new description (after a newline)
async function saveDescription(newText: string) {
const newDescription = `${contract.description}\n\n${newText}`.trim()
// TODO: implement appending rich text description
const textDescription = richTextToString(contract.description)
const newDescription = `${textDescription}\n\n${newText}`.trim()
const tags = parseTags(
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
)
@ -50,7 +52,7 @@ export function ContractDescription(props: {
className
)}
>
<Linkify text={contract.description} />
<Content content={contract.description} />
{categories.length > 0 && (
<div className="mt-4">

View File

@ -37,7 +37,6 @@ export type DescriptionItem = BaseActivityItem & {
export type QuestionItem = BaseActivityItem & {
type: 'question'
showDescription: boolean
contractPath?: string
}

View File

@ -31,7 +31,6 @@ import { FeedAnswerCommentGroup } from 'web/components/feed/feed-answer-comment-
import {
FeedCommentThread,
CommentInput,
TruncatedComment,
} from 'web/components/feed/feed-comments'
import { FeedBet } from 'web/components/feed/feed-bets'
import { NumericContract } from 'common/contract'
@ -101,10 +100,9 @@ export function FeedItem(props: { item: ActivityItem }) {
export function FeedQuestion(props: {
contract: Contract
showDescription: boolean
contractPath?: string
}) {
const { contract, showDescription } = props
const { contract } = props
const {
creatorName,
creatorUsername,
@ -160,13 +158,6 @@ export function FeedQuestion(props: {
/>
)}
</Col>
{showDescription && (
<TruncatedComment
comment={contract.description}
moreHref={contractPath(contract)}
shouldTruncate
/>
)}
</div>
</div>
)

View File

@ -43,6 +43,7 @@ import { AlertBox } from 'web/components/alert-box'
import { useTracking } from 'web/hooks/use-tracking'
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
import { useLiquidity } from 'web/hooks/use-liquidity'
import { richTextToString } from 'common/util/parse'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
@ -181,7 +182,7 @@ export function ContractPageContent(
{ogCardProps && (
<SEO
title={question}
description={ogCardProps.description}
description={richTextToString(ogCardProps.description)}
url={`/${props.username}/${props.slug}`}
ogCardProps={ogCardProps}
/>

View File

@ -6,6 +6,7 @@ import { Contract } from 'common/contract'
import { User } from 'common/user'
import { removeUndefinedProps } from 'common/util/object'
import { ENV_CONFIG } from 'common/envs/constants'
import { JSONContent } from '@tiptap/core'
export type LiteMarket = {
// Unique identifer for this market
@ -20,7 +21,7 @@ export type LiteMarket = {
// Market attributes. All times are in milliseconds since epoch
closeTime?: number
question: string
description: string
description: string | JSONContent
tags: string[]
url: string
outcomeType: string

View File

@ -34,7 +34,6 @@ export default function ContractSearchFirestore(props: {
let matches = (contracts ?? []).filter(
(c) =>
check(c.question) ||
check(c.description) ||
check(c.creatorName) ||
check(c.creatorUsername) ||
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) ||

View File

@ -138,7 +138,6 @@ export default function GroupPage(props: {
? contracts.filter(
(c) =>
checkAgainstQuery(query, c.question) ||
checkAgainstQuery(query, c.description || '') ||
checkAgainstQuery(query, c.creatorName) ||
checkAgainstQuery(query, c.creatorUsername)
)
@ -499,7 +498,6 @@ function AddContractButton(props: { group: Group; user: User }) {
]).filter(
(c) =>
checkAgainstQuery(query, c.question) ||
checkAgainstQuery(query, c.description) ||
checkAgainstQuery(query, c.tags.flat().join(' '))
)
const debouncedQuery = debounce(setQuery, 50)

View File

@ -1,292 +0,0 @@
import clsx from 'clsx'
import dayjs from 'dayjs'
import Link from 'next/link'
import { useState } from 'react'
import Textarea from 'react-expanding-textarea'
import { getProbability } from 'common/calculate'
import { BinaryContract } from 'common/contract'
import { parseWordsAsTags } from 'common/util/parse'
import { BuyAmountInput } from 'web/components/amount-input'
import { InfoTooltip } from 'web/components/info-tooltip'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { Spacer } from 'web/components/layout/spacer'
import { Linkify } from 'web/components/linkify'
import { Page } from 'web/components/page'
import { Title } from 'web/components/title'
import { useUser } from 'web/hooks/use-user'
import { createMarket } from 'web/lib/firebase/api-call'
import { contractPath } from 'web/lib/firebase/contracts'
type Prediction = {
question: string
description: string
initialProb: number
createdUrl?: string
}
function toPrediction(contract: BinaryContract): Prediction {
const startProb = getProbability(contract)
return {
question: contract.question,
description: contract.description,
initialProb: startProb * 100,
createdUrl: contractPath(contract),
}
}
function PredictionRow(props: { prediction: Prediction }) {
const { prediction } = props
return (
<Row className="justify-between gap-4 p-4 hover:bg-gray-300">
<Col className="justify-between">
<div className="mb-2 font-medium text-indigo-700">
<Linkify text={prediction.question} />
</div>
<div className="text-sm text-gray-500">{prediction.description}</div>
</Col>
{/* Initial probability */}
<div className="ml-auto">
<div className="text-3xl">
<div className="text-primary">
{prediction.initialProb.toFixed(0)}%
<div className="text-lg">chance</div>
</div>
</div>
</div>
{/* Current probability; hidden for now */}
{/* <div>
<div className="text-3xl">
<div className="text-primary">
{prediction.initialProb}%<div className="text-lg">chance</div>
</div>
</div>
</div> */}
</Row>
)
}
function PredictionList(props: { predictions: Prediction[] }) {
const { predictions } = props
return (
<Col className="divide-y divide-gray-300 rounded-md border border-gray-300">
{predictions.map((prediction) =>
prediction.createdUrl ? (
<Link href={prediction.createdUrl}>
<a>
<PredictionRow
key={prediction.question}
prediction={prediction}
/>
</a>
</Link>
) : (
<PredictionRow key={prediction.question} prediction={prediction} />
)
)}
</Col>
)
}
const TEST_VALUE = `1. Biden approval rating (as per 538) is greater than 50%: 80%
2. Court packing is clearly going to happen (new justices don't have to be appointed by end of year): 5%
3. Yang is New York mayor: 80%
4. Newsom recalled as CA governor: 5%
5. At least $250 million in damage from BLM protests this year: 30%
6. Significant capital gains tax hike (above 30% for highest bracket): 20%`
export default function MakePredictions() {
const user = useUser()
const [predictionsString, setPredictionsString] = useState('')
const [description, setDescription] = useState('')
const [tags, setTags] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [createdContracts, setCreatedContracts] = useState<BinaryContract[]>([])
const [ante, setAnte] = useState<number | undefined>(100)
const [anteError, setAnteError] = useState<string | undefined>()
// By default, close the market a week from today
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DDT23:59')
const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday)
const closeTime = closeDate ? dayjs(closeDate).valueOf() : undefined
const bulkPlaceholder = `e.g.
${TEST_VALUE}
...
`
const predictions: Prediction[] = []
// Parse bulkContracts, then run createContract for each
const lines = predictionsString ? predictionsString.split('\n') : []
for (const line of lines) {
// Parse line with regex
const matches = line.match(/^(.*):\s*(\d+)%\s*$/) || ['', '', '']
const [_, question, prob] = matches
if (!question || !prob) {
console.error('Invalid prediction: ', line)
continue
}
predictions.push({
question,
description,
initialProb: parseInt(prob),
})
}
async function createMarkets() {
if (!user) {
// TODO: Convey error with snackbar/toast
console.error('You need to be signed in!')
return
}
setIsSubmitting(true)
for (const prediction of predictions) {
const contract = (await createMarket({
question: prediction.question,
description: prediction.description,
initialProb: prediction.initialProb,
ante,
closeTime,
tags: parseWordsAsTags(tags),
})) as BinaryContract
setCreatedContracts((prev) => [...prev, contract])
}
setPredictionsString('')
setIsSubmitting(false)
}
return (
<Page>
<Title text="Make Predictions" />
<div className="w-full rounded-lg bg-gray-100 px-6 py-4 shadow-xl">
<form>
<div className="form-control">
<label className="label">
<span className="label-text">Prediction</span>
<div className="ml-1 text-sm text-gray-500">
One prediction per line, each formatted like "The sun will rise
tomorrow: 99%"
</div>
</label>
<textarea
className="textarea textarea-bordered h-60"
placeholder={bulkPlaceholder}
value={predictionsString}
onChange={(e) => setPredictionsString(e.target.value || '')}
></textarea>
</div>
</form>
<Spacer h={4} />
<div className="form-control w-full">
<label className="label">
<span className="label-text">Description</span>
</label>
<Textarea
placeholder="e.g. This market is part of the ACX predictions for 2022..."
className="input resize-none"
value={description}
onChange={(e) => setDescription(e.target.value || '')}
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">Tags</span>
</label>
<input
type="text"
placeholder="e.g. ACX2021 World"
className="input"
value={tags}
onChange={(e) => setTags(e.target.value || '')}
/>
</div>
<div className="form-control mb-1 items-start">
<label className="label mb-1 gap-2">
<span>Market close</span>
<InfoTooltip text="Trading will be halted after this time (local timezone)." />
</label>
<input
type="datetime-local"
className="input input-bordered"
onClick={(e) => e.stopPropagation()}
onChange={(e) => setCloseDate(e.target.value || '')}
min={Date.now()}
disabled={isSubmitting}
value={closeDate}
/>
</div>
<Spacer h={4} />
<div className="form-control mb-1 items-start">
<label className="label mb-1 gap-2">
<span>Market ante</span>
<InfoTooltip
text={`Subsidize your market to encourage trading. Ante bets are set to match your initial probability.
You earn ${0.01 * 100}% of trading volume.`}
/>
</label>
<BuyAmountInput
amount={ante}
minimumAmount={10}
onChange={setAnte}
error={anteError}
setError={setAnteError}
disabled={isSubmitting}
/>
</div>
{predictions.length > 0 && (
<div>
<Spacer h={4} />
<label className="label">
<span className="label-text">Preview</span>
</label>
<PredictionList predictions={predictions} />
</div>
)}
<Spacer h={4} />
<div className="my-4 flex justify-end">
<button
type="submit"
className={clsx('btn btn-primary', {
loading: isSubmitting,
})}
disabled={predictions.length === 0 || isSubmitting}
onClick={(e) => {
e.preventDefault()
createMarkets()
}}
>
Create all
</button>
</div>
</div>
{createdContracts.length > 0 && (
<>
<Spacer h={16} />
<Title text="Created Predictions" />
<div className="w-full rounded-lg bg-gray-100 px-6 py-4 shadow-xl">
<PredictionList predictions={createdContracts.map(toPrediction)} />
</div>
</>
)}
</Page>
)
}