import { collectionGroup, doc, getDoc, orderBy, query, setDoc, where, } from 'firebase/firestore' import { Challenge } from 'common/challenge' import { customAlphabet } from 'nanoid' import { coll, listenForValue, listenForValues } from './utils' import { useEffect, useState } from 'react' import { User } from 'common/user' import { db } from './init' import { Contract } from 'common/contract' import { ENV_CONFIG } from 'common/envs/constants' export const challenges = (contractId: string) => coll(`contracts/${contractId}/challenges`) export function getChallengeUrl(challenge: Challenge) { return `https://${ENV_CONFIG.domain}/challenges/${challenge.creatorUsername}/${challenge.contractSlug}/${challenge.slug}` } export async function createChallenge(data: { creator: User outcome: 'YES' | 'NO' | number contract: Contract creatorAmount: number acceptorAmount: number expiresTime: number | null message: string }) { const { creator, creatorAmount, expiresTime, message, contract, outcome, acceptorAmount, } = data // At 100 IDs per hour, using this alphabet and 8 chars, there's a 1% chance of collision in 2 years // See https://zelark.github.io/nano-id-cc/ const nanoid = customAlphabet( '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8 ) const slug = nanoid() if (creatorAmount <= 0 || isNaN(creatorAmount) || !isFinite(creatorAmount)) return null const challenge: Challenge = { slug, creatorId: creator.id, creatorUsername: creator.username, creatorName: creator.name, creatorAvatarUrl: creator.avatarUrl, creatorAmount, creatorOutcome: outcome.toString(), creatorOutcomeProb: creatorAmount / (creatorAmount + acceptorAmount), acceptorOutcome: outcome === 'YES' ? 'NO' : 'YES', acceptorAmount, contractSlug: contract.slug, contractId: contract.id, contractQuestion: contract.question, contractCreatorUsername: contract.creatorUsername, createdTime: Date.now(), expiresTime, maxUses: 1, acceptedByUserIds: [], acceptances: [], isResolved: false, message, } await setDoc(doc(challenges(contract.id), slug), challenge) return challenge } // TODO: This required an index, make sure to also set up in prod function listUserChallenges(fromId?: string) { return query( collectionGroup(db, 'challenges'), where('creatorId', '==', fromId), orderBy('createdTime', 'desc') ) } function listChallenges() { return query(collectionGroup(db, 'challenges')) } export const useAcceptedChallenges = () => { const [links, setLinks] = useState([]) useEffect(() => { listenForValues(listChallenges(), (challenges: Challenge[]) => { setLinks( challenges .sort((a: Challenge, b: Challenge) => b.createdTime - a.createdTime) .filter((challenge) => challenge.acceptedByUserIds.length > 0) ) }) }, []) return links } export function listenForChallenge( slug: string, contractId: string, setLinks: (challenge: Challenge | null) => void ) { return listenForValue(doc(challenges(contractId), slug), setLinks) } export function useChallenge(slug: string, contractId: string | undefined) { const [challenge, setChallenge] = useState() useEffect(() => { if (slug && contractId) { listenForChallenge(slug, contractId, setChallenge) } }, [contractId, slug]) return challenge } export function listenForUserChallenges( fromId: string | undefined, setLinks: (links: Challenge[]) => void ) { return listenForValues(listUserChallenges(fromId), setLinks) } export const useUserChallenges = (fromId?: string) => { const [links, setLinks] = useState([]) useEffect(() => { if (fromId) return listenForUserChallenges(fromId, setLinks) }, [fromId]) return links } export const getChallenge = async (slug: string, contractId: string) => { const challenge = await getDoc(doc(challenges(contractId), slug)) return challenge.data() as Challenge }