Date: Thu, 6 Oct 2022 09:26:35 -0400
Subject: [PATCH 25/44] Update groupContracts in db trigger
---
 common/group.ts                        |  1 +
 functions/src/on-update-contract.ts    | 55 ++++++++++++++++++++++----
 functions/src/scripts/update-groups.ts | 27 +++++++------
 web/lib/firebase/groups.ts             | 13 +-----
 4 files changed, 65 insertions(+), 31 deletions(-)
diff --git a/common/group.ts b/common/group.ts
index 8f5728d3..cb6660e8 100644
--- a/common/group.ts
+++ b/common/group.ts
@@ -39,3 +39,4 @@ export type GroupLink = {
   createdTime: number
   userId?: string
 }
+export type GroupContractDoc = { contractId: string; createdTime: number }
diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts
index 301d6286..1e3418fa 100644
--- a/functions/src/on-update-contract.ts
+++ b/functions/src/on-update-contract.ts
@@ -2,6 +2,8 @@ import * as functions from 'firebase-functions'
 import { getUser } from './utils'
 import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
 import { Contract } from '../../common/contract'
+import { GroupContractDoc } from '../../common/group'
+import * as admin from 'firebase-admin'
 
 export const onUpdateContract = functions.firestore
   .document('contracts/{contractId}')
@@ -9,17 +11,14 @@ export const onUpdateContract = functions.firestore
     const contract = change.after.data() as Contract
     const previousContract = change.before.data() as Contract
     const { eventId } = context
-    const { openCommentBounties, closeTime, question } = contract
+    const { closeTime, question } = contract
 
-    if (
-      !previousContract.isResolved &&
-      contract.isResolved &&
-      (openCommentBounties ?? 0) > 0
-    ) {
+    if (!previousContract.isResolved && contract.isResolved) {
       // No need to notify users of resolution, that's handled in resolve-market
       return
-    }
-    if (
+    } else if (previousContract.groupSlugs !== contract.groupSlugs) {
+      await handleContractGroupUpdated(previousContract, contract)
+    } else if (
       previousContract.closeTime !== closeTime ||
       previousContract.question !== question
     ) {
@@ -51,3 +50,43 @@ async function handleUpdatedCloseTime(
     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()
diff --git a/functions/src/scripts/update-groups.ts b/functions/src/scripts/update-groups.ts
index fc402292..56a9f399 100644
--- a/functions/src/scripts/update-groups.ts
+++ b/functions/src/scripts/update-groups.ts
@@ -89,17 +89,20 @@ const getGroups = async () => {
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 async function updateTotalContractsAndMembers() {
   const groups = await getGroups()
-  for (const group of groups) {
-    log('updating group total contracts and members', group.slug)
-    const groupRef = admin.firestore().collection('groups').doc(group.id)
-    const totalMembers = (await groupRef.collection('groupMembers').get()).size
-    const totalContracts = (await groupRef.collection('groupContracts').get())
-      .size
-    await groupRef.update({
-      totalMembers,
-      totalContracts,
+  await Promise.all(
+    groups.map(async (group) => {
+      log('updating group total contracts and members', group.slug)
+      const groupRef = admin.firestore().collection('groups').doc(group.id)
+      const totalMembers = (await groupRef.collection('groupMembers').get())
+        .size
+      const totalContracts = (await groupRef.collection('groupContracts').get())
+        .size
+      await groupRef.update({
+        totalMembers,
+        totalContracts,
+      })
     })
-  }
+  )
 }
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 async function removeUnusedMemberAndContractFields() {
@@ -117,6 +120,6 @@ async function removeUnusedMemberAndContractFields() {
 if (require.main === module) {
   initAdmin()
   // convertGroupFieldsToGroupDocuments()
-  // updateTotalContractsAndMembers()
-  removeUnusedMemberAndContractFields()
+  updateTotalContractsAndMembers()
+  // removeUnusedMemberAndContractFields()
 }
diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts
index 17e41c53..6bfc4e85 100644
--- a/web/lib/firebase/groups.ts
+++ b/web/lib/firebase/groups.ts
@@ -191,6 +191,7 @@ export async function leaveGroup(group: Group, userId: string): Promise
 {
   return await deleteDoc(memberDoc)
 }
 
+// TODO: This doesn't check if the user has permission to do this
 export async function addContractToGroup(
   group: Group,
   contract: Contract,
@@ -211,15 +212,9 @@ export async function addContractToGroup(
     groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
     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(
   group: Group,
   contract: Contract
@@ -234,10 +229,6 @@ export async function removeContractFromGroup(
       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) {
From e127f9646a08b6cf593018c738ecf9242626b075 Mon Sep 17 00:00:00 2001
From: Ian Philips 
Date: Thu, 6 Oct 2022 09:53:55 -0400
Subject: [PATCH 26/44] Default sort to best
---
 web/components/contract/contract-tabs.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx
index 9639a57a..9ce17396 100644
--- a/web/components/contract/contract-tabs.tsx
+++ b/web/components/contract/contract-tabs.tsx
@@ -80,7 +80,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
   const { contract } = props
   const tips = useTipTxns({ contractId: contract.id })
   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`,
     store: storageStore(safeLocalStorage()),
   })
From 26f04fb04a2302f907287401e5db6fb2095f46c4 Mon Sep 17 00:00:00 2001
From: Ian Philips 
Date: Thu, 6 Oct 2022 10:16:29 -0400
Subject: [PATCH 27/44] Save comment sort per user rather than per contract
---
 web/components/contract/contract-tabs.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx
index 9ce17396..fdfc5c3d 100644
--- a/web/components/contract/contract-tabs.tsx
+++ b/web/components/contract/contract-tabs.tsx
@@ -81,7 +81,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
   const tips = useTipTxns({ contractId: contract.id })
   const comments = useComments(contract.id) ?? props.comments
   const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Best', {
-    key: `contract-${contract.id}-comments-sort`,
+    key: `contract-comments-sort`,
     store: storageStore(safeLocalStorage()),
   })
   const me = useUser()
From b8d65acc3f75cc29af12bb56176a64bcb68acbdc Mon Sep 17 00:00:00 2001
From: mantikoros 
Date: Thu, 6 Oct 2022 10:54:42 -0500
Subject: [PATCH 28/44] 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 29/44] 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!
-                
-              
-                {element}
+    
+  ) : (
+    
+  )
+}
 
-                {editMode && (
-                  
 {
-                      const newPinned = group.pinnedItems.filter((item) => {
-                        return item.itemId !== group.pinnedItems[index].itemId
-                      })
-                      updateGroup(group, { pinnedItems: newPinned })
-                    }}
-                  />
-                )}
-              
-                
-                  
-                
-              
+      
+        
+        {isEditable && (
+          
+        
+          {pinned.length == 0 && !editMode && (
+            
+              
+                No pinned items yet. Click the edit button to add some!
+              
             
+              {element}
+
+              {editMode &&  onDeleteClicked(index)} />}
+            
+          ))}
+          {editMode && pinned.length < 6 && (
+            
+              
+                
+              
+            
+        }
+        onSubmit={onSubmit}
+      />
+    
No posts yet
               )}