diff --git a/common/util/promise.ts b/common/util/promise.ts new file mode 100644 index 00000000..33db339c --- /dev/null +++ b/common/util/promise.ts @@ -0,0 +1,18 @@ +export const batchedWaitAll = async ( + createPromises: (() => Promise)[], + batchSize = 10 +) => { + const numBatches = Math.ceil(createPromises.length / batchSize) + const result: T[] = [] + for (let batchIndex = 0; batchIndex < numBatches; batchIndex++) { + const from = batchIndex * batchSize + const to = from + batchSize + + const promises = createPromises.slice(from, to).map((f) => f()) + + const batch = await Promise.all(promises) + result.push(...batch) + } + + return result +} diff --git a/functions/src/update-contract-metrics.ts b/functions/src/update-contract-metrics.ts index 3646e7ad..d203ee99 100644 --- a/functions/src/update-contract-metrics.ts +++ b/functions/src/update-contract-metrics.ts @@ -5,6 +5,7 @@ import * as _ from 'lodash' import { getValues } from './utils' import { Contract } from '../../common/contract' import { Bet } from '../../common/bet' +import { batchedWaitAll } from '../../common/util/promise' const firestore = admin.firestore() @@ -17,8 +18,8 @@ export const updateContractMetrics = functions.pubsub firestore.collection('contracts') ) - await Promise.all( - contracts.map(async (contract) => { + await batchedWaitAll( + contracts.map((contract) => async () => { const volume24Hours = await computeVolumeFrom(contract, oneDay) const volume7Days = await computeVolumeFrom(contract, oneDay * 7) diff --git a/functions/src/update-user-metrics.ts b/functions/src/update-user-metrics.ts index d1d13727..358dd936 100644 --- a/functions/src/update-user-metrics.ts +++ b/functions/src/update-user-metrics.ts @@ -7,6 +7,7 @@ import { Contract } from '../../common/contract' import { Bet } from '../../common/bet' import { User } from '../../common/user' import { calculatePayout } from '../../common/calculate' +import { batchedWaitAll } from '../../common/util/promise' const firestore = admin.firestore() @@ -22,8 +23,8 @@ export const updateUserMetrics = functions.pubsub contracts.map((contract) => [contract.id, contract]) ) - await Promise.all( - users.map(async (user) => { + await batchedWaitAll( + users.map((user) => async () => { const [investmentValue, creatorVolume] = await Promise.all([ computeInvestmentValue(user, contractsDict), computeTotalPool(user, contractsDict), diff --git a/web/components/contract-card.tsx b/web/components/contract-card.tsx index 36bfd26b..cadf7764 100644 --- a/web/components/contract-card.tsx +++ b/web/components/contract-card.tsx @@ -334,7 +334,9 @@ const getTweetText = (contract: Contract, isCreator: boolean) => { contract )} chance, place your bets here:` : `Submit your own answer:` - const url = `https://manifold.markets${contractPath(contract)}` + + const timeParam = `${Date.now()}`.substring(7) + const url = `https://manifold.markets${contractPath(contract)}?t=${timeParam}` return `${tweetQuestion}\n\n${tweetDescription}\n\n${url}` } diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index d2c80061..7aa3abb1 100644 --- a/web/components/contract-feed.tsx +++ b/web/components/contract-feed.tsx @@ -93,10 +93,12 @@ function Timestamp(props: { time: number }) { function FeedBet(props: { activityItem: any; feedType: FeedType }) { const { activityItem, feedType } = props - const { id, contractId, amount, outcome, createdTime } = activityItem + const { id, contractId, amount, outcome, createdTime, contract } = + activityItem const user = useUser() const isSelf = user?.id == activityItem.userId - // The creator can comment if the bet was posted in the last hour + const isCreator = contract.creatorId == activityItem.userId + // You can comment if your bet was posted in the last hour const canComment = isSelf && Date.now() - createdTime < 60 * 60 * 1000 const [comment, setComment] = useState('') @@ -113,6 +115,8 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) {
{isSelf ? ( + ) : isCreator ? ( + ) : (
@@ -123,7 +127,10 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) {
- {isSelf ? 'You' : 'A trader'} {bought} {money} + + {isSelf ? 'You' : isCreator ? contract.creatorName : 'A trader'} + {' '} + {bought} {money} {canComment && ( @@ -494,7 +501,7 @@ function FeedClose(props: { contract: Contract }) { ) } -function toFeedBet(bet: Bet) { +function toFeedBet(bet: Bet, contract: Contract) { return { id: bet.id, contractId: bet.contractId, @@ -504,6 +511,7 @@ function toFeedBet(bet: Bet) { outcome: bet.outcome, createdTime: bet.createdTime, date: fromNow(bet.createdTime), + contract, } } @@ -533,12 +541,13 @@ const DAY_IN_MS = 24 * 60 * 60 * 1000 // Group together bets that are: // - Within `windowMs` of the first in the group // - Do not have a comment -// - Were not created by this user +// - Were not created by this user or the contract creator // Return a list of ActivityItems function groupBets( bets: Bet[], comments: Comment[], windowMs: number, + contract: Contract, userId?: string ) { const commentsMap = mapCommentsByBetId(comments) @@ -548,25 +557,25 @@ function groupBets( // Turn the current group into an ActivityItem function pushGroup() { if (group.length == 1) { - items.push(toActivityItem(group[0])) + items.push(toActivityItem(group[0], false)) } else if (group.length > 1) { items.push({ type: 'betgroup', bets: [...group], id: group[0].id }) } group = [] } - function toActivityItem(bet: Bet) { + function toActivityItem(bet: Bet, isPublic: boolean) { const comment = commentsMap[bet.id] - return comment ? toFeedComment(bet, comment) : toFeedBet(bet) + return comment ? toFeedComment(bet, comment) : toFeedBet(bet, contract) } for (const bet of bets) { - const isCreator = userId === bet.userId + const isCreator = userId === bet.userId || contract.creatorId === bet.userId if (commentsMap[bet.id] || isCreator) { pushGroup() // Create a single item for this - items.push(toActivityItem(bet)) + items.push(toActivityItem(bet, true)) } else { if ( group.length > 0 && @@ -801,7 +810,7 @@ export function ContractFeed(props: { const allItems: ActivityItem[] = [ { type: 'start', id: '0' }, - ...groupBets(bets, comments, groupWindow, user?.id), + ...groupBets(bets, comments, groupWindow, contract, user?.id), ] if (contract.closeTime && contract.closeTime <= Date.now()) { allItems.push({ type: 'close', id: `${contract.closeTime}` }) @@ -851,7 +860,7 @@ export function ContractActivityFeed(props: { const allItems: ActivityItem[] = [ { type: 'start', id: '0' }, - ...groupBets(bets, comments, DAY_IN_MS, user?.id), + ...groupBets(bets, comments, DAY_IN_MS, contract, user?.id), ] if (contract.closeTime && contract.closeTime <= Date.now()) { allItems.push({ type: 'close', id: `${contract.closeTime}` }) diff --git a/web/hooks/use-propz.ts b/web/hooks/use-propz.ts index 16315929..5aee4c61 100644 --- a/web/hooks/use-propz.ts +++ b/web/hooks/use-propz.ts @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react' import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' type PropzProps = { + // Params from the router query params: any } @@ -12,9 +13,7 @@ type PropzProps = { // TODO: Could cache the result using stale-while-revalidate: https://swr.vercel.app/ export function usePropz( initialProps: Object, - getStaticPropz: (props: PropzProps) => Promise, - // Dynamic routes will need the query params from the router - needParams?: boolean + getStaticPropz: (props: PropzProps) => Promise ) { // If props were successfully server-side generated, just use those if (!_.isEmpty(initialProps)) { @@ -27,10 +26,9 @@ export function usePropz( const [propz, setPropz] = useState(undefined) useEffect(() => { - if (needParams && _.isEmpty(params)) { - return + if (router.isReady) { + getStaticPropz({ params }).then((result) => setPropz(result.props)) } - getStaticPropz({ params }).then((result) => setPropz(result.props)) }, [params]) return propz } diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 39142378..ea985f81 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -79,7 +79,7 @@ export default function ContractPage(props: { slug: string folds: Fold[] }) { - props = usePropz(props, getStaticPropz, true) ?? { + props = usePropz(props, getStaticPropz) ?? { contract: null, username: '', comments: [], diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index 6f27c3de..03808d39 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -118,7 +118,7 @@ export default function FoldPage(props: { creatorScores: { [userId: string]: number } topCreators: User[] }) { - props = usePropz(props, getStaticPropz, true) ?? { + props = usePropz(props, getStaticPropz) ?? { fold: null, curator: null, contracts: [],