From fe98a61e435bc092e144e68b06ba57cb73da5617 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Mon, 7 Mar 2022 13:45:56 -0800 Subject: [PATCH 1/4] Fix leaderboard out of memory error with batchedWaitAll instead of Promise.all --- common/util/promise.ts | 18 ++++++++++++++++++ functions/src/update-contract-metrics.ts | 5 +++-- functions/src/update-user-metrics.ts | 5 +++-- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 common/util/promise.ts 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), From b0e4f6d27a8e5b52cdb29b0ce90ec40f7f08eeaa Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 8 Mar 2022 15:36:15 -0600 Subject: [PATCH 2/4] Add time param to Twitter share url, so that the image preview is re-fetched --- web/components/contract-card.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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}` } From 9fbed63eaff8048e76be41312183193d1130397d Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 8 Mar 2022 16:24:06 -0800 Subject: [PATCH 3/4] Show the market creator's bets in feed --- web/components/contract-feed.tsx | 37 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/web/components/contract-feed.tsx b/web/components/contract-feed.tsx index b4cb7bad..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 && ( @@ -354,9 +361,9 @@ function FeedQuestion(props: { {!showDescription && ( -
See more...
+
See more...
)} @@ -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}` }) From da4ce997555cb5eac775a55dd7c1fbdc37b1c726 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 8 Mar 2022 18:43:30 -0800 Subject: [PATCH 4/4] Merge Manifold for Teams infra into main codebase (#61) * Add dev target for TheoremOne * Restrict signups to theoremone.co emails * Add new indices * Forbid reads from unauthenticated users * Client-side render pages that need auth These pages are now client-side rendered: - /home - /leaderboards - /market/... - /fold/... * Hide 404 for private Manifolds * Brand instance for TheoremOne * Hide "Add Funds" and "Personalize your feed" * "M$" => "T$" * Hide Discord & About Page too * Update placeholders for teams * Update firestore.indexes.json * Switch /analytics to propz * Migrate per-env code into common/ * More migrations to PROJECT_ID * Conditionally use SSG depending on public vs private instance * Fix props to be empty object * Move more logic into access * Spin out config files for each environment * Generify most of the customizable brand stuff * Move IS_PRIVATE_MANIFOLD to access.ts * Rename access.ts to envs/constants.ts * Add "dev:dev" alias * Rever firestore rules to existing settings * Fixes according to James's review --- common/access.ts | 14 ----- common/envs/constants.ts | 30 ++++++++++ common/envs/dev.ts | 14 +++++ common/envs/prod.ts | 55 ++++++++++++++++++ common/envs/theoremone.ts | 26 +++++++++ common/util/format.ts | 6 +- firestore.indexes.json | 4 ++ functions/src/create-user.ts | 2 +- functions/src/emails.ts | 21 +++---- web/components/feed-create.tsx | 11 +--- web/components/manifold-logo.tsx | 43 ++++++++------ web/components/profile-menu.tsx | 43 +++++++++----- web/hooks/use-admin.ts | 2 +- web/hooks/use-contract.ts | 2 +- web/hooks/use-propz.ts | 39 +++++++++++++ web/lib/firebase/init.ts | 38 +----------- web/lib/service/stripe.ts | 6 +- web/package.json | 2 + web/pages/404.tsx | 7 ++- web/pages/[username]/[contractSlug].tsx | 15 ++++- web/pages/_document.tsx | 3 +- web/pages/analytics.tsx | 14 ++++- web/pages/fold/[...slugs]/index.tsx | 18 +++++- web/pages/home.tsx | 13 ++++- web/pages/leaderboards.tsx | 8 ++- web/public/theoremone/Th1-Icon-Round.svg | 5 ++ web/public/theoremone/Th1-Icon-Square.svg | 5 ++ .../theoremone/TheoremOne-Logo-White.svg | 12 ++++ web/public/theoremone/TheoremOne-Logo.svg | 12 ++++ web/public/theoremone/logo.ico | Bin 0 -> 16958 bytes 30 files changed, 347 insertions(+), 123 deletions(-) delete mode 100644 common/access.ts create mode 100644 common/envs/constants.ts create mode 100644 common/envs/dev.ts create mode 100644 common/envs/prod.ts create mode 100644 common/envs/theoremone.ts create mode 100644 web/hooks/use-propz.ts create mode 100644 web/public/theoremone/Th1-Icon-Round.svg create mode 100644 web/public/theoremone/Th1-Icon-Square.svg create mode 100644 web/public/theoremone/TheoremOne-Logo-White.svg create mode 100644 web/public/theoremone/TheoremOne-Logo.svg create mode 100644 web/public/theoremone/logo.ico diff --git a/common/access.ts b/common/access.ts deleted file mode 100644 index acd894b1..00000000 --- a/common/access.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function isWhitelisted(email?: string) { - return true - // e.g. return email.endsWith('@theoremone.co') || isAdmin(email) -} - -export function isAdmin(email: string) { - const ADMINS = [ - 'akrolsmir@gmail.com', // Austin - 'jahooma@gmail.com', // James - 'taowell@gmail.com', // Stephen - 'manticmarkets@gmail.com', // Manifold - ] - return ADMINS.includes(email) -} diff --git a/common/envs/constants.ts b/common/envs/constants.ts new file mode 100644 index 00000000..b87948a7 --- /dev/null +++ b/common/envs/constants.ts @@ -0,0 +1,30 @@ +import { DEV_CONFIG } from './dev' +import { EnvConfig, PROD_CONFIG } from './prod' +import { THEOREMONE_CONFIG } from './theoremone' + +const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD' + +const CONFIGS = { + PROD: PROD_CONFIG, + DEV: DEV_CONFIG, + THEOREMONE: THEOREMONE_CONFIG, +} +// @ts-ignore +export const ENV_CONFIG: EnvConfig = CONFIGS[ENV] + +export function isWhitelisted(email?: string) { + if (!ENV_CONFIG.whitelistEmail) { + return true + } + return email && (email.endsWith(ENV_CONFIG.whitelistEmail) || isAdmin(email)) +} + +// TODO: Before open sourcing, we should turn these into env vars +export function isAdmin(email: string) { + return ENV_CONFIG.adminEmails.includes(email) +} + +export const DOMAIN = ENV_CONFIG.domain +export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig +export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId +export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE' diff --git a/common/envs/dev.ts b/common/envs/dev.ts new file mode 100644 index 00000000..9b82f3c0 --- /dev/null +++ b/common/envs/dev.ts @@ -0,0 +1,14 @@ +import { EnvConfig, PROD_CONFIG } from './prod' + +export const DEV_CONFIG: EnvConfig = { + ...PROD_CONFIG, + firebaseConfig: { + apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', + authDomain: 'dev-mantic-markets.firebaseapp.com', + projectId: 'dev-mantic-markets', + storageBucket: 'dev-mantic-markets.appspot.com', + messagingSenderId: '134303100058', + appId: '1:134303100058:web:27f9ea8b83347251f80323', + measurementId: 'G-YJC9E37P37', + }, +} diff --git a/common/envs/prod.ts b/common/envs/prod.ts new file mode 100644 index 00000000..20866429 --- /dev/null +++ b/common/envs/prod.ts @@ -0,0 +1,55 @@ +export type EnvConfig = { + domain: string + firebaseConfig: FirebaseConfig + + // Access controls + adminEmails: string[] + whitelistEmail?: string // e.g. '@theoremone.co'. If not provided, all emails are whitelisted + visibility: 'PRIVATE' | 'PUBLIC' + + // Branding + moneyMoniker: string // e.g. 'M$' + faviconPath?: string // Should be a file in /public + navbarLogoPath?: string + newQuestionPlaceholders: string[] +} + +type FirebaseConfig = { + apiKey: string + authDomain: string + projectId: string + storageBucket: string + messagingSenderId: string + appId: string + measurementId: string +} + +export const PROD_CONFIG: EnvConfig = { + domain: 'manifold.markets', + firebaseConfig: { + apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', + authDomain: 'mantic-markets.firebaseapp.com', + projectId: 'mantic-markets', + storageBucket: 'mantic-markets.appspot.com', + messagingSenderId: '128925704902', + appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', + measurementId: 'G-SSFK1Q138D', + }, + adminEmails: [ + 'akrolsmir@gmail.com', // Austin + 'jahooma@gmail.com', // James + 'taowell@gmail.com', // Stephen + 'manticmarkets@gmail.com', // Manifold + ], + visibility: 'PUBLIC', + + moneyMoniker: 'M$', + navbarLogoPath: '', + faviconPath: '/favicon.ico', + newQuestionPlaceholders: [ + 'Will anyone I know get engaged this year?', + 'Will humans set foot on Mars by the end of 2030?', + 'Will any cryptocurrency eclipse Bitcoin by market cap this year?', + 'Will the Democrats win the 2024 presidential election?', + ], +} diff --git a/common/envs/theoremone.ts b/common/envs/theoremone.ts new file mode 100644 index 00000000..ebe504d9 --- /dev/null +++ b/common/envs/theoremone.ts @@ -0,0 +1,26 @@ +import { EnvConfig, PROD_CONFIG } from './prod' + +export const THEOREMONE_CONFIG: EnvConfig = { + domain: 'theoremone.manifold.markets', + firebaseConfig: { + apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M', + authDomain: 'theoremone-manifold.firebaseapp.com', + projectId: 'theoremone-manifold', + storageBucket: 'theoremone-manifold.appspot.com', + messagingSenderId: '698012149198', + appId: '1:698012149198:web:b342af75662831aa84b79f', + measurementId: 'G-Y3EZ1WNT6E', + }, + adminEmails: [...PROD_CONFIG.adminEmails, 'david.glidden@theoremone.co'], + whitelistEmail: '@theoremone.co', + moneyMoniker: 'T$', + visibility: 'PRIVATE', + faviconPath: '/theoremone/logo.ico', + navbarLogoPath: '/theoremone/TheoremOne-Logo.svg', + newQuestionPlaceholders: [ + 'Will we have at least 5 new team members by the end of this quarter?', + 'Will we meet or exceed our goals this sprint?', + 'Will we sign on 3 or more new clients this month?', + 'Will Paul shave his beard by the end of the month?', + ], +} diff --git a/common/util/format.ts b/common/util/format.ts index 9b202d25..a8e6f58a 100644 --- a/common/util/format.ts +++ b/common/util/format.ts @@ -1,3 +1,5 @@ +import { ENV_CONFIG } from '../envs/constants' + const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', @@ -6,7 +8,9 @@ const formatter = new Intl.NumberFormat('en-US', { }) export function formatMoney(amount: number) { - return 'M$ ' + formatter.format(amount).replace('$', '') + return ( + ENV_CONFIG.moneyMoniker + ' ' + formatter.format(amount).replace('$', '') + ) } export function formatWithCommas(amount: number) { diff --git a/firestore.indexes.json b/firestore.indexes.json index ac88ccf6..e611f18a 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -242,6 +242,10 @@ "arrayConfig": "CONTAINS", "queryScope": "COLLECTION" }, + { + "order": "ASCENDING", + "queryScope": "COLLECTION_GROUP" + }, { "order": "DESCENDING", "queryScope": "COLLECTION_GROUP" diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index f583abe4..f73b868b 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -14,7 +14,7 @@ import { cleanUsername, } from '../../common/util/clean-username' import { sendWelcomeEmail } from './emails' -import { isWhitelisted } from '../../common/access' +import { isWhitelisted } from '../../common/envs/constants' export const createUser = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 0ded7b7d..3d37ba2b 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,4 +1,5 @@ import _ = require('lodash') +import { DOMAIN, PROJECT_ID } from '../../common/envs/constants' import { Answer } from '../../common/answer' import { Bet } from '../../common/bet' import { getProbability } from '../../common/calculate' @@ -8,7 +9,7 @@ import { CREATOR_FEE } from '../../common/fees' import { PrivateUser, User } from '../../common/user' import { formatMoney, formatPercent } from '../../common/util/format' import { sendTemplateEmail, sendTextEmail } from './send-email' -import { getPrivateUser, getUser, isProd } from './utils' +import { getPrivateUser, getUser } from './utils' type market_resolved_template = { userId: string @@ -73,7 +74,7 @@ export const sendMarketResolutionEmail = async ( outcome, investment: `${Math.round(investment)}`, payout: `${Math.round(payout)}`, - url: `https://manifold.markets/${creator.username}/${contract.slug}`, + url: `https://${DOMAIN}/${creator.username}/${contract.slug}`, } // Modify template here: @@ -107,7 +108,7 @@ Or come chat with us on Discord: https://discord.gg/eHQBNBqXuh Best, Austin from Manifold -https://manifold.markets/` +https://${DOMAIN}/` ) } @@ -128,7 +129,7 @@ export const sendMarketCloseEmail = async ( const { question, pool: pools, slug } = contract const pool = formatMoney(_.sum(_.values(pools))) - const url = `https://manifold.markets/${username}/${slug}` + const url = `https://${DOMAIN}/${username}/${slug}` await sendTemplateEmail( privateUser.email, @@ -162,11 +163,9 @@ export const sendNewCommentEmail = async ( return const { question, creatorUsername, slug } = contract - const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}` + const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}` - const unsubscribeUrl = `https://us-central1-${ - isProd ? 'mantic-markets' : 'dev-mantic-markets' - }.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment` + const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-comment` const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator const { text } = comment @@ -238,10 +237,8 @@ export const sendNewAnswerEmail = async ( const { question, creatorUsername, slug } = contract const { name, avatarUrl, text } = answer - const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}` - const unsubscribeUrl = `https://us-central1-${ - isProd ? 'mantic-markets' : 'dev-mantic-markets' - }.cloudfunctions.net/unsubscribe?id=${userId}&type=market-answer` + const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}` + const unsubscribeUrl = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-answer` const subject = `New answer on ${question}` const from = `${name} ` diff --git a/web/components/feed-create.tsx b/web/components/feed-create.tsx index 2a2d291c..00bbc31f 100644 --- a/web/components/feed-create.tsx +++ b/web/components/feed-create.tsx @@ -9,6 +9,7 @@ import { Contract } from '../../common/contract' import { Col } from './layout/col' import clsx from 'clsx' import { Row } from './layout/row' +import { ENV_CONFIG } from '../../common/envs/constants' export function FeedPromo(props: { hotContracts: Contract[] }) { const { hotContracts } = props @@ -72,16 +73,10 @@ export default function FeedCreate(props: { const [isExpanded, setIsExpanded] = useState(false) const inputRef = useRef() - const placeholders = [ - 'Will anyone I know get engaged this year?', - 'Will humans set foot on Mars by the end of 2030?', - 'Will any cryptocurrency eclipse Bitcoin by market cap this year?', - 'Will the Democrats win the 2024 presidential election?', - ] + const placeholders = ENV_CONFIG.newQuestionPlaceholders // Rotate through a new placeholder each day // Easter egg idea: click your own name to shuffle the placeholder // const daysSinceEpoch = Math.floor(Date.now() / 1000 / 60 / 60 / 24) - const [randIndex] = useState( Math.floor(Math.random() * 1e10) % placeholders.length ) @@ -90,7 +85,7 @@ export default function FeedCreate(props: { return (
-
- Manifold -
- Markets -
- + {ENV_CONFIG.navbarLogoPath ? ( + + ) : ( + <> +
+ Manifold +
+ Markets +
+ + + )} ) diff --git a/web/components/profile-menu.tsx b/web/components/profile-menu.tsx index 7cc00e6a..a01614b1 100644 --- a/web/components/profile-menu.tsx +++ b/web/components/profile-menu.tsx @@ -3,6 +3,7 @@ import { formatMoney } from '../../common/util/format' import { Avatar } from './avatar' import { Col } from './layout/col' import { MenuButton } from './menu' +import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' export function ProfileMenu(props: { user: User | undefined }) { const { user } = props @@ -54,22 +55,32 @@ function getNavigationOptions( name: 'Your trades', href: '/trades', }, - { - name: 'Add funds', - href: '/add-funds', - }, - { - name: 'Leaderboards', - href: '/leaderboards', - }, - { - name: 'Discord', - href: 'https://discord.gg/eHQBNBqXuh', - }, - { - name: 'About', - href: '/about', - }, + // Disable irrelevant menu options for teams. + ...(IS_PRIVATE_MANIFOLD + ? [ + { + name: 'Leaderboards', + href: '/leaderboards', + }, + ] + : [ + { + name: 'Add funds', + href: '/add-funds', + }, + { + name: 'Leaderboards', + href: '/leaderboards', + }, + { + name: 'Discord', + href: 'https://discord.gg/eHQBNBqXuh', + }, + { + name: 'About', + href: '/about', + }, + ]), { name: 'Sign out', href: '#', diff --git a/web/hooks/use-admin.ts b/web/hooks/use-admin.ts index bbeaf59c..7c8b8449 100644 --- a/web/hooks/use-admin.ts +++ b/web/hooks/use-admin.ts @@ -1,4 +1,4 @@ -import { isAdmin } from '../../common/access' +import { isAdmin } from '../../common/envs/constants' import { usePrivateUser, useUser } from './use-user' export const useAdmin = () => { diff --git a/web/hooks/use-contract.ts b/web/hooks/use-contract.ts index d48cd26a..ceaa125b 100644 --- a/web/hooks/use-contract.ts +++ b/web/hooks/use-contract.ts @@ -29,7 +29,7 @@ export const useContractWithPreload = ( useEffect(() => { if (contractId) return listenForContract(contractId, setContract) - if (contractId !== null) + if (contractId !== null && slug) getContractFromSlug(slug).then((c) => setContractId(c?.id || null)) }, [contractId, slug]) diff --git a/web/hooks/use-propz.ts b/web/hooks/use-propz.ts new file mode 100644 index 00000000..5aee4c61 --- /dev/null +++ b/web/hooks/use-propz.ts @@ -0,0 +1,39 @@ +import _ from 'lodash' +import { useRouter } from 'next/router' +import { useState, useEffect } from 'react' +import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' + +type PropzProps = { + // Params from the router query + params: any +} + +// getStaticPropz should exactly match getStaticProps +// This allows us to client-side render the page for authenticated users. +// TODO: Could cache the result using stale-while-revalidate: https://swr.vercel.app/ +export function usePropz( + initialProps: Object, + getStaticPropz: (props: PropzProps) => Promise +) { + // If props were successfully server-side generated, just use those + if (!_.isEmpty(initialProps)) { + return initialProps + } + + // Otherwise, get params from router + const router = useRouter() + const params = router.query + + const [propz, setPropz] = useState(undefined) + useEffect(() => { + if (router.isReady) { + getStaticPropz({ params }).then((result) => setPropz(result.props)) + } + }, [params]) + return propz +} + +// Conditionally disable SSG for private Manifold instances +export function fromPropz(getStaticPropz: (props: PropzProps) => Promise) { + return IS_PRIVATE_MANIFOLD ? async () => ({ props: {} }) : getStaticPropz +} diff --git a/web/lib/firebase/init.ts b/web/lib/firebase/init.ts index 145d67c5..b11ad355 100644 --- a/web/lib/firebase/init.ts +++ b/web/lib/firebase/init.ts @@ -1,42 +1,8 @@ import { getFirestore } from '@firebase/firestore' import { initializeApp, getApps, getApp } from 'firebase/app' +import { FIREBASE_CONFIG } from '../../../common/envs/constants' -// Used to decide which Stripe instance to point to -export const isProd = process.env.NEXT_PUBLIC_FIREBASE_ENV !== 'DEV' - -const FIREBASE_CONFIGS = { - PROD: { - apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', - authDomain: 'mantic-markets.firebaseapp.com', - projectId: 'mantic-markets', - storageBucket: 'mantic-markets.appspot.com', - messagingSenderId: '128925704902', - appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', - measurementId: 'G-SSFK1Q138D', - }, - DEV: { - apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', - authDomain: 'dev-mantic-markets.firebaseapp.com', - projectId: 'dev-mantic-markets', - storageBucket: 'dev-mantic-markets.appspot.com', - messagingSenderId: '134303100058', - appId: '1:134303100058:web:27f9ea8b83347251f80323', - measurementId: 'G-YJC9E37P37', - }, - THEOREMONE: { - apiKey: 'AIzaSyBSXL6Ys7InNHnCKSy-_E_luhh4Fkj4Z6M', - authDomain: 'theoremone-manifold.firebaseapp.com', - projectId: 'theoremone-manifold', - storageBucket: 'theoremone-manifold.appspot.com', - messagingSenderId: '698012149198', - appId: '1:698012149198:web:b342af75662831aa84b79f', - measurementId: 'G-Y3EZ1WNT6E', - }, -} -const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD' -// @ts-ignore -const firebaseConfig = FIREBASE_CONFIGS[ENV] // Initialize Firebase -export const app = getApps().length ? getApp() : initializeApp(firebaseConfig) +export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG) export const db = getFirestore(app) diff --git a/web/lib/service/stripe.ts b/web/lib/service/stripe.ts index 3d69c284..6c093483 100644 --- a/web/lib/service/stripe.ts +++ b/web/lib/service/stripe.ts @@ -1,13 +1,11 @@ -import { isProd } from '../firebase/init' +import { PROJECT_ID } from '../../../common/envs/constants' export const checkoutURL = ( userId: string, manticDollarQuantity: number, referer = '' ) => { - const endpoint = isProd - ? 'https://us-central1-mantic-markets.cloudfunctions.net/createCheckoutSession' - : 'https://us-central1-dev-mantic-markets.cloudfunctions.net/createCheckoutSession' + const endpoint = `https://us-central1-${PROJECT_ID}.cloudfunctions.net/createCheckoutSession` return `${endpoint}?userId=${userId}&manticDollarQuantity=${manticDollarQuantity}&referer=${encodeURIComponent( referer diff --git a/web/package.json b/web/package.json index 6a7caecf..61342e4d 100644 --- a/web/package.json +++ b/web/package.json @@ -5,6 +5,8 @@ "scripts": { "dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"", "devdev": "NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=DEV next dev -p 3000\" \"FIREBASE_ENV=DEV yarn ts --watch\"", + "dev:dev": "yarn devdev", + "dev:the": "NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"FIREBASE_ENV=THEOREMONE yarn ts --watch\"", "ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty", "build": "next build", "start": "next start", diff --git a/web/pages/404.tsx b/web/pages/404.tsx index 7e9c9e27..f710263d 100644 --- a/web/pages/404.tsx +++ b/web/pages/404.tsx @@ -1,8 +1,13 @@ -import { useEffect } from 'gridjs' +import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' import { Page } from '../components/page' import { Title } from '../components/title' export default function Custom404() { + if (IS_PRIVATE_MANIFOLD) { + // Since private Manifolds are client-side rendered, they'll blink the 404 + // So we just show a blank page here: + return + } return (
diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 38b7cd3c..ea985f81 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -29,8 +29,10 @@ import { useFoldsWithTags } from '../../hooks/use-fold' import { listAllAnswers } from '../../lib/firebase/answers' import { Answer } from '../../../common/answer' import { AnswersPanel } from '../../components/answers/answers-panel' +import { fromPropz, usePropz } from '../../hooks/use-propz' -export async function getStaticProps(props: { +export const getStaticProps = fromPropz(getStaticPropz) +export async function getStaticPropz(props: { params: { username: string; contractSlug: string } }) { const { username, contractSlug } = props.params @@ -77,6 +79,15 @@ export default function ContractPage(props: { slug: string folds: Fold[] }) { + props = usePropz(props, getStaticPropz) ?? { + contract: null, + username: '', + comments: [], + answers: [], + bets: [], + slug: '', + folds: [], + } const user = useUser() const contract = useContractWithPreload(props.slug, props.contract) @@ -143,7 +154,7 @@ export default function ContractPage(props: { <>
- + {allowTrade && ( )} diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index 2388a222..18329b12 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -1,10 +1,11 @@ import { Html, Head, Main, NextScript } from 'next/document' +import { ENV_CONFIG } from '../../common/envs/constants' export default function Document() { return ( - + - + {!IS_PRIVATE_MANIFOLD && } ) } diff --git a/web/pages/fold/[...slugs]/index.tsx b/web/pages/fold/[...slugs]/index.tsx index 757fe325..03808d39 100644 --- a/web/pages/fold/[...slugs]/index.tsx +++ b/web/pages/fold/[...slugs]/index.tsx @@ -38,12 +38,14 @@ import FeedCreate from '../../../components/feed-create' import { SEO } from '../../../components/SEO' import { useTaggedContracts } from '../../../hooks/use-contracts' import { Linkify } from '../../../components/linkify' +import { fromPropz, usePropz } from '../../../hooks/use-propz' import { filterDefined } from '../../../../common/util/array' import { useRecentBets } from '../../../hooks/use-bets' import { useRecentComments } from '../../../hooks/use-comments' import { LoadingIndicator } from '../../../components/loading-indicator' -export async function getStaticProps(props: { params: { slugs: string[] } }) { +export const getStaticProps = fromPropz(getStaticPropz) +export async function getStaticPropz(props: { params: { slugs: string[] } }) { const { slugs } = props.params const fold = await getFoldBySlug(slugs[0]) @@ -116,13 +118,25 @@ export default function FoldPage(props: { creatorScores: { [userId: string]: number } topCreators: User[] }) { + props = usePropz(props, getStaticPropz) ?? { + fold: null, + curator: null, + contracts: [], + activeContracts: [], + activeContractBets: [], + activeContractComments: [], + traderScores: {}, + topTraders: [], + creatorScores: {}, + topCreators: [], + } const { curator, traderScores, topTraders, creatorScores, topCreators } = props const router = useRouter() const { slugs } = router.query as { slugs: string[] } - const page = (slugs[1] ?? 'activity') as typeof foldSubpages[number] + const page = (slugs?.[1] ?? 'activity') as typeof foldSubpages[number] const fold = useFold(props.fold?.id) ?? props.fold diff --git a/web/pages/home.tsx b/web/pages/home.tsx index 548f1057..8e10106f 100644 --- a/web/pages/home.tsx +++ b/web/pages/home.tsx @@ -22,11 +22,14 @@ import { useFilterYourContracts, useFindActiveContracts, } from '../hooks/use-find-active-contracts' +import { fromPropz, usePropz } from '../hooks/use-propz' import { useGetRecentBets, useRecentBets } from '../hooks/use-bets' import { useActiveContracts } from '../hooks/use-contracts' import { useRecentComments } from '../hooks/use-comments' +import { IS_PRIVATE_MANIFOLD } from '../../common/envs/constants' -export async function getStaticProps() { +export const getStaticProps = fromPropz(getStaticPropz) +export async function getStaticPropz() { const contractInfo = await getAllContractInfo() return { @@ -40,6 +43,11 @@ const Home = (props: { folds: Fold[] recentComments: Comment[] }) => { + props = usePropz(props, getStaticPropz) ?? { + contracts: [], + folds: [], + recentComments: [], + } const { folds } = props const user = useUser() @@ -77,7 +85,8 @@ const Home = (props: { {initialFollowedFoldSlugs !== undefined && - initialFollowedFoldSlugs.length === 0 && ( + initialFollowedFoldSlugs.length === 0 && + !IS_PRIVATE_MANIFOLD && ( {}), getTopCreators().catch((_) => {}), @@ -26,6 +28,10 @@ export default function Leaderboards(props: { topTraders: User[] topCreators: User[] }) { + props = usePropz(props, getStaticPropz) ?? { + topTraders: [], + topCreators: [], + } const { topTraders, topCreators } = props return ( diff --git a/web/public/theoremone/Th1-Icon-Round.svg b/web/public/theoremone/Th1-Icon-Round.svg new file mode 100644 index 00000000..18a5b0c5 --- /dev/null +++ b/web/public/theoremone/Th1-Icon-Round.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/public/theoremone/Th1-Icon-Square.svg b/web/public/theoremone/Th1-Icon-Square.svg new file mode 100644 index 00000000..5914165f --- /dev/null +++ b/web/public/theoremone/Th1-Icon-Square.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/public/theoremone/TheoremOne-Logo-White.svg b/web/public/theoremone/TheoremOne-Logo-White.svg new file mode 100644 index 00000000..fd91d9ee --- /dev/null +++ b/web/public/theoremone/TheoremOne-Logo-White.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web/public/theoremone/TheoremOne-Logo.svg b/web/public/theoremone/TheoremOne-Logo.svg new file mode 100644 index 00000000..76c3156d --- /dev/null +++ b/web/public/theoremone/TheoremOne-Logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web/public/theoremone/logo.ico b/web/public/theoremone/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..658adfd0d371bd07a47785b0dc0f279e70d646d8 GIT binary patch literal 16958 zcmeHPeN0wW7{65JGVv=?LB0ii6$Dd6162H&h{eyPB)eL>Le*ZCd1Z|{BI7c&ZcS?98cd+)i=J@@&Y z^Lw82JckWUa}z&tahmw;ubF0O+E7i?OhQDHwp9q*?fax41E|BscWw%-eo(V2S45iqrLh$?Mv6Ox8Z)>2M&1vtDjH8!To!YpLG;xPUhou zVV+fZf8zL;_&no7ER2uFlEmrwV&5JV=4DxRs9%tiDRk1YaCS6iMw#&GM?3LNezrlU zFgHuSyL8@61Ppd_nf=;v0Yfl-LNHdPK7)$VORzo8es=~-=g-2lCq^R7+Z|y(?(#Y# zeTS_YGSc6}l!yR?diR&}PW__?_ZxKf?S8+bAD;m(gPpu12RI)-?i$|Qve9aDYikRZ zCnd;sIbTAL`hEj6Bua?_2@a$953_7~5 ztDOA?YQh(iar^ddY4h*b>m`@%xypO;rT*I04!z&%F>BIj1E;AIN1eJ_sbg+#lD^?T z=&vzy72LWV#J^(ET(q~fNt>_MR0&S|8*ovV*kZ=|qNeh))T^tl#?*0v20d~r%H)ZP zvJQWe_(lA1W&HjEe~R$+o2{)9SJmZmJQ?bZK!H~uBc6$ElF18~%SxnPZPgXThWk0- zA3xFqMHhaSI=uqF;O63|r&)cL_=uBu?eRy33`6OqUnRC`!GoAEUkCgV{)6$$Pe1f3 z{K*MZU@@De&E=)Vh#EEADgFt;Ls3$6;ST-^M2Gnpc)~FY_Zx)sKc4MX_><v!FKO*@jhg8zzh{!bJ!qU=(U1AgYmFD_2RTU%enu6MU%=N(Dk@dn=6wi)ZvQe>`W zA9q|8e{FT8i2weEwT*Mj9I)~V&pzITTesS!&Z&Z2EL#|l8Id8P zj_7NMx!sMO{daZ#+uCko|K6Q4R{Tp4y&m8Z-C%ciGc|_JooL|bG58js8`HMMH-1r~^Hu9T3QeIMw z)|M79_UhxO+teLr+QypKzVEK$uewq$`M+_^3K{Qnjvq$$(L-ozY_Q@#d|;o$H;F$| ztfR~$SSPS{>~8%0CUd$i8`e1ZSXc2^SBm%->LYC@$Mdp|py9?JxYgb+*OJ2AOfmPt zhPmh1Tk#k79DZ4s2p)cY;6xsjMi&rnM`@~UNEo)W@%AmYNJxl^tAAL9~n26Nv_JlBQmH)Azp zdT01;vD#lV_RO4ULVR=>xEI9OXt9`k#Q%w(`!rm0cyQdAqZZ_Rh5YPe`2O3|(r@{! zN#HwGkRyGC{-VoCvG=9FPd#VzZ;x5OCl|uR{?E3}>*V*CAGNo)i8#?i`vcgr=?=U>g8bUrtq5i7lqIDcH8qSFTH^6udPS+u|tvrtpDumA02OJXRbl4 z4`)vfMcUH&czx4axu;<3L!G_*U$oQr=%4>an4_hoEI`Jt9ny!K`T5;!r9Sy6