Merge branch 'main' into austin/dc-hackathon

This commit is contained in:
Austin Chen 2022-10-06 11:43:58 -04:00
commit a2091a7cb3
9 changed files with 317 additions and 258 deletions

View File

@ -39,3 +39,4 @@ export type GroupLink = {
createdTime: number createdTime: number
userId?: string userId?: string
} }
export type GroupContractDoc = { contractId: string; createdTime: number }

View File

@ -15,7 +15,7 @@ import {
import { slugify } from '../../common/util/slugify' import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random' import { randomString } from '../../common/util/random'
import { chargeUser, getContract, isProd } from './utils' import { isProd } from './utils'
import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api' import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy' 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 { JSONContent } from '@tiptap/core'
import { uniq, zip } from 'lodash' import { uniq, zip } from 'lodash'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { FieldValue } from 'firebase-admin/firestore' import { FieldValue, Transaction } from 'firebase-admin/firestore'
const descScehma: z.ZodType<JSONContent> = z.lazy(() => const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection( z.intersection(
@ -107,6 +107,7 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
visibility = 'public', visibility = 'public',
} = validate(bodySchema, body) } = 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') { if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
@ -115,7 +116,8 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
if (max - min <= 0.01 || initialValue <= min || initialValue >= max) if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
throw new APIError(400, 'Invalid range.') 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 (initialProb < 1 || initialProb > 99)
if (outcomeType === 'PSEUDO_NUMERIC') if (outcomeType === 'PSEUDO_NUMERIC')
@ -134,7 +136,7 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
;({ answers } = validate(multipleChoiceSchema, body)) ;({ answers } = validate(multipleChoiceSchema, body))
} }
const userDoc = await firestore.collection('users').doc(auth.uid).get() const userDoc = await trans.get(firestore.collection('users').doc(auth.uid))
if (!userDoc.exists) { if (!userDoc.exists) {
throw new APIError(400, 'No user exists with the authenticated user ID.') throw new APIError(400, 'No user exists with the authenticated user ID.')
} }
@ -150,15 +152,15 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
let group: Group | null = null let group: Group | null = null
if (groupId) { if (groupId) {
const groupDocRef = firestore.collection('groups').doc(groupId) const groupDocRef = firestore.collection('groups').doc(groupId)
const groupDoc = await groupDocRef.get() const groupDoc = await trans.get(groupDocRef)
if (!groupDoc.exists) { if (!groupDoc.exists) {
throw new APIError(400, 'No group exists with the given group ID.') throw new APIError(400, 'No group exists with the given group ID.')
} }
group = groupDoc.data() as Group group = groupDoc.data() as Group
const groupMembersSnap = await firestore const groupMembersSnap = await trans.get(
.collection(`groups/${groupId}/groupMembers`) firestore.collection(`groups/${groupId}/groupMembers`)
.get() )
const groupMemberDocs = groupMembersSnap.docs.map( const groupMemberDocs = groupMembersSnap.docs.map(
(doc) => doc.data() as { userId: string; createdTime: number } (doc) => doc.data() as { userId: string; createdTime: number }
) )
@ -173,7 +175,7 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
) )
} }
} }
const slug = await getSlug(question) const slug = await getSlug(trans, question)
const contractRef = firestore.collection('contracts').doc() const contractRef = firestore.collection('contracts').doc()
console.log( console.log(
@ -224,28 +226,36 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID : DEV_HOUSE_LIQUIDITY_PROVIDER_ID
: user.id : user.id
if (ante) await chargeUser(providerId, ante, true) if (ante) {
if (deservesFreeMarket) const delta = FieldValue.increment(-ante)
await firestore const providerDoc = firestore.collection('users').doc(providerId)
.collection('users') await trans.update(providerDoc, { balance: delta, totalDeposits: delta })
.doc(user.id) }
.update({ freeMarketsCreated: FieldValue.increment(1) })
if (deservesFreeMarket) {
await trans.update(firestore.collection('users').doc(user.id), {
freeMarketsCreated: FieldValue.increment(1),
})
}
await contractRef.create(contract) await contractRef.create(contract)
if (group != null) { if (group != null) {
const groupContractsSnap = await firestore const groupContractsSnap = await trans.get(
.collection(`groups/${groupId}/groupContracts`) firestore.collection(`groups/${groupId}/groupContracts`)
.get() )
const groupContracts = groupContractsSnap.docs.map( const groupContracts = groupContractsSnap.docs.map(
(doc) => doc.data() as { contractId: string; createdTime: number } (doc) => doc.data() as { contractId: string; createdTime: number }
) )
if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) {
await createGroupLinks(group, [contractRef.id], auth.uid) await createGroupLinks(trans, group, [contractRef.id], auth.uid)
const groupContractRef = firestore const groupContractRef = firestore
.collection(`groups/${groupId}/groupContracts`) .collection(`groups/${groupId}/groupContracts`)
.doc(contract.id) .doc(contract.id)
await groupContractRef.set({
await trans.set(groupContractRef, {
contractId: contract.id, contractId: contract.id,
createdTime: Date.now(), createdTime: Date.now(),
}) })
@ -264,7 +274,7 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
ante ante
) )
await liquidityDoc.set(lp) await trans.set(liquidityDoc, lp)
} else if (outcomeType === 'MULTIPLE_CHOICE') { } else if (outcomeType === 'MULTIPLE_CHOICE') {
const betCol = firestore.collection(`contracts/${contract.id}/bets`) const betCol = firestore.collection(`contracts/${contract.id}/bets`)
const betDocs = (answers ?? []).map(() => betCol.doc()) const betDocs = (answers ?? []).map(() => betCol.doc())
@ -282,21 +292,23 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
) )
await Promise.all( await Promise.all(
zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet)) zip(bets, betDocs).map(([bet, doc]) =>
doc ? trans.create(doc, bet as Bet) : undefined
)
) )
await Promise.all( await Promise.all(
zip(answerObjects, answerDocs).map(([answer, doc]) => zip(answerObjects, answerDocs).map(([answer, doc]) =>
doc?.create(answer as Answer) doc ? trans.create(doc, answer as Answer) : undefined
) )
) )
await contractRef.update({ answers: answerObjects }) await trans.update(contractRef, { answers: answerObjects })
} else if (outcomeType === 'FREE_RESPONSE') { } else if (outcomeType === 'FREE_RESPONSE') {
const noneAnswerDoc = firestore const noneAnswerDoc = firestore
.collection(`contracts/${contract.id}/answers`) .collection(`contracts/${contract.id}/answers`)
.doc('0') .doc('0')
const noneAnswer = getNoneAnswer(contract.id, user) const noneAnswer = getNoneAnswer(contract.id, user)
await noneAnswerDoc.set(noneAnswer) await trans.set(noneAnswerDoc, noneAnswer)
const anteBetDoc = firestore const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`) .collection(`contracts/${contract.id}/bets`)
@ -307,7 +319,7 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
contract as FreeResponseContract, contract as FreeResponseContract,
anteBetDoc.id anteBetDoc.id
) )
await anteBetDoc.set(anteBet) await trans.set(anteBetDoc, anteBet)
} else if (outcomeType === 'NUMERIC') { } else if (outcomeType === 'NUMERIC') {
const anteBetDoc = firestore const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`) .collection(`contracts/${contract.id}/bets`)
@ -320,16 +332,17 @@ export async function createMarketHelper(body: any, auth: AuthedUser) {
anteBetDoc.id anteBetDoc.id
) )
await anteBetDoc.set(anteBet) await trans.set(anteBetDoc, anteBet)
} }
return contract return contract
})
} }
const getSlug = async (question: string) => { const getSlug = async (trans: Transaction, question: string) => {
const proposedSlug = slugify(question) const proposedSlug = slugify(question)
const preexistingContract = await getContractFromSlug(proposedSlug) const preexistingContract = await getContractFromSlug(trans, proposedSlug)
return preexistingContract return preexistingContract
? proposedSlug + '-' + randomString() ? proposedSlug + '-' + randomString()
@ -338,35 +351,31 @@ const getSlug = async (question: string) => {
const firestore = admin.firestore() const firestore = admin.firestore()
export async function getContractFromSlug(slug: string) { async function getContractFromSlug(trans: Transaction, slug: string) {
const snap = await firestore const snap = await trans.get(
.collection('contracts') firestore.collection('contracts').where('slug', '==', slug)
.where('slug', '==', slug) )
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Contract) return snap.empty ? undefined : (snap.docs[0].data() as Contract)
} }
async function createGroupLinks( async function createGroupLinks(
trans: Transaction,
group: Group, group: Group,
contractIds: string[], contractIds: string[],
userId: string userId: string
) { ) {
for (const contractId of contractIds) { for (const contractId of contractIds) {
const contract = await getContract(contractId) const contractRef = firestore.collection('contracts').doc(contractId)
const contract = (await trans.get(contractRef)).data() as Contract
if (!contract?.groupSlugs?.includes(group.slug)) { if (!contract?.groupSlugs?.includes(group.slug)) {
await firestore await trans.update(contractRef, {
.collection('contracts')
.doc(contractId)
.update({
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
}) })
} }
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) { if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
await firestore await trans.update(contractRef, {
.collection('contracts')
.doc(contractId)
.update({
groupLinks: [ groupLinks: [
{ {
groupId: group.id, groupId: group.id,

View File

@ -2,6 +2,8 @@ import * as functions from 'firebase-functions'
import { getUser } from './utils' import { getUser } from './utils'
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification' import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { GroupContractDoc } from '../../common/group'
import * as admin from 'firebase-admin'
export const onUpdateContract = functions.firestore export const onUpdateContract = functions.firestore
.document('contracts/{contractId}') .document('contracts/{contractId}')
@ -9,17 +11,14 @@ export const onUpdateContract = functions.firestore
const contract = change.after.data() as Contract const contract = change.after.data() as Contract
const previousContract = change.before.data() as Contract const previousContract = change.before.data() as Contract
const { eventId } = context const { eventId } = context
const { openCommentBounties, closeTime, question } = contract const { closeTime, question } = contract
if ( if (!previousContract.isResolved && contract.isResolved) {
!previousContract.isResolved &&
contract.isResolved &&
(openCommentBounties ?? 0) > 0
) {
// No need to notify users of resolution, that's handled in resolve-market // No need to notify users of resolution, that's handled in resolve-market
return return
} } else if (previousContract.groupSlugs !== contract.groupSlugs) {
if ( await handleContractGroupUpdated(previousContract, contract)
} else if (
previousContract.closeTime !== closeTime || previousContract.closeTime !== closeTime ||
previousContract.question !== question previousContract.question !== question
) { ) {
@ -51,3 +50,43 @@ async function handleUpdatedCloseTime(
contract contract
) )
} }
async function handleContractGroupUpdated(
previousContract: Contract,
contract: Contract
) {
const prevLength = previousContract.groupSlugs?.length ?? 0
const newLength = contract.groupSlugs?.length ?? 0
if (prevLength < newLength) {
// Contract was added to a new group
const groupId = contract.groupLinks?.find(
(link) =>
!previousContract.groupLinks
?.map((l) => l.groupId)
.includes(link.groupId)
)?.groupId
if (!groupId) throw new Error('Could not find new group id')
await firestore
.collection(`groups/${groupId}/groupContracts`)
.doc(contract.id)
.set({
contractId: contract.id,
createdTime: Date.now(),
} as GroupContractDoc)
}
if (prevLength > newLength) {
// Contract was removed from a group
const groupId = previousContract.groupLinks?.find(
(link) =>
!contract.groupLinks?.map((l) => l.groupId).includes(link.groupId)
)?.groupId
if (!groupId) throw new Error('Could not find old group id')
await firestore
.collection(`groups/${groupId}/groupContracts`)
.doc(contract.id)
.delete()
}
}
const firestore = admin.firestore()

View File

@ -89,17 +89,20 @@ const getGroups = async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async function updateTotalContractsAndMembers() { async function updateTotalContractsAndMembers() {
const groups = await getGroups() const groups = await getGroups()
for (const group of groups) { await Promise.all(
groups.map(async (group) => {
log('updating group total contracts and members', group.slug) log('updating group total contracts and members', group.slug)
const groupRef = admin.firestore().collection('groups').doc(group.id) const groupRef = admin.firestore().collection('groups').doc(group.id)
const totalMembers = (await groupRef.collection('groupMembers').get()).size const totalMembers = (await groupRef.collection('groupMembers').get())
.size
const totalContracts = (await groupRef.collection('groupContracts').get()) const totalContracts = (await groupRef.collection('groupContracts').get())
.size .size
await groupRef.update({ await groupRef.update({
totalMembers, totalMembers,
totalContracts, totalContracts,
}) })
} })
)
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async function removeUnusedMemberAndContractFields() { async function removeUnusedMemberAndContractFields() {
@ -117,6 +120,6 @@ async function removeUnusedMemberAndContractFields() {
if (require.main === module) { if (require.main === module) {
initAdmin() initAdmin()
// convertGroupFieldsToGroupDocuments() // convertGroupFieldsToGroupDocuments()
// updateTotalContractsAndMembers() updateTotalContractsAndMembers()
removeUnusedMemberAndContractFields() // removeUnusedMemberAndContractFields()
} }

View File

@ -5,7 +5,7 @@ export function Card(props: JSX.IntrinsicElements['div']) {
return ( return (
<div <div
className={clsx( className={clsx(
'cursor-pointer rounded-lg border-2 bg-white transition-shadow hover:shadow-md focus:shadow-md', 'cursor-pointer rounded-lg border bg-white transition-shadow hover:shadow-md focus:shadow-md',
className className
)} )}
{...rest} {...rest}

View File

@ -80,8 +80,8 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
const { contract } = props const { contract } = props
const tips = useTipTxns({ contractId: contract.id }) const tips = useTipTxns({ contractId: contract.id })
const comments = useComments(contract.id) ?? props.comments const comments = useComments(contract.id) ?? props.comments
const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Newest', { const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Best', {
key: `contract-${contract.id}-comments-sort`, key: `contract-comments-sort`,
store: storageStore(safeLocalStorage()), store: storageStore(safeLocalStorage()),
}) })
const me = useUser() const me = useUser()

View File

@ -72,6 +72,7 @@ export default function GroupSelectorDialog(props: {
) : ( ) : (
displayedGroups.map((group) => ( displayedGroups.map((group) => (
<PillButton <PillButton
key={group.id}
selected={memberGroupIds.includes(group.id)} selected={memberGroupIds.includes(group.id)}
onSelect={withTracking( onSelect={withTracking(
() => () =>

View File

@ -191,6 +191,7 @@ export async function leaveGroup(group: Group, userId: string): Promise<void> {
return await deleteDoc(memberDoc) return await deleteDoc(memberDoc)
} }
// TODO: This doesn't check if the user has permission to do this
export async function addContractToGroup( export async function addContractToGroup(
group: Group, group: Group,
contract: Contract, contract: Contract,
@ -211,15 +212,9 @@ export async function addContractToGroup(
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks, groupLinks: newGroupLinks,
}) })
// create new contract document in groupContracts collection
const contractDoc = doc(groupContracts(group.id), contract.id)
await setDoc(contractDoc, {
contractId: contract.id,
createdTime: Date.now(),
})
} }
// TODO: This doesn't check if the user has permission to do this
export async function removeContractFromGroup( export async function removeContractFromGroup(
group: Group, group: Group,
contract: Contract contract: Contract
@ -234,10 +229,6 @@ export async function removeContractFromGroup(
groupLinks: newGroupLinks ?? [], groupLinks: newGroupLinks ?? [],
}) })
} }
// delete the contract document in groupContracts collection
const contractDoc = doc(groupContracts(group.id), contract.id)
await deleteDoc(contractDoc)
} }
export function getGroupLinkToDisplay(contract: Contract) { export function getGroupLinkToDisplay(contract: Contract) {

View File

@ -1,3 +1,6 @@
import { useEffect } from 'react'
import Router from 'next/router'
import { Contract, getTrendingContracts } from 'web/lib/firebase/contracts' import { Contract, getTrendingContracts } from 'web/lib/firebase/contracts'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
import { LandingPagePanel } from 'web/components/landing-page-panel' import { LandingPagePanel } from 'web/components/landing-page-panel'
@ -6,6 +9,7 @@ import { ManifoldLogo } from 'web/components/nav/manifold-logo'
import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth' import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth'
import { useSaveReferral } from 'web/hooks/use-save-referral' import { useSaveReferral } from 'web/hooks/use-save-referral'
import { SEO } from 'web/components/SEO' import { SEO } from 'web/components/SEO'
import { useUser } from 'web/hooks/use-user'
export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => {
const hotContracts = await getTrendingContracts() const hotContracts = await getTrendingContracts()
@ -16,6 +20,7 @@ export default function Home(props: { hotContracts: Contract[] }) {
const { hotContracts } = props const { hotContracts } = props
useSaveReferral() useSaveReferral()
useRedirectAfterLogin()
return ( return (
<Page> <Page>
@ -35,3 +40,13 @@ export default function Home(props: { hotContracts: Contract[] }) {
</Page> </Page>
) )
} }
const useRedirectAfterLogin = () => {
const user = useUser()
useEffect(() => {
if (user) {
Router.replace('/home')
}
}, [user])
}