diff --git a/common/contract.ts b/common/contract.ts index b1242ab9..177af862 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -1,6 +1,7 @@ import { Answer } from './answer' import { Fees } from './fees' import { JSONContent } from '@tiptap/core' +import { GroupLink } from 'common/group' export type AnyMechanism = DPM | CPMM export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric @@ -46,6 +47,7 @@ export type Contract = { collectedFees: Fees groupSlugs?: string[] + groupLinks?: GroupLink[] uniqueBettorIds?: string[] uniqueBettorCount?: number popularityScore?: number diff --git a/common/group.ts b/common/group.ts index e367ded7..7d3215ae 100644 --- a/common/group.ts +++ b/common/group.ts @@ -19,3 +19,11 @@ export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] export const GROUP_CHAT_SLUG = 'chat' + +export type GroupLink = { + slug: string + name: string + groupId: string + createdTime: number + userId?: string +} diff --git a/firestore.rules b/firestore.rules index 96378d8b..0f28ca80 100644 --- a/firestore.rules +++ b/firestore.rules @@ -74,7 +74,7 @@ service cloud.firestore { match /contracts/{contractId} { allow read; allow update: if request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['tags', 'lowercaseTags', 'groupSlugs']); + .hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks']); allow update: if request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['description', 'closeTime', 'question']) && resource.data.creatorId == request.auth.uid; diff --git a/functions/src/on-delete-group.ts b/functions/src/on-delete-group.ts index ca833254..e5531d7b 100644 --- a/functions/src/on-delete-group.ts +++ b/functions/src/on-delete-group.ts @@ -3,6 +3,7 @@ import * as admin from 'firebase-admin' import { Group } from 'common/group' import { Contract } from 'common/contract' + const firestore = admin.firestore() export const onDeleteGroup = functions.firestore @@ -15,17 +16,21 @@ export const onDeleteGroup = functions.firestore .collection('contracts') .where('groupSlugs', 'array-contains', group.slug) .get() + console.log("contracts with group's slug:", contracts) for (const doc of contracts.docs) { const contract = doc.data() as Contract + const newGroupLinks = contract.groupLinks?.filter( + (link) => link.slug !== group.slug + ) + // remove the group from the contract await firestore .collection('contracts') .doc(contract.id) .update({ - groupSlugs: (contract.groupSlugs ?? []).filter( - (groupSlug) => groupSlug !== group.slug - ), + groupSlugs: contract.groupSlugs?.filter((s) => s !== group.slug), + groupLinks: newGroupLinks ?? [], }) } }) diff --git a/functions/src/scripts/convert-categories.ts b/functions/src/scripts/convert-categories.ts index d559bf92..3436bcbc 100644 --- a/functions/src/scripts/convert-categories.ts +++ b/functions/src/scripts/convert-categories.ts @@ -3,7 +3,7 @@ import * as admin from 'firebase-admin' import { initAdmin } from './script-init' import { getValues, isProd } from '../utils' import { CATEGORIES_GROUP_SLUG_POSTFIX } from 'common/categories' -import { Group } from 'common/group' +import { Group, GroupLink } from 'common/group' import { uniq } from 'lodash' import { Contract } from 'common/contract' import { User } from 'common/user' @@ -17,27 +17,6 @@ initAdmin() const adminFirestore = admin.firestore() -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const addGroupIdToContracts = async () => { - const groups = await getValues(adminFirestore.collection('groups')) - const contracts = await getValues( - adminFirestore.collection('contracts') - ) - for (const group of groups) { - const groupContracts = contracts.filter((contract) => - group.contractIds.includes(contract.id) - ) - for (const contract of groupContracts) { - await adminFirestore - .collection('contracts') - .doc(contract.id) - .update({ - groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), - }) - } - } -} - const convertCategoriesToGroupsInternal = async (categories: string[]) => { for (const category of categories) { const markets = await getValues( @@ -93,18 +72,30 @@ const convertCategoriesToGroupsInternal = async (categories: string[]) => { }) for (const market of markets) { + if (market.groupLinks?.map((l) => l.groupId).includes(newGroup.id)) + continue // already in that group + + const newGroupLinks = [ + ...(market.groupLinks ?? []), + { + groupId: newGroup.id, + createdTime: Date.now(), + slug: newGroup.slug, + name: newGroup.name, + } as GroupLink, + ] await adminFirestore .collection('contracts') .doc(market.id) .update({ - groupSlugs: uniq([...(market?.groupSlugs ?? []), newGroup.slug]), + groupSlugs: uniq([...(market.groupSlugs ?? []), newGroup.slug]), + groupLinks: newGroupLinks, }) } } } async function convertCategoriesToGroups() { - // await addGroupIdToContracts() // const defaultCategories = Object.values(DEFAULT_CATEGORIES) const moreCategories = ['world', 'culture'] await convertCategoriesToGroupsInternal(moreCategories) diff --git a/functions/src/scripts/link-contracts-to-groups.ts b/functions/src/scripts/link-contracts-to-groups.ts new file mode 100644 index 00000000..feda249e --- /dev/null +++ b/functions/src/scripts/link-contracts-to-groups.ts @@ -0,0 +1,52 @@ +import { getValues } from 'functions/src/utils' +import { Group } from 'common/group' +import { Contract } from 'common/contract' +import { initAdmin } from 'functions/src/scripts/script-init' +import * as admin from 'firebase-admin' +import { filterDefined } from 'common/util/array' +import { uniq } from 'lodash' + +initAdmin() + +const adminFirestore = admin.firestore() + +const addGroupIdToContracts = async () => { + const groups = await getValues(adminFirestore.collection('groups')) + const contracts = await getValues( + adminFirestore.collection('contracts') + ) + for (const group of groups) { + const groupContracts = contracts.filter((contract) => + group.contractIds.includes(contract.id) + ) + for (const contract of groupContracts) { + const oldGroupLinks = contract.groupLinks?.filter( + (l) => l.slug != group.slug + ) + const newGroupLinks = filterDefined([ + ...(oldGroupLinks ?? []), + group.id + ? { + slug: group.slug, + name: group.name, + groupId: group.id, + createdTime: Date.now(), + } + : undefined, + ]) + await adminFirestore + .collection('contracts') + .doc(contract.id) + .update({ + groupSlugs: uniq([...(contract.groupSlugs ?? []), group.slug]), + groupLinks: newGroupLinks, + }) + } + } +} + +if (require.main === module) { + addGroupIdToContracts() + .then(() => process.exit()) + .catch(console.log) +} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 0f5a1d42..544e9c27 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -23,11 +23,8 @@ import { useState } from 'react' import { ContractInfoDialog } from './contract-info-dialog' import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' -import { CATEGORY_LIST } from 'common/categories' -import { TagsList } from '../tags-list' import { UserFollowButton } from '../follow-button' import { DAY_MS } from 'common/util/time' -import { useGroupsWithContract } from 'web/hooks/use-group' import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' import { Editor } from '@tiptap/react' @@ -37,6 +34,8 @@ import { Button } from 'web/components/button' import { Modal } from 'web/components/layout/modal' import { Col } from 'web/components/layout/col' import { ContractGroupsList } from 'web/components/groups/contract-groups-list' +import { SiteLink } from 'web/components/site-link' +import { groupPath } from 'web/lib/firebase/groups' export type ShowTime = 'resolve-date' | 'close-date' @@ -50,15 +49,16 @@ export function MiscDetails(props: { volume, volume24Hours, closeTime, - tags, isResolved, createdTime, resolutionTime, + groupLinks, } = contract + // Show at most one category that this contract is tagged by - const categories = CATEGORY_LIST.filter((category) => - tags.map((t) => t.toLowerCase()).includes(category) - ).slice(0, 1) + // const categories = CATEGORY_LIST.filter((category) => + // tags.map((t) => t.toLowerCase()).includes(category) + // ).slice(0, 1) const isNew = createdTime > Date.now() - DAY_MS && !isResolved return ( @@ -80,13 +80,24 @@ export function MiscDetails(props: { {fromNow(resolutionTime || 0)} ) : volume > 0 || !isNew ? ( - {contractPool(contract)} pool + {contractPool(contract)} pool ) : ( )} - {categories.length > 0 && ( - + {/*{categories.length > 0 && (*/} + {/* */} + {/*)}*/} + {groupLinks && groupLinks.length > 0 && ( + + + + {groupLinks[0].name} + + )} ) @@ -134,11 +145,12 @@ export function ContractDetails(props: { disabled?: boolean }) { const { contract, bets, isCreator, disabled } = props - const { closeTime, creatorName, creatorUsername, creatorId } = contract + const { closeTime, creatorName, creatorUsername, creatorId, groupLinks } = + contract const { volumeLabel, resolvedDate } = contractMetrics(contract) - const groups = useGroupsWithContract(contract) - const groupToDisplay = groups[0] ?? null + const groupToDisplay = + groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null const user = useUser() const [open, setOpen] = useState(false) @@ -172,11 +184,7 @@ export function ContractDetails(props: { - {contract.groupSlugs && !groupToDisplay - ? '' - : groupToDisplay - ? groupToDisplay.name - : 'No group'} + {groupToDisplay ? groupToDisplay.name : 'No group'} @@ -187,7 +195,11 @@ export function ContractDetails(props: { 'max-h-[70vh] min-h-[20rem] overflow-auto rounded bg-white p-6' } > - + diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx index 7fab42d8..423cbb97 100644 --- a/web/components/groups/contract-groups-list.tsx +++ b/web/components/groups/contract-groups-list.tsx @@ -1,8 +1,7 @@ -import { Group } from 'common/group' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import clsx from 'clsx' -import { GroupLink } from 'web/pages/groups' +import { GroupLinkItem } from 'web/pages/groups' import { XIcon } from '@heroicons/react/outline' import { Button } from 'web/components/button' import { GroupSelector } from 'web/components/groups/group-selector' @@ -13,14 +12,16 @@ import { import { User } from 'common/user' import { Contract } from 'common/contract' import { SiteLink } from 'web/components/site-link' +import { GroupLink } from 'common/group' +import { useGroupsWithContract } from 'web/hooks/use-group' export function ContractGroupsList(props: { - groups: Group[] + groupLinks: GroupLink[] contract: Contract user: User | null | undefined }) { - const { groups, user, contract } = props - + const { groupLinks, user, contract } = props + const groups = useGroupsWithContract(contract) return ( @@ -33,10 +34,10 @@ export function ContractGroupsList(props: { options={{ showSelector: true, showLabel: false, - ignoreGroupIds: groups.map((g) => g.id), + ignoreGroupIds: groupLinks.map((g) => g.groupId), }} setSelectedGroup={(group) => - group && addContractToGroup(group, contract) + group && addContractToGroup(group, contract, user.id) } selectedGroup={undefined} creator={user} @@ -54,7 +55,7 @@ export function ContractGroupsList(props: { className={clsx('items-center justify-between gap-2 p-2')} > - + {user && group.memberIds.includes(user.id) && (