From 5b29d32cfad901aa66ca876a08a32ad5e7e29014 Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Tue, 28 Jun 2022 18:08:37 -0500 Subject: [PATCH] Type description as JSON, fix string-based logic - Delete make-predictions.tsx - Delete feed logic that showed descriptions --- common/contract.ts | 5 +- common/new-contract.ts | 13 +- common/util/parse.ts | 6 + functions/src/on-create-contract.ts | 3 +- .../contract/contract-description.tsx | 10 +- web/components/feed/activity-items.ts | 1 - web/components/feed/feed-items.tsx | 11 +- web/pages/[username]/[contractSlug].tsx | 3 +- web/pages/api/v0/_types.ts | 3 +- web/pages/contract-search-firestore.tsx | 1 - web/pages/group/[...slugs]/index.tsx | 2 - web/pages/make-predictions.tsx | 292 ------------------ 12 files changed, 31 insertions(+), 319 deletions(-) delete mode 100644 web/pages/make-predictions.tsx diff --git a/common/contract.ts b/common/contract.ts index dc91a20e..79ecda31 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -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 = { 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 = { isResolved: boolean resolutionTime?: number // When the contract creator resolved the market resolution?: string - resolutionProbability?: number, + resolutionProbability?: number closeEmailsSent?: number diff --git a/common/new-contract.ts b/common/new-contract.ts index 0b7d294a..e408a743 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -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', diff --git a/common/util/parse.ts b/common/util/parse.ts index b73bdfb3..fe629721 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -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]) +} diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 20c7ceba..875260c4 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -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 ) }) diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index d8b657cb..ca53fe15 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -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 )} > - + {categories.length > 0 && (
diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index 68dfcb2d..511767c6 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -37,7 +37,6 @@ export type DescriptionItem = BaseActivityItem & { export type QuestionItem = BaseActivityItem & { type: 'question' - showDescription: boolean contractPath?: string } diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index 312190e4..ebdd28f8 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -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: { /> )} - {showDescription && ( - - )}
) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 24982b4f..7fbfda30 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -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 && ( diff --git a/web/pages/api/v0/_types.ts b/web/pages/api/v0/_types.ts index e0012c2b..bbcb702e 100644 --- a/web/pages/api/v0/_types.ts +++ b/web/pages/api/v0/_types.ts @@ -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 diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index c9a7a666..4adcdbe7 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -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(' ')) || diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index a3b99128..853af215 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -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) diff --git a/web/pages/make-predictions.tsx b/web/pages/make-predictions.tsx deleted file mode 100644 index ce694278..00000000 --- a/web/pages/make-predictions.tsx +++ /dev/null @@ -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 ( - - -
- -
-
{prediction.description}
- - {/* Initial probability */} -
-
-
- {prediction.initialProb.toFixed(0)}% -
chance
-
-
-
- {/* Current probability; hidden for now */} - {/*
-
-
- {prediction.initialProb}%
chance
-
-
-
*/} -
- ) -} - -function PredictionList(props: { predictions: Prediction[] }) { - const { predictions } = props - return ( - - {predictions.map((prediction) => - prediction.createdUrl ? ( - - - - - - ) : ( - - ) - )} - - ) -} - -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([]) - - const [ante, setAnte] = useState(100) - const [anteError, setAnteError] = useState() - // By default, close the market a week from today - const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DDT23:59') - const [closeDate, setCloseDate] = useState(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 ( - - - <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> - ) -}