From b8d65acc3f75cc29af12bb56176a64bcb68acbdc Mon Sep 17 00:00:00 2001 From: mantikoros Date: Thu, 6 Oct 2022 10:54:42 -0500 Subject: [PATCH 01/17] Revert "create market: use transaction" This reverts commit e1f24f24a96fa8d811ebcaa3b10b19d9b67cb282. --- functions/src/create-market.ts | 457 ++++++++++++++++----------------- 1 file changed, 224 insertions(+), 233 deletions(-) diff --git a/functions/src/create-market.ts b/functions/src/create-market.ts index 38852184..d1483ca4 100644 --- a/functions/src/create-market.ts +++ b/functions/src/create-market.ts @@ -15,7 +15,7 @@ import { import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' -import { isProd } from './utils' +import { chargeUser, getContract, isProd } from './utils' import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api' import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy' @@ -36,7 +36,7 @@ import { getPseudoProbability } from '../../common/pseudo-numeric' import { JSONContent } from '@tiptap/core' import { uniq, zip } from 'lodash' import { Bet } from '../../common/bet' -import { FieldValue, Transaction } from 'firebase-admin/firestore' +import { FieldValue } from 'firebase-admin/firestore' const descScehma: z.ZodType = z.lazy(() => z.intersection( @@ -107,242 +107,229 @@ export async function createMarketHelper(body: any, auth: AuthedUser) { visibility = 'public', } = validate(bodySchema, body) - return await firestore.runTransaction(async (trans) => { - let min, max, initialProb, isLogScale, answers + let min, max, initialProb, isLogScale, answers - if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { - let initialValue - ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) - if (max - min <= 0.01 || initialValue <= min || initialValue >= max) - throw new APIError(400, 'Invalid range.') + if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { + let initialValue + ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) + if (max - min <= 0.01 || initialValue <= min || initialValue >= max) + throw new APIError(400, 'Invalid range.') - initialProb = - getPseudoProbability(initialValue, min, max, isLogScale) * 100 + initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100 - if (initialProb < 1 || initialProb > 99) - if (outcomeType === 'PSEUDO_NUMERIC') - throw new APIError( - 400, - `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` - ) - else throw new APIError(400, 'Invalid initial probability.') - } - - if (outcomeType === 'BINARY') { - ;({ initialProb } = validate(binarySchema, body)) - } - - if (outcomeType === 'MULTIPLE_CHOICE') { - ;({ answers } = validate(multipleChoiceSchema, body)) - } - - const userDoc = await trans.get(firestore.collection('users').doc(auth.uid)) - if (!userDoc.exists) { - throw new APIError(400, 'No user exists with the authenticated user ID.') - } - const user = userDoc.data() as User - - const ante = FIXED_ANTE - const deservesFreeMarket = - (user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX - // TODO: this is broken because it's not in a transaction - if (ante > user.balance && !deservesFreeMarket) - throw new APIError(400, `Balance must be at least ${ante}.`) - - let group: Group | null = null - if (groupId) { - const groupDocRef = firestore.collection('groups').doc(groupId) - const groupDoc = await trans.get(groupDocRef) - if (!groupDoc.exists) { - throw new APIError(400, 'No group exists with the given group ID.') - } - - group = groupDoc.data() as Group - const groupMembersSnap = await trans.get( - firestore.collection(`groups/${groupId}/groupMembers`) - ) - const groupMemberDocs = groupMembersSnap.docs.map( - (doc) => doc.data() as { userId: string; createdTime: number } - ) - if ( - !groupMemberDocs.map((m) => m.userId).includes(user.id) && - !group.anyoneCanJoin && - group.creatorId !== user.id - ) { + if (initialProb < 1 || initialProb > 99) + if (outcomeType === 'PSEUDO_NUMERIC') throw new APIError( 400, - 'User must be a member/creator of the group or group must be open to add markets to it.' + `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` ) - } - } - const slug = await getSlug(trans, question) - const contractRef = firestore.collection('contracts').doc() + else throw new APIError(400, 'Invalid initial probability.') + } - console.log( - 'creating contract for', - user.username, - 'on', - question, - 'ante:', - ante || 0 - ) + if (outcomeType === 'BINARY') { + ;({ initialProb } = validate(binarySchema, body)) + } - // convert string descriptions into JSONContent - const newDescription = - !description || typeof description === 'string' - ? { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: description || ' ' }], - }, - ], - } - : description + if (outcomeType === 'MULTIPLE_CHOICE') { + ;({ answers } = validate(multipleChoiceSchema, body)) + } - const contract = getNewContract( - contractRef.id, - slug, - user, - question, - outcomeType, - newDescription, - initialProb ?? 0, - ante, - closeTime.getTime(), - tags ?? [], - NUMERIC_BUCKET_COUNT, - min ?? 0, - max ?? 0, - isLogScale ?? false, - answers ?? [], - visibility - ) + const userDoc = await firestore.collection('users').doc(auth.uid).get() + if (!userDoc.exists) { + throw new APIError(400, 'No user exists with the authenticated user ID.') + } + const user = userDoc.data() as User - const providerId = deservesFreeMarket - ? isProd() - ? HOUSE_LIQUIDITY_PROVIDER_ID - : DEV_HOUSE_LIQUIDITY_PROVIDER_ID - : user.id + const ante = FIXED_ANTE + const deservesFreeMarket = + (user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX + // TODO: this is broken because it's not in a transaction + if (ante > user.balance && !deservesFreeMarket) + throw new APIError(400, `Balance must be at least ${ante}.`) - if (ante) { - const delta = FieldValue.increment(-ante) - const providerDoc = firestore.collection('users').doc(providerId) - await trans.update(providerDoc, { balance: delta, totalDeposits: delta }) + let group: Group | null = null + if (groupId) { + const groupDocRef = firestore.collection('groups').doc(groupId) + const groupDoc = await groupDocRef.get() + if (!groupDoc.exists) { + throw new APIError(400, 'No group exists with the given group ID.') } - if (deservesFreeMarket) { - await trans.update(firestore.collection('users').doc(user.id), { - freeMarketsCreated: FieldValue.increment(1), + group = groupDoc.data() as Group + const groupMembersSnap = await firestore + .collection(`groups/${groupId}/groupMembers`) + .get() + const groupMemberDocs = groupMembersSnap.docs.map( + (doc) => doc.data() as { userId: string; createdTime: number } + ) + if ( + !groupMemberDocs.map((m) => m.userId).includes(user.id) && + !group.anyoneCanJoin && + group.creatorId !== user.id + ) { + throw new APIError( + 400, + 'User must be a member/creator of the group or group must be open to add markets to it.' + ) + } + } + const slug = await getSlug(question) + const contractRef = firestore.collection('contracts').doc() + + console.log( + 'creating contract for', + user.username, + 'on', + question, + 'ante:', + ante || 0 + ) + + // convert string descriptions into JSONContent + const newDescription = + !description || typeof description === 'string' + ? { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: description || ' ' }], + }, + ], + } + : description + + const contract = getNewContract( + contractRef.id, + slug, + user, + question, + outcomeType, + newDescription, + initialProb ?? 0, + ante, + closeTime.getTime(), + tags ?? [], + NUMERIC_BUCKET_COUNT, + min ?? 0, + max ?? 0, + isLogScale ?? false, + answers ?? [], + visibility + ) + + const providerId = deservesFreeMarket + ? isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + : user.id + + if (ante) await chargeUser(providerId, ante, true) + if (deservesFreeMarket) + await firestore + .collection('users') + .doc(user.id) + .update({ freeMarketsCreated: FieldValue.increment(1) }) + + await contractRef.create(contract) + + if (group != null) { + const groupContractsSnap = await firestore + .collection(`groups/${groupId}/groupContracts`) + .get() + const groupContracts = groupContractsSnap.docs.map( + (doc) => doc.data() as { contractId: string; createdTime: number } + ) + if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { + await createGroupLinks(group, [contractRef.id], auth.uid) + const groupContractRef = firestore + .collection(`groups/${groupId}/groupContracts`) + .doc(contract.id) + await groupContractRef.set({ + contractId: contract.id, + createdTime: Date.now(), }) } + } - await contractRef.create(contract) + if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { + const liquidityDoc = firestore + .collection(`contracts/${contract.id}/liquidity`) + .doc() - if (group != null) { - const groupContractsSnap = await trans.get( - firestore.collection(`groups/${groupId}/groupContracts`) - ) - const groupContracts = groupContractsSnap.docs.map( - (doc) => doc.data() as { contractId: string; createdTime: number } + const lp = getCpmmInitialLiquidity( + providerId, + contract as CPMMBinaryContract, + liquidityDoc.id, + ante + ) + + await liquidityDoc.set(lp) + } else if (outcomeType === 'MULTIPLE_CHOICE') { + const betCol = firestore.collection(`contracts/${contract.id}/bets`) + const betDocs = (answers ?? []).map(() => betCol.doc()) + + const answerCol = firestore.collection(`contracts/${contract.id}/answers`) + const answerDocs = (answers ?? []).map((_, i) => + answerCol.doc(i.toString()) + ) + + const { bets, answerObjects } = getMultipleChoiceAntes( + user, + contract as MultipleChoiceContract, + answers ?? [], + betDocs.map((bd) => bd.id) + ) + + await Promise.all( + zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet)) + ) + await Promise.all( + zip(answerObjects, answerDocs).map(([answer, doc]) => + doc?.create(answer as Answer) ) + ) + await contractRef.update({ answers: answerObjects }) + } else if (outcomeType === 'FREE_RESPONSE') { + const noneAnswerDoc = firestore + .collection(`contracts/${contract.id}/answers`) + .doc('0') - if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { - await createGroupLinks(trans, group, [contractRef.id], auth.uid) + const noneAnswer = getNoneAnswer(contract.id, user) + await noneAnswerDoc.set(noneAnswer) - const groupContractRef = firestore - .collection(`groups/${groupId}/groupContracts`) - .doc(contract.id) + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() - await trans.set(groupContractRef, { - contractId: contract.id, - createdTime: Date.now(), - }) - } - } + const anteBet = getFreeAnswerAnte( + providerId, + contract as FreeResponseContract, + anteBetDoc.id + ) + await anteBetDoc.set(anteBet) + } else if (outcomeType === 'NUMERIC') { + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() - if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { - const liquidityDoc = firestore - .collection(`contracts/${contract.id}/liquidity`) - .doc() + const anteBet = getNumericAnte( + providerId, + contract as NumericContract, + ante, + anteBetDoc.id + ) - const lp = getCpmmInitialLiquidity( - providerId, - contract as CPMMBinaryContract, - liquidityDoc.id, - ante - ) + await anteBetDoc.set(anteBet) + } - await trans.set(liquidityDoc, lp) - } else if (outcomeType === 'MULTIPLE_CHOICE') { - const betCol = firestore.collection(`contracts/${contract.id}/bets`) - const betDocs = (answers ?? []).map(() => betCol.doc()) - - const answerCol = firestore.collection(`contracts/${contract.id}/answers`) - const answerDocs = (answers ?? []).map((_, i) => - answerCol.doc(i.toString()) - ) - - const { bets, answerObjects } = getMultipleChoiceAntes( - user, - contract as MultipleChoiceContract, - answers ?? [], - betDocs.map((bd) => bd.id) - ) - - await Promise.all( - zip(bets, betDocs).map(([bet, doc]) => - doc ? trans.create(doc, bet as Bet) : undefined - ) - ) - await Promise.all( - zip(answerObjects, answerDocs).map(([answer, doc]) => - doc ? trans.create(doc, answer as Answer) : undefined - ) - ) - await trans.update(contractRef, { answers: answerObjects }) - } else if (outcomeType === 'FREE_RESPONSE') { - const noneAnswerDoc = firestore - .collection(`contracts/${contract.id}/answers`) - .doc('0') - - const noneAnswer = getNoneAnswer(contract.id, user) - await trans.set(noneAnswerDoc, noneAnswer) - - const anteBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - - const anteBet = getFreeAnswerAnte( - providerId, - contract as FreeResponseContract, - anteBetDoc.id - ) - await trans.set(anteBetDoc, anteBet) - } else if (outcomeType === 'NUMERIC') { - const anteBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - - const anteBet = getNumericAnte( - providerId, - contract as NumericContract, - ante, - anteBetDoc.id - ) - - await trans.set(anteBetDoc, anteBet) - } - - return contract - }) + return contract } -const getSlug = async (trans: Transaction, question: string) => { +const getSlug = async (question: string) => { const proposedSlug = slugify(question) - const preexistingContract = await getContractFromSlug(trans, proposedSlug) + const preexistingContract = await getContractFromSlug(proposedSlug) return preexistingContract ? proposedSlug + '-' + randomString() @@ -351,42 +338,46 @@ const getSlug = async (trans: Transaction, question: string) => { const firestore = admin.firestore() -async function getContractFromSlug(trans: Transaction, slug: string) { - const snap = await trans.get( - firestore.collection('contracts').where('slug', '==', slug) - ) +export async function getContractFromSlug(slug: string) { + const snap = await firestore + .collection('contracts') + .where('slug', '==', slug) + .get() return snap.empty ? undefined : (snap.docs[0].data() as Contract) } async function createGroupLinks( - trans: Transaction, group: Group, contractIds: string[], userId: string ) { for (const contractId of contractIds) { - const contractRef = firestore.collection('contracts').doc(contractId) - const contract = (await trans.get(contractRef)).data() as Contract - + const contract = await getContract(contractId) if (!contract?.groupSlugs?.includes(group.slug)) { - await trans.update(contractRef, { - groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), - }) + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), + }) } if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) { - await trans.update(contractRef, { - groupLinks: [ - { - groupId: group.id, - name: group.name, - slug: group.slug, - userId, - createdTime: Date.now(), - } as GroupLink, - ...(contract?.groupLinks ?? []), - ], - }) + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupLinks: [ + { + groupId: group.id, + name: group.name, + slug: group.slug, + userId, + createdTime: Date.now(), + } as GroupLink, + ...(contract?.groupLinks ?? []), + ], + }) } } } From 59de9799498a5710fc8d79ae18f8edd224d67af4 Mon Sep 17 00:00:00 2001 From: Pico2x Date: Thu, 6 Oct 2022 17:04:00 +0100 Subject: [PATCH 02/17] Refactor Pinned Items into a reusable component --- web/components/groups/group-overview.tsx | 203 +++++++++++++---------- web/components/pinned-select-modal.tsx | 8 +- 2 files changed, 118 insertions(+), 93 deletions(-) diff --git a/web/components/groups/group-overview.tsx b/web/components/groups/group-overview.tsx index 080453ca..d5cdaafa 100644 --- a/web/components/groups/group-overview.tsx +++ b/web/components/groups/group-overview.tsx @@ -145,8 +145,6 @@ function GroupOverviewPinned(props: { }) { const { group, posts, isEditable } = props const [pinned, setPinned] = useState([]) - const [open, setOpen] = useState(false) - const [editMode, setEditMode] = useState(false) useEffect(() => { async function getPinned() { @@ -185,100 +183,127 @@ function GroupOverviewPinned(props: { ...(selectedItems as { itemId: string; type: 'contract' | 'post' }[]), ], }) - setOpen(false) + } + + function onDeleteClicked(index: number) { + const newPinned = group.pinnedItems.filter((item) => { + return item.itemId !== group.pinnedItems[index].itemId + }) + updateGroup(group, { pinnedItems: newPinned }) } return isEditable || (group.pinnedItems && group.pinnedItems.length > 0) ? ( - pinned.length > 0 || isEditable ? ( -
- - - {isEditable && ( - - )} - -
- - {pinned.length == 0 && !editMode && ( -
-

- No pinned items yet. Click the edit button to add some! -

-
- )} - {pinned.map((element, index) => ( -
- {element} + + ) : ( + + ) +} - {editMode && ( - { - const newPinned = group.pinnedItems.filter((item) => { - return item.itemId !== group.pinnedItems[index].itemId - }) - updateGroup(group, { pinnedItems: newPinned }) - }} - /> - )} -
- ))} - {editMode && group.pinnedItems && pinned.length < 6 && ( -
- - - -
+export function PinnedItems(props: { + posts: Post[] + isEditable: boolean + pinned: JSX.Element[] + onDeleteClicked: (index: number) => void + onSubmit: (selectedItems: { itemId: string; type: string }[]) => void + group?: Group + modalMessage: string +}) { + const { + isEditable, + pinned, + onDeleteClicked, + onSubmit, + posts, + group, + modalMessage, + } = props + const [editMode, setEditMode] = useState(false) + const [open, setOpen] = useState(false) + + return pinned.length > 0 || isEditable ? ( +
+ + + {isEditable && ( +
- - Pin posts or markets to the overview of this group. + + )} + +
+ + {pinned.length == 0 && !editMode && ( +
+

+ No pinned items yet. Click the edit button to add some! +

- } - onSubmit={onSubmit} - /> + )} + {pinned.map((element, index) => ( +
+ {element} + + {editMode && onDeleteClicked(index)} />} +
+ ))} + {editMode && pinned.length < 6 && ( +
+ + + +
+ )} +
- ) : ( - - ) + {modalMessage}
+ } + onSubmit={onSubmit} + /> +
) : ( <> ) diff --git a/web/components/pinned-select-modal.tsx b/web/components/pinned-select-modal.tsx index e72deee2..c43c7534 100644 --- a/web/components/pinned-select-modal.tsx +++ b/web/components/pinned-select-modal.tsx @@ -20,8 +20,8 @@ export function PinnedSelectModal(props: { selectedItems: { itemId: string; type: string }[] ) => void | Promise contractSearchOptions?: Partial[0]> - group: Group posts: Post[] + group?: Group }) { const { title, @@ -134,8 +134,8 @@ export function PinnedSelectModal(props: { highlightClassName: '!bg-indigo-100 outline outline-2 outline-indigo-300', }} - additionalFilter={{ groupSlug: group.slug }} - persistPrefix={`group-${group.slug}`} + additionalFilter={group ? { groupSlug: group.slug } : undefined} + persistPrefix={group ? `group-${group.slug}` : undefined} headerClassName="bg-white sticky" {...contractSearchOptions} /> @@ -152,7 +152,7 @@ export function PinnedSelectModal(props: { '!bg-indigo-100 outline outline-2 outline-indigo-300', }} /> - {posts.length === 0 && ( + {posts.length == 0 && (
No posts yet
)} From 853e3e48967ac83173269edf698f45ba91e72fdd Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Thu, 6 Oct 2022 14:20:35 -0400 Subject: [PATCH 03/17] Mark @v with a (Bot) label --- web/components/user-link.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/components/user-link.tsx b/web/components/user-link.tsx index 4b05ccd0..c3a273fc 100644 --- a/web/components/user-link.tsx +++ b/web/components/user-link.tsx @@ -34,6 +34,17 @@ export function UserLink(props: { )} > {shortName} + {BOT_USERNAMES.includes(username) && } ) } + +const BOT_USERNAMES = ['v'] + +function BotBadge() { + return ( + + Bot + + ) +} From 2f2c586d5de08b87b2857b04acfc34bc78fcabdb Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Thu, 6 Oct 2022 12:01:00 -0700 Subject: [PATCH 04/17] fix padding on daily movers --- web/components/contract/prob-change-table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 70eaf18c..c5eb9e55 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -37,12 +37,12 @@ export function ProbChangeTable(props: { return ( - + {filteredPositiveChanges.map((contract) => ( ))} - + {filteredNegativeChanges.map((contract) => ( ))} From 91da39370f08d27ec957285f5afea6fe8b697386 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Thu, 6 Oct 2022 14:54:22 -0500 Subject: [PATCH 05/17] fix type errors --- functions/src/scripts/denormalize.ts | 3 +-- functions/src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/functions/src/scripts/denormalize.ts b/functions/src/scripts/denormalize.ts index d4feb425..3362e940 100644 --- a/functions/src/scripts/denormalize.ts +++ b/functions/src/scripts/denormalize.ts @@ -3,7 +3,6 @@ import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore' import { isEqual, zip } from 'lodash' -import { UpdateSpec } from '../utils' export type DocumentValue = { doc: DocumentSnapshot @@ -54,7 +53,7 @@ export function getDiffUpdate(diff: DocumentDiff) { return { doc: diff.dest.doc.ref, fields: Object.fromEntries(zip(diff.dest.fields, diff.src.vals)), - } as UpdateSpec + } } export function applyDiff(transaction: Transaction, diff: DocumentDiff) { diff --git a/functions/src/utils.ts b/functions/src/utils.ts index efc22e53..91f4b293 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -47,7 +47,7 @@ export const writeAsync = async ( const batch = db.batch() for (const { doc, fields } of chunks[i]) { if (operationType === 'update') { - batch.update(doc, fields) + batch.update(doc, fields as any) } else { batch.set(doc, fields) } From 4162cca3ff4bceff8f93a9eca3e35b9679864d97 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Thu, 6 Oct 2022 15:23:51 -0500 Subject: [PATCH 06/17] Wrap sprig init in check for window --- web/lib/service/sprig.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/lib/service/sprig.ts b/web/lib/service/sprig.ts index f89a9678..7a478d77 100644 --- a/web/lib/service/sprig.ts +++ b/web/lib/service/sprig.ts @@ -5,7 +5,7 @@ import { ENV_CONFIG } from 'common/envs/constants' import { PROD_CONFIG } from 'common/envs/prod' -if (ENV_CONFIG.domain === PROD_CONFIG.domain) { +if (ENV_CONFIG.domain === PROD_CONFIG.domain && typeof window !== 'undefined') { try { ;(function (l, e, a, p) { if (window.Sprig) return @@ -20,7 +20,8 @@ if (ENV_CONFIG.domain === PROD_CONFIG.domain) { a.async = 1 a.src = e + '?id=' + S.appId p = l.getElementsByTagName('script')[0] - p.parentNode.insertBefore(a, p) + ENV_CONFIG.domain === PROD_CONFIG.domain && + p.parentNode.insertBefore(a, p) })(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId) } catch (error) { console.log('Error initializing Sprig, please complain to Barak', error) From bc5af50b0ccd92dea103a74144b087d9006dc01b Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Thu, 6 Oct 2022 13:49:39 -0700 Subject: [PATCH 07/17] unindex date-docs from search engines --- web/components/NoSEO.tsx | 10 ++++++++++ web/pages/date-docs/[username].tsx | 2 ++ web/pages/date-docs/create.tsx | 2 ++ web/pages/date-docs/index.tsx | 2 ++ 4 files changed, 16 insertions(+) create mode 100644 web/components/NoSEO.tsx diff --git a/web/components/NoSEO.tsx b/web/components/NoSEO.tsx new file mode 100644 index 00000000..f72907c8 --- /dev/null +++ b/web/components/NoSEO.tsx @@ -0,0 +1,10 @@ +import Head from "next/head"; + +/** Exclude page from search results */ +export function NoSEO() { + return ( + + + + ) +} \ No newline at end of file diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx index 350e79b7..dd7d5d73 100644 --- a/web/pages/date-docs/[username].tsx +++ b/web/pages/date-docs/[username].tsx @@ -22,6 +22,7 @@ import { PostCommentsActivity, RichEditPost } from '../post/[...slugs]' import { usePost } from 'web/hooks/use-post' import { useTipTxns } from 'web/hooks/use-tip-txns' import { useCommentsOnPost } from 'web/hooks/use-comments' +import { NoSEO } from 'web/components/NoSEO' export async function getStaticProps(props: { params: { username: string } }) { const { username } = props.params @@ -62,6 +63,7 @@ function DateDocPage(props: { creator: User; post: DateDoc }) { return ( + diff --git a/web/pages/date-docs/create.tsx b/web/pages/date-docs/create.tsx index 08442cc1..ed1df677 100644 --- a/web/pages/date-docs/create.tsx +++ b/web/pages/date-docs/create.tsx @@ -14,6 +14,7 @@ import dayjs from 'dayjs' import { MINUTE_MS } from 'common/util/time' import { Col } from 'web/components/layout/col' import { MAX_QUESTION_LENGTH } from 'common/contract' +import { NoSEO } from 'web/components/NoSEO' export default function CreateDateDocPage() { const user = useUser() @@ -64,6 +65,7 @@ export default function CreateDateDocPage() { return ( +
diff --git a/web/pages/date-docs/index.tsx b/web/pages/date-docs/index.tsx index 9ddeb57f..48e0bb13 100644 --- a/web/pages/date-docs/index.tsx +++ b/web/pages/date-docs/index.tsx @@ -12,6 +12,7 @@ import { Button } from 'web/components/button' import { SiteLink } from 'web/components/site-link' import { getUser, User } from 'web/lib/firebase/users' import { DateDocPost } from './[username]' +import { NoSEO } from 'web/components/NoSEO' export async function getStaticProps() { const dateDocs = await getDateDocs() @@ -40,6 +41,7 @@ export default function DatePage(props: { return ( +
From ac37f94cf78f7985d71c0ef1790d8bbc77e36bd7 Mon Sep 17 00:00:00 2001 From: sipec <sipec@users.noreply.github.com> Date: Thu, 6 Oct 2022 20:50:29 +0000 Subject: [PATCH 08/17] Auto-prettification --- web/components/NoSEO.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/NoSEO.tsx b/web/components/NoSEO.tsx index f72907c8..53437b10 100644 --- a/web/components/NoSEO.tsx +++ b/web/components/NoSEO.tsx @@ -1,10 +1,10 @@ -import Head from "next/head"; +import Head from 'next/head' /** Exclude page from search results */ export function NoSEO() { return ( <Head> - <meta name="robots" content="noindex,follow"/> + <meta name="robots" content="noindex,follow" /> </Head> ) -} \ No newline at end of file +} From 7ca0fb72fcb70389884d93ea543f45a7ea2c7546 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 16:36:16 -0500 Subject: [PATCH 09/17] compute elasticity --- common/calculate-metrics.ts | 59 +++++++++++++++++++++++++++++++-- common/contract.ts | 1 + common/new-bet.ts | 5 ++- common/new-contract.ts | 1 + functions/src/update-metrics.ts | 2 ++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 7c2153c1..524dfadd 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -1,9 +1,15 @@ import { last, sortBy, sum, sumBy } from 'lodash' import { calculatePayout } from './calculate' -import { Bet } from './bet' -import { Contract } from './contract' +import { Bet, LimitBet } from './bet' +import { + Contract, + CPMMContract, + DPMContract, +} from './contract' import { PortfolioMetrics, User } from './user' import { DAY_MS } from './util/time' +import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet' +import { getCpmmProbability } from './calculate-cpmm' const computeInvestmentValue = ( bets: Bet[], @@ -40,6 +46,55 @@ export const computeInvestmentValueCustomProb = ( }) } +export const computeElasticity = ( + bets: Bet[], + contract: Contract, + betAmount = 50 +) => { + const { mechanism, outcomeType } = contract + return mechanism === 'cpmm-1' && + (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') + ? computeBinaryCpmmElasticity(bets, contract, betAmount) + : computeDpmElasticity(contract, betAmount) +} + +export const computeBinaryCpmmElasticity = ( + bets: Bet[], + contract: CPMMContract, + betAmount = 50 +) => { + const limitBets = bets + .filter( + (b) => + !b.isFilled && !b.isSold && !b.isRedemption && !b.sale && !b.isCancelled + ) + .sort((a, b) => a.createdTime - b.createdTime) + + const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo( + 'YES', + betAmount, + contract, + undefined, + limitBets as LimitBet[] + ) + const resultYes = getCpmmProbability(poolY, pY) + + const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo( + 'NO', + betAmount, + contract, + undefined, + limitBets as LimitBet[] + ) + const resultNo = getCpmmProbability(poolN, pN) + + return resultYes - resultNo +} + +export const computeDpmElasticity = (contract: DPMContract, betAmount = 50) => { + return getNewMultiBetInfo('', betAmount, contract).newBet.probAfter +} + const computeTotalPool = (userContracts: Contract[], startTime = 0) => { const periodFilteredContracts = userContracts.filter( (contract) => contract.createdTime >= startTime diff --git a/common/contract.ts b/common/contract.ts index 1255874d..2656b5d5 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -49,6 +49,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { volume: number volume24Hours: number volume7Days: number + elasticity: number collectedFees: Fees diff --git a/common/new-bet.ts b/common/new-bet.ts index 91faf640..e9f5c554 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -17,8 +17,7 @@ import { import { CPMMBinaryContract, DPMBinaryContract, - FreeResponseContract, - MultipleChoiceContract, + DPMContract, NumericContract, PseudoNumericContract, } from './contract' @@ -325,7 +324,7 @@ export const getNewBinaryDpmBetInfo = ( export const getNewMultiBetInfo = ( outcome: string, amount: number, - contract: FreeResponseContract | MultipleChoiceContract + contract: DPMContract ) => { const { pool, totalShares, totalBets } = contract diff --git a/common/new-contract.ts b/common/new-contract.ts index 3580b164..8ab44d2e 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -70,6 +70,7 @@ export function getNewContract( volume: 0, volume24Hours: 0, volume7Days: 0, + elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.56, collectedFees: { creatorFee: 0, diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts index 70c7c742..24dc07e7 100644 --- a/functions/src/update-metrics.ts +++ b/functions/src/update-metrics.ts @@ -14,6 +14,7 @@ import { calculateNewPortfolioMetrics, calculateNewProfit, calculateProbChanges, + computeElasticity, computeVolume, } from '../../common/calculate-metrics' import { getProbability } from '../../common/calculate' @@ -103,6 +104,7 @@ export async function updateMetricsCore() { fields: { volume24Hours: computeVolume(contractBets, now - DAY_MS), volume7Days: computeVolume(contractBets, now - DAY_MS * 7), + elasticity: computeElasticity(contractBets, contract), ...cpmmFields, }, } From a63405ca7c4d43a8f317a0d7028f7c4a5cd7be86 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 16:47:52 -0500 Subject: [PATCH 10/17] change dpm elasticity --- common/calculate-metrics.ts | 15 +++++++-------- common/new-contract.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 524dfadd..bf588345 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -1,11 +1,7 @@ import { last, sortBy, sum, sumBy } from 'lodash' import { calculatePayout } from './calculate' import { Bet, LimitBet } from './bet' -import { - Contract, - CPMMContract, - DPMContract, -} from './contract' +import { Contract, CPMMContract, DPMContract } from './contract' import { PortfolioMetrics, User } from './user' import { DAY_MS } from './util/time' import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet' @@ -61,7 +57,7 @@ export const computeElasticity = ( export const computeBinaryCpmmElasticity = ( bets: Bet[], contract: CPMMContract, - betAmount = 50 + betAmount: number ) => { const limitBets = bets .filter( @@ -91,8 +87,11 @@ export const computeBinaryCpmmElasticity = ( return resultYes - resultNo } -export const computeDpmElasticity = (contract: DPMContract, betAmount = 50) => { - return getNewMultiBetInfo('', betAmount, contract).newBet.probAfter +export const computeDpmElasticity = ( + contract: DPMContract, + betAmount: number +) => { + return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter } const computeTotalPool = (userContracts: Contract[], startTime = 0) => { diff --git a/common/new-contract.ts b/common/new-contract.ts index 8ab44d2e..9a73e2ea 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -70,7 +70,7 @@ export function getNewContract( volume: 0, volume24Hours: 0, volume7Days: 0, - elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.56, + elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75, collectedFees: { creatorFee: 0, From adb809f9733fe54a7a3862f75aa0ab35e78bb638 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 6 Oct 2022 15:19:37 -0700 Subject: [PATCH 11/17] Fix google lighthouse issues (#1013) --- web/components/nav/manifold-logo.tsx | 1 + web/components/nav/more-button.tsx | 4 ++-- web/pages/_app.tsx | 5 +---- web/pages/_document.tsx | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/web/components/nav/manifold-logo.tsx b/web/components/nav/manifold-logo.tsx index ec15d54b..b6dbc885 100644 --- a/web/components/nav/manifold-logo.tsx +++ b/web/components/nav/manifold-logo.tsx @@ -22,6 +22,7 @@ export function ManifoldLogo(props: { src={darkBackground ? '/logo-white.svg' : '/logo.svg'} width={45} height={45} + alt="" /> {!hideText && diff --git a/web/components/nav/more-button.tsx b/web/components/nav/more-button.tsx index 5e6653f3..9847541c 100644 --- a/web/components/nav/more-button.tsx +++ b/web/components/nav/more-button.tsx @@ -11,13 +11,13 @@ function SidebarButton(props: { }) { const { text, children } = props return ( - <a className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"> + <button className="group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"> <props.icon className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500" aria-hidden="true" /> <span className="truncate">{text}</span> {children} - </a> + </button> ) } diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 48ad5a9a..7a96b2e2 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -74,10 +74,7 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) { content="https://manifold.markets/logo-bg-white.png" key="image2" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1, maximum-scale=1" - /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> </Head> <AuthProvider serverUser={pageProps.auth}> <QueryClientProvider client={queryClient}> diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index b8cb657c..f2c46854 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -3,7 +3,7 @@ import { ENV_CONFIG } from 'common/envs/constants' export default function Document() { return ( - <Html data-theme="mantic" className="min-h-screen"> + <Html lang="en" data-theme="mantic" className="min-h-screen"> <Head> <link rel="icon" href={ENV_CONFIG.faviconPath} /> <link From d9c8925ea0a7e5c354307d4e4f1b4f24e2d8aad7 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 6 Oct 2022 15:20:46 -0700 Subject: [PATCH 12/17] don't hide free response panel on open resolve --- web/components/answers/answers-panel.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 08b1373f..6b35f74e 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -157,11 +157,9 @@ export function AnswersPanel(props: { <div className="pb-4 text-gray-500">No answers yet...</div> )} - {outcomeType === 'FREE_RESPONSE' && - tradingAllowed(contract) && - (!resolveOption || resolveOption === 'CANCEL') && ( - <CreateAnswerPanel contract={contract} /> - )} + {outcomeType === 'FREE_RESPONSE' && tradingAllowed(contract) && ( + <CreateAnswerPanel contract={contract} /> + )} {(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) && !resolution && ( From 80622dc7ee59269d7d69100984f219a2243184a0 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 18:23:27 -0500 Subject: [PATCH 13/17] liquidity sort --- web/components/contract-search.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 78eacd36..8d871d65 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -48,6 +48,7 @@ export const SORTS = [ { label: 'Daily trending', value: 'daily-score' }, { label: '24h volume', value: '24-hour-vol' }, { label: 'Most popular', value: 'most-popular' }, + { label: 'Liquidity', value: 'liquidity' }, { label: 'Last updated', value: 'last-updated' }, { label: 'Closing soon', value: 'close-date' }, { label: 'Resolve date', value: 'resolve-date' }, From 77e0631ea45aba6cff3a1b1e3d7268873faacabd Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 18:03:44 -0500 Subject: [PATCH 14/17] Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'. --- web/components/feed/feed-bets.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 900265cb..e164f6fa 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -64,11 +64,11 @@ export function BetStatusText(props: { }, [challengeSlug, contract.id]) const bought = amount >= 0 ? 'bought' : 'sold' + const money = formatMoney(Math.abs(amount)) const outOfTotalAmount = bet.limitProb !== undefined && bet.orderAmount !== undefined - ? ` / ${formatMoney(bet.orderAmount)}` + ? ` of ${bet.isCancelled ? money : formatMoney(bet.orderAmount)}` : '' - const money = formatMoney(Math.abs(amount)) const hadPoolMatch = (bet.limitProb === undefined || @@ -105,7 +105,6 @@ export function BetStatusText(props: { {!hideOutcome && ( <> {' '} - of{' '} <OutcomeLabel outcome={outcome} value={(bet as any).value} From d846b9fb3064fd82b698aaa3683921fa28d12e24 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 18:36:27 -0500 Subject: [PATCH 15/17] Date doc: Toggle to disable creating a prediction market --- functions/src/create-post.ts | 30 +++++++++++++++++------------- web/pages/date-docs/[username].tsx | 20 +++++++++++--------- web/pages/date-docs/create.tsx | 26 +++++++++++++++++++------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index 96e3c66a..d1864ac2 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -71,19 +71,23 @@ export const createpost = newEndpoint({}, async (req, auth) => { if (question) { const closeTime = Date.now() + DAY_MS * 30 * 3 - const result = await createMarketHelper( - { - question, - closeTime, - outcomeType: 'BINARY', - visibility: 'unlisted', - initialProb: 50, - // Dating group! - groupId: 'j3ZE8fkeqiKmRGumy3O1', - }, - auth - ) - contractSlug = result.slug + try { + const result = await createMarketHelper( + { + question, + closeTime, + outcomeType: 'BINARY', + visibility: 'unlisted', + initialProb: 50, + // Dating group! + groupId: 'j3ZE8fkeqiKmRGumy3O1', + }, + auth + ) + contractSlug = result.slug + } catch (e) { + console.error(e) + } } const post: Post = removeUndefinedProps({ diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx index dd7d5d73..6f6eaf5e 100644 --- a/web/pages/date-docs/[username].tsx +++ b/web/pages/date-docs/[username].tsx @@ -142,15 +142,17 @@ export function DateDocPost(props: { ) : ( <Content content={content} /> )} - <div className="mt-4 w-full max-w-lg self-center rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-3"> - <iframe - height="405" - src={marketUrl} - title="" - frameBorder="0" - className="w-full rounded-xl bg-white p-10" - ></iframe> - </div> + {contractSlug && ( + <div className="mt-4 w-full max-w-lg self-center rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-3"> + <iframe + height="405" + src={marketUrl} + title="" + frameBorder="0" + className="w-full rounded-xl bg-white p-10" + ></iframe> + </div> + )} </Col> ) } diff --git a/web/pages/date-docs/create.tsx b/web/pages/date-docs/create.tsx index ed1df677..a0fe8922 100644 --- a/web/pages/date-docs/create.tsx +++ b/web/pages/date-docs/create.tsx @@ -15,6 +15,8 @@ import { MINUTE_MS } from 'common/util/time' import { Col } from 'web/components/layout/col' import { MAX_QUESTION_LENGTH } from 'common/contract' import { NoSEO } from 'web/components/NoSEO' +import ShortToggle from 'web/components/widgets/short-toggle' +import { removeUndefinedProps } from 'common/util/object' export default function CreateDateDocPage() { const user = useUser() @@ -26,6 +28,7 @@ export default function CreateDateDocPage() { const title = `${user?.name}'s Date Doc` const subtitle = 'Manifold dating docs' const [birthday, setBirthday] = useState<undefined | string>(undefined) + const [createMarket, setCreateMarket] = useState(true) const [question, setQuestion] = useState( 'Will I find a partner in the next 3 months?' ) @@ -38,7 +41,11 @@ export default function CreateDateDocPage() { const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined const isValid = - user && birthday && editor && editor.isEmpty === false && question + user && + birthday && + editor && + editor.isEmpty === false && + (question || !createMarket) async function saveDateDoc() { if (!user || !editor || !birthdayTime) return @@ -46,15 +53,15 @@ export default function CreateDateDocPage() { const newPost: Omit< DateDoc, 'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug' - > & { question: string } = { + > & { question?: string } = removeUndefinedProps({ title, subtitle, content: editor.getJSON(), bounty: 0, birthday: birthdayTime, type: 'date-doc', - question, - } + question: createMarket ? question : undefined, + }) const result = await createPost(newPost) @@ -106,9 +113,13 @@ export default function CreateDateDocPage() { </Col> <Col className="gap-4"> - <div className=""> - Finally, we'll create an (unlisted) prediction market! - </div> + <Row className="items-center gap-4"> + <ShortToggle + on={createMarket} + setOn={(on) => setCreateMarket(on)} + /> + Create an (unlisted) prediction market attached to the date doc + </Row> <Col className="gap-2"> <Textarea @@ -116,6 +127,7 @@ export default function CreateDateDocPage() { maxLength={MAX_QUESTION_LENGTH} value={question} onChange={(e) => setQuestion(e.target.value || '')} + disabled={!createMarket} /> <div className="ml-2 text-gray-500">Cost: M$100</div> </Col> From 0dc8753a921366fb3d69814dc005c2d4a1264f46 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 18:50:53 -0500 Subject: [PATCH 16/17] Listen for date doc changes --- web/hooks/use-post.ts | 14 ++++++++++++-- web/lib/firebase/posts.ts | 13 ++++++++++++- web/pages/date-docs/index.tsx | 4 +++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/web/hooks/use-post.ts b/web/hooks/use-post.ts index ff7bf6b9..1fd69888 100644 --- a/web/hooks/use-post.ts +++ b/web/hooks/use-post.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { Post } from 'common/post' -import { listenForPost } from 'web/lib/firebase/posts' +import { DateDoc, Post } from 'common/post' +import { listenForDateDocs, listenForPost } from 'web/lib/firebase/posts' export const usePost = (postId: string | undefined) => { const [post, setPost] = useState<Post | null | undefined>() @@ -37,3 +37,13 @@ export const usePosts = (postIds: string[]) => { ) .sort((a, b) => b.createdTime - a.createdTime) } + +export const useDateDocs = () => { + const [dateDocs, setDateDocs] = useState<DateDoc[]>() + + useEffect(() => { + return listenForDateDocs(setDateDocs) + }, []) + + return dateDocs +} diff --git a/web/lib/firebase/posts.ts b/web/lib/firebase/posts.ts index 22b9d095..343243cd 100644 --- a/web/lib/firebase/posts.ts +++ b/web/lib/firebase/posts.ts @@ -7,7 +7,13 @@ import { where, } from 'firebase/firestore' import { DateDoc, Post } from 'common/post' -import { coll, getValue, getValues, listenForValue } from './utils' +import { + coll, + getValue, + getValues, + listenForValue, + listenForValues, +} from './utils' import { getUserByUsername } from './users' export const posts = coll<Post>('posts') @@ -51,6 +57,11 @@ export async function getDateDocs() { return getValues<DateDoc>(q) } +export function listenForDateDocs(setDateDocs: (dateDocs: DateDoc[]) => void) { + const q = query(posts, where('type', '==', 'date-doc')) + return listenForValues<DateDoc>(q, setDateDocs) +} + export async function getDateDoc(username: string) { const user = await getUserByUsername(username) if (!user) return null diff --git a/web/pages/date-docs/index.tsx b/web/pages/date-docs/index.tsx index 48e0bb13..f25746ee 100644 --- a/web/pages/date-docs/index.tsx +++ b/web/pages/date-docs/index.tsx @@ -13,6 +13,7 @@ import { SiteLink } from 'web/components/site-link' import { getUser, User } from 'web/lib/firebase/users' import { DateDocPost } from './[username]' import { NoSEO } from 'web/components/NoSEO' +import { useDateDocs } from 'web/hooks/use-post' export async function getStaticProps() { const dateDocs = await getDateDocs() @@ -34,9 +35,10 @@ export default function DatePage(props: { dateDocs: DateDoc[] docCreators: User[] }) { - const { dateDocs, docCreators } = props + const { docCreators } = props const user = useUser() + const dateDocs = useDateDocs() ?? props.dateDocs const hasDoc = dateDocs.some((d) => d.creatorId === user?.id) return ( From 42a7d04b4dc344c55920be706000012b763dc5f6 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 6 Oct 2022 20:17:26 -0400 Subject: [PATCH 17/17] Tag ArbitrageBot with bot badge --- web/components/user-link.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/user-link.tsx b/web/components/user-link.tsx index c3a273fc..d7f660ae 100644 --- a/web/components/user-link.tsx +++ b/web/components/user-link.tsx @@ -39,7 +39,7 @@ export function UserLink(props: { ) } -const BOT_USERNAMES = ['v'] +const BOT_USERNAMES = ['v', 'ArbitrageBot'] function BotBadge() { return (