From 0b06ded5e5b6d20ceaf39ee67b4399fa58661580 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 1 Aug 2022 21:15:09 -0600 Subject: [PATCH] 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 --- functions/src/create-contract.ts | 91 ++++++++++++++----- functions/src/on-update-group.ts | 24 ++++- .../groups/contract-groups-list.tsx | 5 +- web/components/groups/group-selector.tsx | 15 ++- web/hooks/use-group.ts | 11 +++ web/lib/firebase/groups.ts | 77 +++++++--------- web/pages/create.tsx | 8 +- 7 files changed, 149 insertions(+), 82 deletions(-) diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 786ee8ae..44ced6a8 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -14,7 +14,7 @@ import { import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' -import { chargeUser } from './utils' +import { chargeUser, getContract } from './utils' import { APIError, newEndpoint, validate, zTimestamp } from './api' import { @@ -28,11 +28,11 @@ import { Answer, getNoneAnswer } from '../../common/answer' import { getNewContract } from '../../common/new-contract' import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants' 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 { JSONContent } from '@tiptap/core' -import { zip } from 'lodash' -import { Bet } from 'common/bet' +import { uniq, zip } from 'lodash' +import { Bet } from '../../common/bet' const descScehma: z.ZodType = z.lazy(() => z.intersection( @@ -136,27 +136,6 @@ export const createmarket = newEndpoint({}, async (req, auth) => { const slug = await getSlug(question) 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( 'creating contract for', user.username, @@ -188,6 +167,33 @@ export const createmarket = newEndpoint({}, async (req, auth) => { 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 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) } + +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 ?? []), + ], + }) + } + } +} diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index 3ab2a249..7e6a5697 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -1,6 +1,8 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { Group } from '../../common/group' +import { getContract } from './utils' +import { uniq } from 'lodash' const firestore = admin.firestore() export const onUpdateGroup = functions.firestore @@ -9,7 +11,7 @@ export const onUpdateGroup = functions.firestore const prevGroup = change.before.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) return @@ -27,3 +29,23 @@ export const onUpdateGroup = functions.firestore .doc(group.id) .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 + ) ?? []), + ], + }) + } +} diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx index 423cbb97..79f2390f 100644 --- a/web/components/groups/contract-groups-list.tsx +++ b/web/components/groups/contract-groups-list.tsx @@ -7,6 +7,7 @@ import { Button } from 'web/components/button' import { GroupSelector } from 'web/components/groups/group-selector' import { addContractToGroup, + canModifyGroupContracts, removeContractFromGroup, } from 'web/lib/firebase/groups' import { User } from 'common/user' @@ -57,11 +58,11 @@ export function ContractGroupsList(props: { - {user && group.memberIds.includes(user.id) && ( + {user && canModifyGroupContracts(group, user.id) && ( diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index e6270a4d..d48256a6 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -9,7 +9,7 @@ import { import clsx from 'clsx' import { CreateGroupButton } from 'web/components/groups/create-group-button' 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 { searchInAny } from 'common/util/parse' @@ -27,10 +27,15 @@ export function GroupSelector(props: { const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) const { showSelector, showLabel, ignoreGroupIds } = options const [query, setQuery] = useState('') - const memberGroups = (useMemberGroups(creator?.id) ?? []).filter( - (group) => !ignoreGroupIds?.includes(group.id) - ) - const filteredGroups = memberGroups.filter((group) => + const openGroups = useOpenGroups() + const availableGroups = openGroups + .concat( + (useMemberGroups(creator?.id) ?? []).filter( + (g) => !openGroups.map((og) => og.id).includes(g.id) + ) + ) + .filter((group) => !ignoreGroupIds?.includes(group.id)) + const filteredGroups = availableGroups.filter((group) => searchInAny(query, group.name) ) diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 84913962..aeeaf2ab 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -5,6 +5,7 @@ import { listenForGroup, listenForGroups, listenForMemberGroups, + listenForOpenGroups, listGroups, } from 'web/lib/firebase/groups' import { getUser, getUsers } from 'web/lib/firebase/users' @@ -32,6 +33,16 @@ export const useGroups = () => { return groups } +export const useOpenGroups = () => { + const [groups, setGroups] = useState([]) + + useEffect(() => { + return listenForOpenGroups(setGroups) + }, []) + + return groups +} + export const useMemberGroups = ( userId: string | null | undefined, options?: { withChatEnabled: boolean }, diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index debc9a97..3f5d18af 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -8,7 +8,6 @@ import { } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group' -import { updateContract } from './contracts' import { coll, getValue, @@ -17,6 +16,7 @@ import { listenForValues, } from './utils' import { Contract } from 'common/contract' +import { updateContract } from 'web/lib/firebase/contracts' export const groups = coll('groups') @@ -52,6 +52,13 @@ export function listenForGroups(setGroups: (groups: Group[]) => void) { return listenForValues(groups, setGroups) } +export function listenForOpenGroups(setGroups: (groups: Group[]) => void) { + return listenForValues( + query(groups, where('anyoneCanJoin', '==', true)), + setGroups + ) +} + export function getGroup(groupId: string) { return getValue(doc(groups, groupId)) } @@ -129,23 +136,23 @@ export async function addContractToGroup( contract: Contract, userId: string ) { - if (!contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { - const newGroupLinks = [ - ...(contract.groupLinks ?? []), - { - groupId: group.id, - createdTime: Date.now(), - slug: group.slug, - userId, - name: group.name, - } as GroupLink, - ] + if (!canModifyGroupContracts(group, userId)) return + const newGroupLinks = [ + ...(contract.groupLinks ?? []), + { + groupId: group.id, + createdTime: Date.now(), + slug: group.slug, + userId, + name: group.name, + } 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, { + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + groupLinks: newGroupLinks, + }) - await updateContract(contract.id, { - groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), - groupLinks: newGroupLinks, - }) - } if (!group.contractIds.includes(contract.id)) { return await updateGroup(group, { contractIds: uniq([...group.contractIds, contract.id]), @@ -160,8 +167,11 @@ export async function addContractToGroup( export async function removeContractFromGroup( group: Group, - contract: Contract + contract: Contract, + userId: string ) { + if (!canModifyGroupContracts(group, userId)) return + if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { const newGroupLinks = contract.groupLinks?.filter( (link) => link.slug !== group.slug @@ -186,29 +196,10 @@ export async function removeContractFromGroup( } } -export async function setContractGroupLinks( - group: Group, - contractId: string, - userId: string -) { - 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 - }) +export function canModifyGroupContracts(group: Group, userId: string) { + return ( + group.creatorId === userId || + group.memberIds.includes(userId) || + group.anyoneCanJoin + ) } diff --git a/web/pages/create.tsx b/web/pages/create.tsx index ca29cba9..642cbaec 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -19,7 +19,7 @@ import { import { formatMoney } from 'common/util/format' import { removeUndefinedProps } from 'common/util/object' 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 { useTracking } from 'web/hooks/use-tracking' import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' @@ -122,7 +122,7 @@ export function NewContract(props: { useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { - if (group && group.memberIds.includes(creator.id)) { + if (group && canModifyGroupContracts(group, creator.id)) { setSelectedGroup(group) setShowGroupSelector(false) } @@ -239,10 +239,6 @@ export function NewContract(props: { selectedGroup: selectedGroup?.id, isFree: false, }) - if (result && selectedGroup) { - await setContractGroupLinks(selectedGroup, result.id, creator.id) - } - await router.push(contractPath(result as Contract)) } catch (e) { console.error('error creating contract', e, (e as any).details)