Groups contracts (#709)

* Update group links in trigger and api

* Remove extra call during creation

* Remove grouplinks on frontend

* Deserialize

* Consolidate logic

* Move function locally
This commit is contained in:
Ian Philips 2022-08-01 21:15:09 -06:00 committed by GitHub
parent b4e8c5d602
commit 0b06ded5e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 149 additions and 82 deletions

View File

@ -14,7 +14,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 } from './utils' import { chargeUser, getContract } from './utils'
import { APIError, newEndpoint, validate, zTimestamp } from './api' import { APIError, newEndpoint, validate, zTimestamp } from './api'
import { import {
@ -28,11 +28,11 @@ import { Answer, getNoneAnswer } from '../../common/answer'
import { getNewContract } from '../../common/new-contract' import { getNewContract } from '../../common/new-contract'
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
import { User } from '../../common/user' import { User } from '../../common/user'
import { Group, MAX_ID_LENGTH } from '../../common/group' import { Group, GroupLink, MAX_ID_LENGTH } from '../../common/group'
import { getPseudoProbability } from '../../common/pseudo-numeric' import { getPseudoProbability } from '../../common/pseudo-numeric'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { zip } from 'lodash' import { uniq, zip } from 'lodash'
import { Bet } from 'common/bet' import { Bet } from '../../common/bet'
const descScehma: z.ZodType<JSONContent> = z.lazy(() => const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection( z.intersection(
@ -136,27 +136,6 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
const slug = await getSlug(question) const slug = await getSlug(question)
const contractRef = firestore.collection('contracts').doc() const contractRef = firestore.collection('contracts').doc()
let group = 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.')
}
group = groupDoc.data() as Group
if (!group.memberIds.includes(user.id)) {
throw new APIError(
400,
'User must be a member of the group to add markets to it.'
)
}
if (!group.contractIds.includes(contractRef.id))
await groupDocRef.update({
contractIds: [...group.contractIds, contractRef.id],
})
}
console.log( console.log(
'creating contract for', 'creating contract for',
user.username, user.username,
@ -188,6 +167,33 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
await contractRef.create(contract) await contractRef.create(contract)
let group = 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.')
}
group = groupDoc.data() as Group
if (
!group.memberIds.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.'
)
}
if (!group.contractIds.includes(contractRef.id)) {
await createGroupLinks(group, [contractRef.id], auth.uid)
await groupDocRef.update({
contractIds: uniq([...group.contractIds, contractRef.id]),
})
}
}
const providerId = user.id const providerId = user.id
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
@ -284,3 +290,38 @@ export async function getContractFromSlug(slug: string) {
return snap.empty ? undefined : (snap.docs[0].data() as Contract) return snap.empty ? undefined : (snap.docs[0].data() as Contract)
} }
async function createGroupLinks(
group: Group,
contractIds: string[],
userId: string
) {
for (const contractId of contractIds) {
const contract = await getContract(contractId)
if (!contract?.groupSlugs?.includes(group.slug)) {
await firestore
.collection('contracts')
.doc(contractId)
.update({
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
})
}
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
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 ?? []),
],
})
}
}
}

View File

@ -1,6 +1,8 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { Group } from '../../common/group' import { Group } from '../../common/group'
import { getContract } from './utils'
import { uniq } from 'lodash'
const firestore = admin.firestore() const firestore = admin.firestore()
export const onUpdateGroup = functions.firestore export const onUpdateGroup = functions.firestore
@ -9,7 +11,7 @@ export const onUpdateGroup = functions.firestore
const prevGroup = change.before.data() as Group const prevGroup = change.before.data() as Group
const group = change.after.data() as Group const group = change.after.data() as Group
// ignore the update we just made // Ignore the activity update we just made
if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime)
return return
@ -27,3 +29,23 @@ export const onUpdateGroup = functions.firestore
.doc(group.id) .doc(group.id)
.update({ mostRecentActivityTime: Date.now() }) .update({ mostRecentActivityTime: Date.now() })
}) })
export async function removeGroupLinks(group: Group, contractIds: string[]) {
for (const contractId of contractIds) {
const contract = await getContract(contractId)
await firestore
.collection('contracts')
.doc(contractId)
.update({
groupSlugs: uniq([
...(contract?.groupSlugs?.filter((slug) => slug !== group.slug) ??
[]),
]),
groupLinks: [
...(contract?.groupLinks?.filter(
(link) => link.groupId !== group.id
) ?? []),
],
})
}
}

View File

@ -7,6 +7,7 @@ import { Button } from 'web/components/button'
import { GroupSelector } from 'web/components/groups/group-selector' import { GroupSelector } from 'web/components/groups/group-selector'
import { import {
addContractToGroup, addContractToGroup,
canModifyGroupContracts,
removeContractFromGroup, removeContractFromGroup,
} from 'web/lib/firebase/groups' } from 'web/lib/firebase/groups'
import { User } from 'common/user' import { User } from 'common/user'
@ -57,11 +58,11 @@ export function ContractGroupsList(props: {
<Row className="line-clamp-1 items-center gap-2"> <Row className="line-clamp-1 items-center gap-2">
<GroupLinkItem group={group} /> <GroupLinkItem group={group} />
</Row> </Row>
{user && group.memberIds.includes(user.id) && ( {user && canModifyGroupContracts(group, user.id) && (
<Button <Button
color={'gray-white'} color={'gray-white'}
size={'xs'} size={'xs'}
onClick={() => removeContractFromGroup(group, contract)} onClick={() => removeContractFromGroup(group, contract, user.id)}
> >
<XIcon className="h-4 w-4 text-gray-500" /> <XIcon className="h-4 w-4 text-gray-500" />
</Button> </Button>

View File

@ -9,7 +9,7 @@ import {
import clsx from 'clsx' import clsx from 'clsx'
import { CreateGroupButton } from 'web/components/groups/create-group-button' import { CreateGroupButton } from 'web/components/groups/create-group-button'
import { useState } from 'react' import { useState } from 'react'
import { useMemberGroups } from 'web/hooks/use-group' import { useMemberGroups, useOpenGroups } from 'web/hooks/use-group'
import { User } from 'common/user' import { User } from 'common/user'
import { searchInAny } from 'common/util/parse' import { searchInAny } from 'common/util/parse'
@ -27,10 +27,15 @@ export function GroupSelector(props: {
const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false)
const { showSelector, showLabel, ignoreGroupIds } = options const { showSelector, showLabel, ignoreGroupIds } = options
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const memberGroups = (useMemberGroups(creator?.id) ?? []).filter( const openGroups = useOpenGroups()
(group) => !ignoreGroupIds?.includes(group.id) const availableGroups = openGroups
.concat(
(useMemberGroups(creator?.id) ?? []).filter(
(g) => !openGroups.map((og) => og.id).includes(g.id)
) )
const filteredGroups = memberGroups.filter((group) => )
.filter((group) => !ignoreGroupIds?.includes(group.id))
const filteredGroups = availableGroups.filter((group) =>
searchInAny(query, group.name) searchInAny(query, group.name)
) )

View File

@ -5,6 +5,7 @@ import {
listenForGroup, listenForGroup,
listenForGroups, listenForGroups,
listenForMemberGroups, listenForMemberGroups,
listenForOpenGroups,
listGroups, listGroups,
} from 'web/lib/firebase/groups' } from 'web/lib/firebase/groups'
import { getUser, getUsers } from 'web/lib/firebase/users' import { getUser, getUsers } from 'web/lib/firebase/users'
@ -32,6 +33,16 @@ export const useGroups = () => {
return groups return groups
} }
export const useOpenGroups = () => {
const [groups, setGroups] = useState<Group[]>([])
useEffect(() => {
return listenForOpenGroups(setGroups)
}, [])
return groups
}
export const useMemberGroups = ( export const useMemberGroups = (
userId: string | null | undefined, userId: string | null | undefined,
options?: { withChatEnabled: boolean }, options?: { withChatEnabled: boolean },

View File

@ -8,7 +8,6 @@ import {
} from 'firebase/firestore' } from 'firebase/firestore'
import { sortBy, uniq } from 'lodash' import { sortBy, uniq } from 'lodash'
import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group' import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group'
import { updateContract } from './contracts'
import { import {
coll, coll,
getValue, getValue,
@ -17,6 +16,7 @@ import {
listenForValues, listenForValues,
} from './utils' } from './utils'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { updateContract } from 'web/lib/firebase/contracts'
export const groups = coll<Group>('groups') export const groups = coll<Group>('groups')
@ -52,6 +52,13 @@ export function listenForGroups(setGroups: (groups: Group[]) => void) {
return listenForValues(groups, setGroups) return listenForValues(groups, setGroups)
} }
export function listenForOpenGroups(setGroups: (groups: Group[]) => void) {
return listenForValues(
query(groups, where('anyoneCanJoin', '==', true)),
setGroups
)
}
export function getGroup(groupId: string) { export function getGroup(groupId: string) {
return getValue<Group>(doc(groups, groupId)) return getValue<Group>(doc(groups, groupId))
} }
@ -129,7 +136,7 @@ export async function addContractToGroup(
contract: Contract, contract: Contract,
userId: string userId: string
) { ) {
if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { if (!canModifyGroupContracts(group, userId)) return
const newGroupLinks = [ const newGroupLinks = [
...(contract.groupLinks ?? []), ...(contract.groupLinks ?? []),
{ {
@ -140,12 +147,12 @@ export async function addContractToGroup(
name: group.name, name: group.name,
} as GroupLink, } as GroupLink,
] ]
// It's good to update the contract first, so the on-update-group trigger doesn't re-add them
await updateContract(contract.id, { await updateContract(contract.id, {
groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]),
groupLinks: newGroupLinks, groupLinks: newGroupLinks,
}) })
}
if (!group.contractIds.includes(contract.id)) { if (!group.contractIds.includes(contract.id)) {
return await updateGroup(group, { return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contract.id]), contractIds: uniq([...group.contractIds, contract.id]),
@ -160,8 +167,11 @@ export async function addContractToGroup(
export async function removeContractFromGroup( export async function removeContractFromGroup(
group: Group, group: Group,
contract: Contract contract: Contract,
userId: string
) { ) {
if (!canModifyGroupContracts(group, userId)) return
if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) {
const newGroupLinks = contract.groupLinks?.filter( const newGroupLinks = contract.groupLinks?.filter(
(link) => link.slug !== group.slug (link) => link.slug !== group.slug
@ -186,29 +196,10 @@ export async function removeContractFromGroup(
} }
} }
export async function setContractGroupLinks( export function canModifyGroupContracts(group: Group, userId: string) {
group: Group, return (
contractId: string, group.creatorId === userId ||
userId: string group.memberIds.includes(userId) ||
) { group.anyoneCanJoin
await updateContract(contractId, { )
groupSlugs: [group.slug],
groupLinks: [
{
groupId: group.id,
name: group.name,
slug: group.slug,
userId,
createdTime: Date.now(),
} as GroupLink,
],
})
return await updateGroup(group, {
contractIds: uniq([...group.contractIds, contractId]),
})
.then(() => group)
.catch((err) => {
console.error('error adding contract to group', err)
return err
})
} }

View File

@ -19,7 +19,7 @@ import {
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { removeUndefinedProps } from 'common/util/object' import { removeUndefinedProps } from 'common/util/object'
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
import { getGroup, setContractGroupLinks } from 'web/lib/firebase/groups' import { canModifyGroupContracts, getGroup } from 'web/lib/firebase/groups'
import { Group } from 'common/group' import { Group } from 'common/group'
import { useTracking } from 'web/hooks/use-tracking' import { useTracking } from 'web/hooks/use-tracking'
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
@ -122,7 +122,7 @@ export function NewContract(props: {
useEffect(() => { useEffect(() => {
if (groupId && creator) if (groupId && creator)
getGroup(groupId).then((group) => { getGroup(groupId).then((group) => {
if (group && group.memberIds.includes(creator.id)) { if (group && canModifyGroupContracts(group, creator.id)) {
setSelectedGroup(group) setSelectedGroup(group)
setShowGroupSelector(false) setShowGroupSelector(false)
} }
@ -239,10 +239,6 @@ export function NewContract(props: {
selectedGroup: selectedGroup?.id, selectedGroup: selectedGroup?.id,
isFree: false, isFree: false,
}) })
if (result && selectedGroup) {
await setContractGroupLinks(selectedGroup, result.id, creator.id)
}
await router.push(contractPath(result as Contract)) await router.push(contractPath(result as Contract))
} catch (e) { } catch (e) {
console.error('error creating contract', e, (e as any).details) console.error('error creating contract', e, (e as any).details)