diff --git a/functions/src/create-contract.ts b/functions/src/create-contract.ts index 786ee8ae..77d9f25a 100644 --- a/functions/src/create-contract.ts +++ b/functions/src/create-contract.ts @@ -31,8 +31,9 @@ import { User } from '../../common/user' import { Group, MAX_ID_LENGTH } from '../../common/group' import { getPseudoProbability } from '../../common/pseudo-numeric' import { JSONContent } from '@tiptap/core' -import { zip } from 'lodash' +import { uniq, zip } from 'lodash' import { Bet } from 'common/bet' +import { createGroupLinks } from 'functions/src/on-update-group' const descScehma: z.ZodType = z.lazy(() => z.intersection( @@ -145,16 +146,22 @@ export const createmarket = newEndpoint({}, async (req, auth) => { } group = groupDoc.data() as Group - if (!group.memberIds.includes(user.id)) { + if ( + !group.memberIds.includes(user.id) && + !group.anyoneCanJoin && + group.creatorId !== user.id + ) { throw new APIError( 400, - 'User must be a member of the group to add markets to it.' + '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 groupDocRef.update({ - contractIds: [...group.contractIds, contractRef.id], + contractIds: uniq([...group.contractIds, contractRef.id]), }) + // We'll update the group links manually here bc we have the user's id and won't in the trigger + await createGroupLinks(group, [contractRef.id], auth.uid) } console.log( diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index 3ab2a249..58da0af1 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -1,13 +1,16 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { Group } from '../../common/group' +import { Group, GroupLink } from '../../common/group' +import { getContract } from './utils' +import { uniq } from 'lodash' const firestore = admin.firestore() export const onUpdateGroup = functions.firestore .document('groups/{groupId}') - .onUpdate(async (change) => { + .onUpdate(async (change, context) => { const prevGroup = change.before.data() as Group const group = change.after.data() as Group + const userId = context.auth?.uid // ignore the update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) @@ -21,9 +24,73 @@ export const onUpdateGroup = functions.firestore //TODO: create notification with isSeeOnHref set to the group's /group/slug/questions url // but first, let the new /group/slug/chat notification permeate so that we can differentiate between the two } + if (prevGroup.contractIds.length != group.contractIds.length) { + if (prevGroup.contractIds.length < group.contractIds.length) + await createGroupLinks( + group, + group.contractIds.slice(prevGroup.contractIds.length), + userId + ) + else + await removeGroupLinks( + group, + group.contractIds.slice(prevGroup.contractIds.length) + ) + } await firestore .collection('groups') .doc(group.id) .update({ mostRecentActivityTime: Date.now() }) }) + +export 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) && + contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id) + ) + continue + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), + groupLinks: [ + { + groupId: group.id, + name: group.name, + slug: group.slug, + userId, + createdTime: Date.now(), + } as GroupLink, + contract?.groupLinks?.filter((link) => link.groupId !== group.id) ?? + [], + ], + }) + } +} + +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..edafef48 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' @@ -37,7 +38,7 @@ export function ContractGroupsList(props: { ignoreGroupIds: groupLinks.map((g) => g.groupId), }} setSelectedGroup={(group) => - group && addContractToGroup(group, contract, user.id) + group && addContractToGroup(group, contract.id, user.id) } selectedGroup={undefined} creator={user} @@ -57,11 +58,13 @@ 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..f5cf3542 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -9,9 +9,10 @@ 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' +import { uniq } from 'lodash' export function GroupSelector(props: { selectedGroup: Group | undefined @@ -27,10 +28,12 @@ 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 availableGroups = uniq( + useOpenGroups() + .concat(useMemberGroups(creator?.id) ?? []) + .filter((group) => !ignoreGroupIds?.includes(group.id)) ) - const filteredGroups = memberGroups.filter((group) => + 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..4aa5368a 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, @@ -16,7 +15,6 @@ import { listenForValue, listenForValues, } from './utils' -import { Contract } from 'common/contract' export const groups = coll('groups') @@ -52,6 +50,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)) } @@ -126,29 +131,14 @@ export async function leaveGroup(group: Group, userId: string): Promise { export async function addContractToGroup( group: Group, - contract: Contract, + contractId: string, 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 - await updateContract(contract.id, { - groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), - groupLinks: newGroupLinks, - }) - } - if (!group.contractIds.includes(contract.id)) { + if (!group.contractIds.includes(contractId)) { return await updateGroup(group, { - contractIds: uniq([...group.contractIds, contract.id]), + contractIds: uniq([...group.contractIds, contractId]), }) .then(() => group) .catch((err) => { @@ -160,21 +150,13 @@ export async function addContractToGroup( export async function removeContractFromGroup( group: Group, - contract: Contract + contractId: string, + userId: string ) { - if (contract.groupLinks?.map((l) => l.groupId).includes(group.id)) { - const newGroupLinks = contract.groupLinks?.filter( - (link) => link.slug !== group.slug - ) - await updateContract(contract.id, { - groupSlugs: - contract.groupSlugs?.filter((slug) => slug !== group.slug) ?? [], - groupLinks: newGroupLinks ?? [], - }) - } + if (!canModifyGroupContracts(group, userId)) return - if (group.contractIds.includes(contract.id)) { - const newContractIds = group.contractIds.filter((id) => id !== contract.id) + if (group.contractIds.includes(contractId)) { + const newContractIds = group.contractIds.filter((id) => id !== contractId) return await updateGroup(group, { contractIds: uniq(newContractIds), }) @@ -186,29 +168,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 84ac82da..14058a7d 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 { addContractToGroup, 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,10 @@ export function NewContract(props: { useEffect(() => { if (groupId && creator) getGroup(groupId).then((group) => { - if (group && group.memberIds.includes(creator.id)) { + if ( + group && + (group.memberIds.includes(creator.id) || group.anyoneCanJoin) + ) { setSelectedGroup(group) setShowGroupSelector(false) } @@ -240,7 +243,7 @@ export function NewContract(props: { isFree: false, }) if (result && selectedGroup) { - await setContractGroupLinks(selectedGroup, result.id, creator.id) + await addContractToGroup(selectedGroup, result.id, creator.id) } await router.push(contractPath(result as Contract)) diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index dd712a36..bd5fc4d9 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -555,7 +555,7 @@ function AddContractButton(props: { group: Group; user: User }) { Promise.all( contracts.map(async (contract) => { setLoading(true) - await addContractToGroup(group, contract, user.id) + await addContractToGroup(group, contract.id, user.id) }) ).then(() => { setLoading(false)