import { collection, collectionGroup, deleteDoc, deleteField, doc, getDocs, onSnapshot, query, setDoc, updateDoc, where, } from 'firebase/firestore' import { uniq, uniqBy } from 'lodash' import { Group, GROUP_CHAT_SLUG, GroupLink } from 'common/group' import { coll, getValue, getValues, listenForValue, listenForValues, } from './utils' import { Contract } from 'common/contract' import { getContractFromId, updateContract } from 'web/lib/firebase/contracts' import { db } from 'web/lib/firebase/init' import { filterDefined } from 'common/util/array' export const groups = coll('groups') export const groupMembers = (groupId: string) => collection(groups, groupId, 'groupMembers') export const groupContracts = (groupId: string) => collection(groups, groupId, 'groupContracts') const openGroupsQuery = query(groups, where('anyoneCanJoin', '==', true)) export const memberGroupsQuery = (userId: string) => query(collectionGroup(db, 'groupMembers'), where('userId', '==', userId)) export function groupPath( groupSlug: string, subpath?: | 'edit' | 'markets' | 'about' | typeof GROUP_CHAT_SLUG | 'leaderboards' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } export type GroupContractDoc = { contractId: string; createdTime: number } export type GroupMemberDoc = { userId: string; createdTime: number } export function updateGroup(group: Group, updates: Partial) { return updateDoc(doc(groups, group.id), updates) } export function deleteFieldFromGroup(group: Group, field: string) { return updateDoc(doc(groups, group.id), { [field]: deleteField() }) } export function deleteGroup(group: Group) { return deleteDoc(doc(groups, group.id)) } export async function listAllGroups() { return getValues(groups) } export async function listGroups(groupSlugs: string[]) { return Promise.all(groupSlugs.map(getGroupBySlug)) } export function listenForGroups(setGroups: (groups: Group[]) => void) { return listenForValues(groups, setGroups) } export function listenForGroupContractDocs( groupId: string, setContractDocs: (docs: GroupContractDoc[]) => void ) { return listenForValues(groupContracts(groupId), setContractDocs) } export async function listGroupContracts(groupId: string) { const contractDocs = await getValues<{ contractId: string createdTime: number }>(groupContracts(groupId)) const contracts = await Promise.all( contractDocs.map((doc) => getContractFromId(doc.contractId)) ) return filterDefined(contracts) } export function listenForOpenGroups(setGroups: (groups: Group[]) => void) { return listenForValues(openGroupsQuery, setGroups) } export function getGroup(groupId: string) { return getValue(doc(groups, groupId)) } export async function getGroupBySlug(slug: string) { const q = query(groups, where('slug', '==', slug)) const docs = (await getDocs(q)).docs return docs.length === 0 ? null : docs[0].data() } export function listenForGroup( groupId: string, setGroup: (group: Group | null) => void ) { return listenForValue(doc(groups, groupId), setGroup) } export async function getMemberGroups(userId: string) { const snapshot = await getDocs(memberGroupsQuery(userId)) const groupIds = filterDefined( snapshot.docs.map((doc) => doc.ref.parent.parent?.id) ) const groups = await Promise.all(groupIds.map(getGroup)) return filterDefined(groups) } export function listenForMemberGroupIds( userId: string, setGroupIds: (groupIds: string[]) => void ) { const q = memberGroupsQuery(userId) return onSnapshot(q, { includeMetadataChanges: true }, (snapshot) => { if (snapshot.metadata.fromCache) return const values = snapshot.docs.map((doc) => doc.ref.parent.parent?.id) setGroupIds(filterDefined(values)) }) } export function listenForMemberGroups( userId: string, setGroups: (groups: Group[]) => void ) { return listenForMemberGroupIds(userId, (groupIds) => { return Promise.all(groupIds.map(getGroup)).then((groups) => { setGroups(filterDefined(groups)) }) }) } export async function listAvailableGroups(userId: string) { const [openGroups, memberGroupSnapshot] = await Promise.all([ getValues(openGroupsQuery), getDocs(memberGroupsQuery(userId)), ]) const memberGroups = filterDefined( await Promise.all( memberGroupSnapshot.docs.map((doc) => { return doc.ref.parent.parent?.id ? getGroup(doc.ref.parent.parent?.id) : null }) ) ) return uniqBy([...openGroups, ...memberGroups], (g) => g.id) } export async function addUserToGroupViaId(groupId: string, userId: string) { // get group to get the member ids const group = await getGroup(groupId) if (!group) { console.error(`Group not found: ${groupId}`) return } return await joinGroup(group, userId) } export async function joinGroup(group: Group, userId: string): Promise { // create a new member document in grouoMembers collection const memberDoc = doc(groupMembers(group.id), userId) return await setDoc(memberDoc, { userId, createdTime: Date.now(), }) } export async function leaveGroup(group: Group, userId: string): Promise { // delete the member document in groupMembers collection const memberDoc = doc(groupMembers(group.id), userId) return await deleteDoc(memberDoc) } export async function addContractToGroup( group: Group, contract: Contract, userId: string ) { 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, }) // create new contract document in groupContracts collection const contractDoc = doc(groupContracts(group.id), contract.id) await setDoc(contractDoc, { contractId: contract.id, createdTime: Date.now(), }) } export async function removeContractFromGroup( group: Group, contract: Contract ) { 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 ?? [], }) } // delete the contract document in groupContracts collection const contractDoc = doc(groupContracts(group.id), contract.id) await deleteDoc(contractDoc) } export function getGroupLinkToDisplay(contract: Contract) { const { groupLinks } = contract const sortedGroupLinks = groupLinks?.sort( (a, b) => b.createdTime - a.createdTime ) const groupCreatorAdded = sortedGroupLinks?.find( (g) => g.userId === contract.creatorId ) const groupToDisplay = groupCreatorAdded ? groupCreatorAdded : sortedGroupLinks?.[0] ?? null return groupToDisplay } export async function listMemberIds(group: Group) { const members = await getValues(groupMembers(group.id)) return members.map((m) => m.userId) }