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:
parent
b4e8c5d602
commit
0b06ded5e5
|
@ -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 ?? []),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
) ?? []),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user