From ec006f25c4526a94c52f8c029d96b2f297b57396 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 5 Oct 2022 15:19:10 -0500 Subject: [PATCH 01/44] buttons overlaying content fix (#1005) * buttons overlaying content fix --- web/components/groups/group-overview-post.tsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/web/components/groups/group-overview-post.tsx b/web/components/groups/group-overview-post.tsx index 73c30e40..9f9dd399 100644 --- a/web/components/groups/group-overview-post.tsx +++ b/web/components/groups/group-overview-post.tsx @@ -6,12 +6,13 @@ import { Spacer } from '../layout/spacer' import { Group } from 'common/group' import { deleteFieldFromGroup, updateGroup } from 'web/lib/firebase/groups' import PencilIcon from '@heroicons/react/solid/PencilIcon' -import { DocumentRemoveIcon } from '@heroicons/react/solid' +import { TrashIcon } from '@heroicons/react/solid' import { createPost } from 'web/lib/firebase/api' import { Post } from 'common/post' import { deletePost, updatePost } from 'web/lib/firebase/posts' import { useState } from 'react' import { usePost } from 'web/hooks/use-post' +import { Col } from '../layout/col' export function GroupOverviewPost(props: { group: Group @@ -99,35 +100,31 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {

) : ( -
-
+ + + -
- - - -
+ + )} ) From f1f8082600d6debb6a229e080848458bf89fff41 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Wed, 5 Oct 2022 15:11:27 -0500 Subject: [PATCH 02/44] stats: round DAU number --- web/pages/stats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pages/stats.tsx b/web/pages/stats.tsx index 125af4bd..e2a9b279 100644 --- a/web/pages/stats.tsx +++ b/web/pages/stats.tsx @@ -103,7 +103,7 @@ export function CustomAnalytics(props: Stats) { title: 'Daily (7d avg)', content: ( ), From 2d56525d653d962d2e3965dfb2f45bb1ba734447 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 5 Oct 2022 15:40:26 -0500 Subject: [PATCH 03/44] made set width for portfolio/profit fields (#1006) --- web/components/portfolio/portfolio-value-section.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index 833060b4..2ee7d524 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -32,10 +32,10 @@ export const PortfolioValueSection = memo( return ( <> - + { From 10f0bbc63d832536e8139e2f91c047c2cbe29c17 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Wed, 5 Oct 2022 15:45:40 -0500 Subject: [PATCH 04/44] tournaments: included resolved markets --- web/lib/firebase/contracts.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index d7f6cd88..188c29bf 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -108,7 +108,6 @@ export const tournamentContractsByGroupSlugQuery = (slug: string) => query( contracts, where('groupSlugs', 'array-contains', slug), - where('isResolved', '==', false), orderBy('popularityScore', 'desc') ) From 189da4a0cf198bc023c3eebc83636ded77f24d21 Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 5 Oct 2022 16:21:03 -0500 Subject: [PATCH 05/44] made delete red, moved button for regular posts (#1008) --- web/components/groups/group-overview-post.tsx | 2 +- web/pages/post/[...slugs]/index.tsx | 35 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/web/components/groups/group-overview-post.tsx b/web/components/groups/group-overview-post.tsx index 9f9dd399..01ca13ba 100644 --- a/web/components/groups/group-overview-post.tsx +++ b/web/components/groups/group-overview-post.tsx @@ -121,7 +121,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) { deleteGroupAboutPost() }} > - + diff --git a/web/pages/post/[...slugs]/index.tsx b/web/pages/post/[...slugs]/index.tsx index aab75eab..beaff445 100644 --- a/web/pages/post/[...slugs]/index.tsx +++ b/web/pages/post/[...slugs]/index.tsx @@ -207,25 +207,20 @@ export function RichEditPost(props: { post: Post }) { ) : ( - <> -
-
- -
- - - -
- + + + + + + ) } From 1ef1af8234b5e701e52b3c8f1472b297977bd0df Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 5 Oct 2022 16:49:28 -0500 Subject: [PATCH 06/44] Fix localstorage saved user being overwritten on every page load --- web/components/auth-context.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index 19ced0b2..223fe123 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -68,11 +68,11 @@ export function AuthProvider(props: { }, [setAuthUser, serverUser]) useEffect(() => { - if (authUser != null) { + if (authUser) { // Persist to local storage, to reduce login blink next time. // Note: Cap on localStorage size is ~5mb localStorage.setItem(CACHED_USER_KEY, JSON.stringify(authUser)) - } else { + } else if (authUser === null) { localStorage.removeItem(CACHED_USER_KEY) } }, [authUser]) From a3acd3fa3c5884b579671b41bfc3e158c8d04e70 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 5 Oct 2022 16:52:16 -0500 Subject: [PATCH 07/44] Market page: Show no right panel while user loading --- web/pages/[username]/[contractSlug].tsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 2fb02642..1de472c5 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -207,14 +207,18 @@ export function ContractPageContent( return ( - - {isCreator && ( - - - - )} - + user || user === null ? ( + <> + + {isCreator && ( + + + + )} + + ) : ( +
+ ) } > {showConfetti && ( From 0818a943071af30406b39156009fe9e7b9be3c38 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 5 Oct 2022 16:56:47 -0500 Subject: [PATCH 08/44] Don't flash sign in button if user is loading --- web/components/nav/sidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index efe7994b..23b1115c 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -63,7 +63,8 @@ export default function Sidebar(props: { )} - {!user && } + {user === undefined &&
} + {user === null && } {user && } From 60aa294131e57e19466fe5575ad1f627d9d7911b Mon Sep 17 00:00:00 2001 From: mantikoros Date: Wed, 5 Oct 2022 16:57:01 -0500 Subject: [PATCH 09/44] election map coloring --- common/util/color.ts | 24 +++++++++++++++++++ web/components/usa-map/state-election-map.tsx | 7 +++--- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 common/util/color.ts diff --git a/common/util/color.ts b/common/util/color.ts new file mode 100644 index 00000000..fb15cc6a --- /dev/null +++ b/common/util/color.ts @@ -0,0 +1,24 @@ +export const interpolateColor = (color1: string, color2: string, p: number) => { + const rgb1 = parseInt(color1.replace('#', ''), 16) + const rgb2 = parseInt(color2.replace('#', ''), 16) + + const [r1, g1, b1] = toArray(rgb1) + const [r2, g2, b2] = toArray(rgb2) + + const q = 1 - p + const rr = Math.round(r1 * q + r2 * p) + const rg = Math.round(g1 * q + g2 * p) + const rb = Math.round(b1 * q + b2 * p) + + const hexString = Number((rr << 16) + (rg << 8) + rb).toString(16) + const hex = `#${'0'.repeat(6 - hexString.length)}${hexString}` + return hex +} + +function toArray(rgb: number) { + const r = rgb >> 16 + const g = (rgb >> 8) % 256 + const b = rgb % 256 + + return [r, g, b] +} diff --git a/web/components/usa-map/state-election-map.tsx b/web/components/usa-map/state-election-map.tsx index 8f7bb284..5e5a9a64 100644 --- a/web/components/usa-map/state-election-map.tsx +++ b/web/components/usa-map/state-election-map.tsx @@ -9,6 +9,7 @@ import { getContractFromSlug, listenForContract, } from 'web/lib/firebase/contracts' +import { interpolateColor } from 'common/util/color' export interface StateElectionMarket { creatorUsername: string @@ -45,10 +46,8 @@ export function StateElectionMap(props: { markets: StateElectionMarket[] }) { const probToColor = (prob: number, isWinRepublican: boolean) => { const p = isWinRepublican ? prob : 1 - prob - const hue = p > 0.5 ? 350 : 240 - const saturation = 100 - const lightness = 100 - 50 * Math.abs(p - 0.5) - return `hsl(${hue}, ${saturation}%, ${lightness}%)` + const color = p > 0.5 ? '#e4534b' : '#5f6eb0' + return interpolateColor('#ebe4ec', color, Math.abs(p - 0.5) * 2) } const useContracts = (slugs: string[]) => { From b8911cafe8b4eb094e7199b3437d6f8ac6f97dee Mon Sep 17 00:00:00 2001 From: ingawei <46611122+ingawei@users.noreply.github.com> Date: Wed, 5 Oct 2022 17:07:41 -0500 Subject: [PATCH 10/44] market group modal scroll fix (#1009) --- .../groups/contract-groups-list.tsx | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/web/components/groups/contract-groups-list.tsx b/web/components/groups/contract-groups-list.tsx index d39a35d3..e19e2113 100644 --- a/web/components/groups/contract-groups-list.tsx +++ b/web/components/groups/contract-groups-list.tsx @@ -53,30 +53,30 @@ export function ContractGroupsList(props: { /> )} - {groups.length === 0 && ( - - No groups yet... - - )} - {groups.map((group) => ( - - - + + {groups.length === 0 && ( + No groups yet... + )} + {groups.map((group) => ( + + + + + {user && canModifyGroupContracts(group, user.id) && ( + + )} - {user && canModifyGroupContracts(group, user.id) && ( - - )} - - ))} + ))} + ) } From a3b841423f5d3ccc7d538349273dd393152aea6e Mon Sep 17 00:00:00 2001 From: mantikoros Date: Wed, 5 Oct 2022 17:12:42 -0500 Subject: [PATCH 11/44] midterms: posititoning, make mobile friendly --- web/components/usa-map/usa-map.tsx | 70 +++++++++++++++--------------- web/pages/midterms.tsx | 33 +++----------- 2 files changed, 41 insertions(+), 62 deletions(-) diff --git a/web/components/usa-map/usa-map.tsx b/web/components/usa-map/usa-map.tsx index 2841e04c..c372397e 100644 --- a/web/components/usa-map/usa-map.tsx +++ b/web/components/usa-map/usa-map.tsx @@ -53,8 +53,6 @@ export const USAMap = ({ onClick = (e) => { console.log(e.currentTarget.dataset.name) }, - width = 959, - height = 593, title = 'US states map', defaultFill = '#d3d3d3', customize, @@ -67,40 +65,40 @@ export const USAMap = ({ const stateClickHandler = (state: string) => customize?.[state]?.clickHandler return ( - - {title} - - {States({ - hideStateTitle, - fillStateColor, - stateClickHandler, - })} - - - +
+ + {title} + + {States({ + hideStateTitle, + fillStateColor, + stateClickHandler, + })} + + + + - - + +
) } diff --git a/web/pages/midterms.tsx b/web/pages/midterms.tsx index d1dfb509..47930bc1 100644 --- a/web/pages/midterms.tsx +++ b/web/pages/midterms.tsx @@ -184,67 +184,48 @@ const App = () => {
Governors
House
Related markets
From 7863a4232d873cdc64195ec8fb358f36050f1226 Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Wed, 5 Oct 2022 15:51:10 -0700 Subject: [PATCH 12/44] Un-daisy share buttons (#1010) * Make embed and challenge buttons non-daisyui * Allow link Buttons. Change tweet, dupe buttons. * lint --- web/components/button.tsx | 75 ++++++++++---------- web/components/contract/share-modal.tsx | 18 +++-- web/components/duplicate-contract-button.tsx | 21 +++--- web/components/share-embed-button.tsx | 26 ++++--- web/components/share-post-modal.tsx | 8 +-- web/components/tweet-button.tsx | 19 ++--- web/lib/icons/twitter-logo.tsx | 28 ++++++++ web/public/twitter-icon-white.svg | 16 ----- 8 files changed, 108 insertions(+), 103 deletions(-) create mode 100644 web/lib/icons/twitter-logo.tsx delete mode 100644 web/public/twitter-icon-white.svg diff --git a/web/components/button.tsx b/web/components/button.tsx index 57c0bb4b..b64b8daf 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -15,12 +15,49 @@ export type ColorType = | 'gray-white' | 'highlight-blue' +const sizeClasses = { + '2xs': 'px-2 py-1 text-xs', + xs: 'px-2.5 py-1.5 text-sm', + sm: 'px-3 py-2 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-4 py-2 text-base', + xl: 'px-6 py-2.5 text-base font-semibold', + '2xl': 'px-6 py-3 text-xl font-semibold', +} + +export function buttonClass(size: SizeType, color: ColorType | 'override') { + return clsx( + 'font-md inline-flex items-center justify-center rounded-md border border-transparent shadow-sm transition-colors disabled:cursor-not-allowed', + sizeClasses[size], + color === 'green' && + 'disabled:bg-greyscale-2 bg-teal-500 text-white hover:bg-teal-600', + color === 'red' && + 'disabled:bg-greyscale-2 bg-red-400 text-white hover:bg-red-500', + color === 'yellow' && + 'disabled:bg-greyscale-2 bg-yellow-400 text-white hover:bg-yellow-500', + color === 'blue' && + 'disabled:bg-greyscale-2 bg-blue-400 text-white hover:bg-blue-500', + color === 'indigo' && + 'disabled:bg-greyscale-2 bg-indigo-500 text-white hover:bg-indigo-600', + color === 'gray' && + 'bg-greyscale-1 text-greyscale-6 hover:bg-greyscale-2 disabled:opacity-50', + color === 'gray-outline' && + 'border-greyscale-4 text-greyscale-4 hover:bg-greyscale-4 border-2 hover:text-white disabled:opacity-50', + color === 'gradient' && + 'disabled:bg-greyscale-2 border-none bg-gradient-to-r from-indigo-500 to-blue-500 text-white hover:from-indigo-700 hover:to-blue-700', + color === 'gray-white' && + 'text-greyscale-6 hover:bg-greyscale-2 border-none shadow-none disabled:opacity-50', + color === 'highlight-blue' && + 'text-highlight-blue disabled:bg-greyscale-2 border-none shadow-none' + ) +} + export function Button(props: { className?: string onClick?: MouseEventHandler | undefined children?: ReactNode size?: SizeType - color?: ColorType + color?: ColorType | 'override' type?: 'button' | 'reset' | 'submit' disabled?: boolean loading?: boolean @@ -36,44 +73,10 @@ export function Button(props: { loading, } = props - const sizeClasses = { - '2xs': 'px-2 py-1 text-xs', - xs: 'px-2.5 py-1.5 text-sm', - sm: 'px-3 py-2 text-sm', - md: 'px-4 py-2 text-sm', - lg: 'px-4 py-2 text-base', - xl: 'px-6 py-2.5 text-base font-semibold', - '2xl': 'px-6 py-3 text-xl font-semibold', - }[size] - return ( + )}
diff --git a/web/components/duplicate-contract-button.tsx b/web/components/duplicate-contract-button.tsx index c02a2a9c..9c89d8c5 100644 --- a/web/components/duplicate-contract-button.tsx +++ b/web/components/duplicate-contract-button.tsx @@ -3,27 +3,22 @@ import clsx from 'clsx' import { Contract } from 'common/contract' import { getMappedValue } from 'common/pseudo-numeric' import { trackCallback } from 'web/lib/service/analytics' +import { buttonClass } from './button' -export function DuplicateContractButton(props: { - contract: Contract - className?: string -}) { - const { contract, className } = props +export function DuplicateContractButton(props: { contract: Contract }) { + const { contract } = props return ( - ) diff --git a/web/components/share-embed-button.tsx b/web/components/share-embed-button.tsx index 0a5dc0c9..ad57b2f7 100644 --- a/web/components/share-embed-button.tsx +++ b/web/components/share-embed-button.tsx @@ -1,6 +1,5 @@ import React from 'react' import { CodeIcon } from '@heroicons/react/outline' -import { Menu } from '@headlessui/react' import toast from 'react-hot-toast' import { Contract } from 'common/contract' @@ -8,6 +7,7 @@ import { contractPath } from 'web/lib/firebase/contracts' import { DOMAIN } from 'common/envs/constants' import { copyToClipboard } from 'web/lib/util/copy' import { track } from 'web/lib/service/analytics' +import { Button } from './button' export function embedContractCode(contract: Contract) { const title = contract.question @@ -15,6 +15,7 @@ export function embedContractCode(contract: Contract) { return `` } +// TODO: move this function elsewhere export function embedContractGridCode(contracts: Contract[]) { const height = (contracts.length - (contracts.length % 2)) * 100 + 'px' const src = `https://${DOMAIN}/embed/grid/${contracts @@ -26,24 +27,21 @@ export function embedContractGridCode(contracts: Contract[]) { export function ShareEmbedButton(props: { contract: Contract }) { const { contract } = props - const codeIcon =
+ + ) } diff --git a/web/pages/midterms.tsx b/web/pages/midterms.tsx index 47930bc1..e838ca3f 100644 --- a/web/pages/midterms.tsx +++ b/web/pages/midterms.tsx @@ -1,11 +1,15 @@ +import { CPMMBinaryContract } from 'common/contract' +import { useEffect } from 'react' import { Col } from 'web/components/layout/col' import { Spacer } from 'web/components/layout/spacer' import { Page } from 'web/components/page' +import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' import { StateElectionMarket, StateElectionMap, } from 'web/components/usa-map/state-election-map' +import { getContractFromSlug } from 'web/lib/firebase/contracts' const senateMidterms: StateElectionMarket[] = [ { @@ -175,27 +179,60 @@ const governorMidterms: StateElectionMarket[] = [ }, ] -const App = () => { +export async function getStaticProps() { + const senateContracts = await Promise.all( + senateMidterms.map((m) => getContractFromSlug(m.slug)) + ) + + const governorContracts = await Promise.all( + governorMidterms.map((m) => getContractFromSlug(m.slug)) + ) + + return { + props: { senateContracts, governorContracts }, + revalidate: 60, // regenerate after a minute + } +} + +const App = (props: { + senateContracts: CPMMBinaryContract[] + governorContracts: CPMMBinaryContract[] +}) => { + useSetIframeBackbroundColor() + const { senateContracts, governorContracts } = props + return ( + <SEO + title="2022 US Midterm Elections" + description="Bet on the midterm elections using prediction markets. See Manifold's state-by-state breakdown of senate and governor races." + /> <div className="mt-2 text-2xl">Senate</div> - <StateElectionMap markets={senateMidterms} /> + <StateElectionMap + markets={senateMidterms} + contracts={senateContracts} + /> <iframe src="https://manifold.markets/TomShlomi/will-the-gop-control-the-us-senate" frameBorder="0" className="mt-8 flex h-96 w-full" ></iframe> <Spacer h={8} /> + <div className="mt-8 text-2xl">Governors</div> - <StateElectionMap markets={governorMidterms} /> + <StateElectionMap + markets={governorMidterms} + contracts={governorContracts} + /> <iframe src="https://manifold.markets/ManifoldMarkets/democrats-go-down-at-least-one-gove" frameBorder="0" className="mt-8 flex h-96 w-full" ></iframe> <Spacer h={8} /> + <div className="mt-8 text-2xl">House</div> <iframe src="https://manifold.markets/BoltonBailey/will-democrats-maintain-control-of" @@ -203,6 +240,7 @@ const App = () => { className="mt-8 flex h-96 w-full" ></iframe> <Spacer h={8} /> + <div className="mt-8 text-2xl">Related markets</div> <iframe src="https://manifold.markets/BoltonBailey/balance-of-power-in-us-congress-aft" @@ -232,4 +270,13 @@ const App = () => { ) } +const useSetIframeBackbroundColor = () => { + useEffect(() => { + if (window.location.host !== 'manifold.markets') return + for (let i = 0; i < self.frames.length; i++) { + self.frames[i].document.body.style.backgroundColor = '#f9fafb' + } + }, []) +} + export default App From 81fb2456bd61806cfca29413ae4022481f2a8b5d Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 18:29:08 -0500 Subject: [PATCH 15/44] remove hook --- web/pages/midterms.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/web/pages/midterms.tsx b/web/pages/midterms.tsx index e838ca3f..77efc7fd 100644 --- a/web/pages/midterms.tsx +++ b/web/pages/midterms.tsx @@ -1,5 +1,4 @@ import { CPMMBinaryContract } from 'common/contract' -import { useEffect } from 'react' import { Col } from 'web/components/layout/col' import { Spacer } from 'web/components/layout/spacer' import { Page } from 'web/components/page' @@ -198,7 +197,6 @@ const App = (props: { senateContracts: CPMMBinaryContract[] governorContracts: CPMMBinaryContract[] }) => { - useSetIframeBackbroundColor() const { senateContracts, governorContracts } = props return ( @@ -270,13 +268,4 @@ const App = (props: { ) } -const useSetIframeBackbroundColor = () => { - useEffect(() => { - if (window.location.host !== 'manifold.markets') return - for (let i = 0; i < self.frames.length; i++) { - self.frames[i].document.body.style.backgroundColor = '#f9fafb' - } - }, []) -} - export default App From a149777c0eece99dbe768b24bfa79ef8138ac203 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 18:36:32 -0500 Subject: [PATCH 16/44] turn off sprig on dev --- web/lib/service/sprig.ts | 42 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/web/lib/service/sprig.ts b/web/lib/service/sprig.ts index b827253f..f89a9678 100644 --- a/web/lib/service/sprig.ts +++ b/web/lib/service/sprig.ts @@ -5,30 +5,32 @@ import { ENV_CONFIG } from 'common/envs/constants' import { PROD_CONFIG } from 'common/envs/prod' -try { - ;(function (l, e, a, p) { - if (window.Sprig) return - window.Sprig = function (...args) { - S._queue.push(args) - } - const S = window.Sprig - S.appId = a - S._queue = [] - window.UserLeap = S - a = l.createElement('script') - a.async = 1 - a.src = e + '?id=' + S.appId - p = l.getElementsByTagName('script')[0] - ENV_CONFIG.domain === PROD_CONFIG.domain && p.parentNode.insertBefore(a, p) - })(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId) -} catch (error) { - console.log('Error initializing Sprig, please complain to Barak', error) +if (ENV_CONFIG.domain === PROD_CONFIG.domain) { + try { + ;(function (l, e, a, p) { + if (window.Sprig) return + window.Sprig = function (...args) { + S._queue.push(args) + } + const S = window.Sprig + S.appId = a + S._queue = [] + window.UserLeap = S + a = l.createElement('script') + a.async = 1 + a.src = e + '?id=' + S.appId + p = l.getElementsByTagName('script')[0] + p.parentNode.insertBefore(a, p) + })(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId) + } catch (error) { + console.log('Error initializing Sprig, please complain to Barak', error) + } } export function setUserId(userId: string): void { - window.Sprig('setUserId', userId) + if (window.Sprig) window.Sprig('setUserId', userId) } export function setAttributes(attributes: Record<string, unknown>): void { - window.Sprig('setAttributes', attributes) + if (window.Sprig) window.Sprig('setAttributes', attributes) } From 5d7721e041019162a6ad656b1805d106a78abe69 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 5 Oct 2022 18:43:51 -0500 Subject: [PATCH 17/44] Render timestamp only on client to prevent error of server not matching client --- web/components/relative-timestamp.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/components/relative-timestamp.tsx b/web/components/relative-timestamp.tsx index d4b1c189..eb03e324 100644 --- a/web/components/relative-timestamp.tsx +++ b/web/components/relative-timestamp.tsx @@ -1,15 +1,22 @@ import { DateTimeTooltip } from './datetime-tooltip' -import React from 'react' +import React, { useEffect, useState } from 'react' import { fromNow } from 'web/lib/util/time' export function RelativeTimestamp(props: { time: number }) { const { time } = props + const [isClient, setIsClient] = useState(false) + + useEffect(() => { + // Only render on client to prevent difference from server. + setIsClient(true) + }, []) + return ( <DateTimeTooltip className="ml-1 whitespace-nowrap text-gray-400" time={time} > - {fromNow(time)} + {isClient ? fromNow(time) : ''} </DateTimeTooltip> ) } From 935bdd12a7208f416be24561bdab39ededadfe6c Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Wed, 5 Oct 2022 18:44:28 -0500 Subject: [PATCH 18/44] Make sized container have default height so graph doesn't jump --- web/components/sized-container.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/components/sized-container.tsx b/web/components/sized-container.tsx index 26532047..03fe9b11 100644 --- a/web/components/sized-container.tsx +++ b/web/components/sized-container.tsx @@ -29,7 +29,14 @@ export const SizedContainer = (props: { }, [threshold, fullHeight, mobileHeight]) return ( <div ref={containerRef}> - {width != null && height != null && children(width, height)} + {width != null && height != null ? ( + children(width, height) + ) : ( + <> + <div className="sm:hidden" style={{ height: mobileHeight }} /> + <div className="hidden sm:flex" style={{ height: fullHeight }} /> + </> + )} </div> ) } From 7ce09ae39d2cbb817c428bd476ce5d61c9766e83 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 19:23:47 -0500 Subject: [PATCH 19/44] midterms: use null in static props --- web/components/usa-map/state-election-map.tsx | 15 +++++++++------ web/pages/midterms.tsx | 8 ++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/web/components/usa-map/state-election-map.tsx b/web/components/usa-map/state-election-map.tsx index ac642fa0..a9a53a91 100644 --- a/web/components/usa-map/state-election-map.tsx +++ b/web/components/usa-map/state-election-map.tsx @@ -59,13 +59,16 @@ const useUpdateContracts = ( if (contracts.some((c) => c === undefined)) return // listen to contract updates - const unsubs = contracts.map((c, i) => - listenForContract( - c.id, - (newC) => - newC && setContracts(setAt(contracts, i, newC as CPMMBinaryContract)) + const unsubs = contracts + .filter((c) => !!c) + .map((c, i) => + listenForContract( + c.id, + (newC) => + newC && + setContracts(setAt(contracts, i, newC as CPMMBinaryContract)) + ) ) - ) return () => unsubs.forEach((u) => u()) }, [contracts, setContracts]) diff --git a/web/pages/midterms.tsx b/web/pages/midterms.tsx index 77efc7fd..d14f9d0f 100644 --- a/web/pages/midterms.tsx +++ b/web/pages/midterms.tsx @@ -180,11 +180,15 @@ const governorMidterms: StateElectionMarket[] = [ export async function getStaticProps() { const senateContracts = await Promise.all( - senateMidterms.map((m) => getContractFromSlug(m.slug)) + senateMidterms.map((m) => + getContractFromSlug(m.slug).then((c) => c ?? null) + ) ) const governorContracts = await Promise.all( - governorMidterms.map((m) => getContractFromSlug(m.slug)) + governorMidterms.map((m) => + getContractFromSlug(m.slug).then((c) => c ?? null) + ) ) return { From 94624c5387d1048f16957227f913fca267745a29 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Wed, 5 Oct 2022 18:02:24 -0700 Subject: [PATCH 20/44] Create common card component (#1012) * Create common card component * lint --- web/components/card.tsx | 16 +++++ web/components/charity/charity-card.tsx | 67 ++++++++++--------- web/components/contract/contract-card.tsx | 22 ++---- web/components/contract/prob-change-table.tsx | 42 ++---------- web/components/post-card.tsx | 8 +-- 5 files changed, 69 insertions(+), 86 deletions(-) create mode 100644 web/components/card.tsx diff --git a/web/components/card.tsx b/web/components/card.tsx new file mode 100644 index 00000000..e9d7f8c5 --- /dev/null +++ b/web/components/card.tsx @@ -0,0 +1,16 @@ +import clsx from 'clsx' + +export function Card(props: JSX.IntrinsicElements['div']) { + const { children, className, ...rest } = props + return ( + <div + className={clsx( + 'cursor-pointer rounded-lg border-2 bg-white transition-shadow hover:shadow-md focus:shadow-md', + className + )} + {...rest} + > + {children} + </div> + ) +} diff --git a/web/components/charity/charity-card.tsx b/web/components/charity/charity-card.tsx index fc327b9f..5222f284 100644 --- a/web/components/charity/charity-card.tsx +++ b/web/components/charity/charity-card.tsx @@ -6,6 +6,8 @@ import { Charity } from 'common/charity' import { useCharityTxns } from 'web/hooks/use-charity-txns' import { manaToUSD } from '../../../common/util/format' import { Row } from '../layout/row' +import { Col } from '../layout/col' +import { Card } from '../card' export function CharityCard(props: { charity: Charity; match?: number }) { const { charity } = props @@ -15,43 +17,44 @@ export function CharityCard(props: { charity: Charity; match?: number }) { const raised = sumBy(txns, (txn) => txn.amount) return ( - <Link href={`/charity/${slug}`} passHref> - <div className="card card-compact transition:shadow flex-1 cursor-pointer border-2 bg-white hover:shadow-md"> - <Row className="mt-6 mb-2"> - {tags?.includes('Featured') && <FeaturedBadge />} - </Row> - <div className="px-8"> - <figure className="relative h-32"> - {photo ? ( - <Image src={photo} alt="" layout="fill" objectFit="contain" /> - ) : ( - <div className="h-full w-full bg-gradient-to-r from-slate-300 to-indigo-200" /> - )} - </figure> - </div> - <div className="card-body"> - {/* <h3 className="card-title line-clamp-3">{name}</h3> */} - <div className="line-clamp-4 text-sm">{preview}</div> - {raised > 0 && ( - <> - <Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900"> - <Row className="items-baseline gap-1"> - <span className="text-3xl font-semibold"> - {formatUsd(raised)} - </span> - raised - </Row> - {/* {match && ( + <Link href={`/charity/${slug}`}> + <a className="flex-1"> + <Card className="!rounded-2xl"> + <Row className="mt-6 mb-2"> + {tags?.includes('Featured') && <FeaturedBadge />} + </Row> + <div className="px-8"> + <figure className="relative h-32"> + {photo ? ( + <Image src={photo} alt="" layout="fill" objectFit="contain" /> + ) : ( + <div className="h-full w-full bg-gradient-to-r from-slate-300 to-indigo-200" /> + )} + </figure> + </div> + <Col className="p-8"> + <div className="line-clamp-4 text-sm">{preview}</div> + {raised > 0 && ( + <> + <Row className="mt-4 flex-1 items-end justify-center gap-6 text-gray-900"> + <Row className="items-baseline gap-1"> + <span className="text-3xl font-semibold"> + {formatUsd(raised)} + </span> + raised + </Row> + {/* {match && ( <Col className="text-gray-500"> <span className="text-xl">+{formatUsd(match)}</span> <span className="">match</span> </Col> )} */} - </Row> - </> - )} - </div> - </div> + </Row> + </> + )} + </Col> + </Card> + </a> </Link> ) } diff --git a/web/components/contract/contract-card.tsx b/web/components/contract/contract-card.tsx index 401005bf..a9ee6318 100644 --- a/web/components/contract/contract-card.tsx +++ b/web/components/contract/contract-card.tsx @@ -8,6 +8,7 @@ import { BinaryContract, Contract, CPMMBinaryContract, + CPMMContract, FreeResponseContract, MultipleChoiceContract, NumericContract, @@ -35,6 +36,7 @@ import { getMappedValue } from 'common/pseudo-numeric' import { Tooltip } from '../tooltip' import { SiteLink } from '../site-link' import { ProbChange } from './prob-change-table' +import { Card } from '../card' export function ContractCard(props: { contract: Contract @@ -75,12 +77,7 @@ export function ContractCard(props: { !hideQuickBet return ( - <Row - className={clsx( - 'group relative gap-3 rounded-lg bg-white shadow-md hover:cursor-pointer hover:bg-gray-100', - className - )} - > + <Card className={clsx('group relative flex gap-3', className)}> <Col className="relative flex-1 gap-3 py-4 pb-12 pl-6"> <AvatarDetails contract={contract} @@ -195,7 +192,7 @@ export function ContractCard(props: { /> </Link> )} - </Row> + </Card> ) } @@ -391,7 +388,7 @@ export function PseudoNumericResolutionOrExpectation(props: { } export function ContractCardProbChange(props: { - contract: CPMMBinaryContract + contract: CPMMContract noLinkAvatar?: boolean className?: string }) { @@ -399,12 +396,7 @@ export function ContractCardProbChange(props: { const contract = useContractWithPreload(props.contract) as CPMMBinaryContract return ( - <Col - className={clsx( - className, - 'mb-4 rounded-lg bg-white shadow hover:bg-gray-100 hover:shadow-lg' - )} - > + <Card className={clsx(className, 'mb-4')}> <AvatarDetails contract={contract} className={'px-6 pt-4'} @@ -419,6 +411,6 @@ export function ContractCardProbChange(props: { </SiteLink> <ProbChange className="py-2 pr-4" contract={contract} /> </Row> - </Col> + </Card> ) } diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 6b671830..70eaf18c 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -1,13 +1,10 @@ -import { sortBy } from 'lodash' import clsx from 'clsx' -import { contractPath } from 'web/lib/firebase/contracts' import { CPMMContract } from 'common/contract' import { formatPercent } from 'common/util/format' -import { SiteLink } from '../site-link' +import { sortBy } from 'lodash' import { Col } from '../layout/col' -import { Row } from '../layout/row' import { LoadingIndicator } from '../loading-indicator' -import { useContractWithPreload } from 'web/hooks/use-contract' +import { ContractCardProbChange } from './contract-card' export function ProbChangeTable(props: { changes: CPMMContract[] | undefined @@ -39,46 +36,21 @@ export function ProbChangeTable(props: { if (rows === 0) return <div className="px-4 text-gray-500">None</div> return ( - <Col className="mb-4 w-full divide-x-2 divide-y rounded-lg bg-white shadow-md md:flex-row md:divide-y-0"> - <Col className="flex-1 divide-y"> + <Col className="mb-4 w-full gap-4 rounded-lg md:flex-row"> + <Col className="flex-1 gap-4"> {filteredPositiveChanges.map((contract) => ( - <ProbChangeRow key={contract.id} contract={contract} /> + <ContractCardProbChange key={contract.id} contract={contract} /> ))} </Col> - <Col className="flex-1 divide-y"> + <Col className="flex-1 gap-4"> {filteredNegativeChanges.map((contract) => ( - <ProbChangeRow key={contract.id} contract={contract} /> + <ContractCardProbChange key={contract.id} contract={contract} /> ))} </Col> </Col> ) } -export function ProbChangeRow(props: { - contract: CPMMContract - className?: string -}) { - const { className } = props - const contract = - (useContractWithPreload(props.contract) as CPMMContract) ?? props.contract - return ( - <Row - className={clsx( - 'items-center justify-between gap-4 hover:bg-gray-100', - className - )} - > - <SiteLink - className="p-4 pr-0 font-semibold text-indigo-700" - href={contractPath(contract)} - > - <span className="line-clamp-2">{contract.question}</span> - </SiteLink> - <ProbChange className="py-2 pr-4 text-xl" contract={contract} /> - </Row> - ) -} - export function ProbChange(props: { contract: CPMMContract className?: string diff --git a/web/components/post-card.tsx b/web/components/post-card.tsx index ed208f63..19dce45a 100644 --- a/web/components/post-card.tsx +++ b/web/components/post-card.tsx @@ -7,8 +7,8 @@ import { useUserById } from 'web/hooks/use-user' import { postPath } from 'web/lib/firebase/posts' import { fromNow } from 'web/lib/util/time' import { Avatar } from './avatar' +import { Card } from './card' import { CardHighlightOptions } from './contract/contracts-grid' -import { Row } from './layout/row' import { UserLink } from './user-link' export function PostCard(props: { @@ -26,9 +26,9 @@ export function PostCard(props: { return ( <div className="relative py-1"> - <Row + <Card className={clsx( - 'relative gap-3 rounded-lg bg-white py-2 px-3 shadow-md hover:cursor-pointer hover:bg-gray-100', + 'relative flex gap-3 py-2 px-3', itemIds?.includes(post.id) && highlightClassName )} > @@ -58,7 +58,7 @@ export function PostCard(props: { Post </span> </div> - </Row> + </Card> {onPostClick ? ( <a className="absolute top-0 left-0 right-0 bottom-0" From f1e400765a4bbfdd1471d1f51a995b06da852be9 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 21:31:34 -0500 Subject: [PATCH 21/44] add key prop to pills --- web/components/onboarding/group-selector-dialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/onboarding/group-selector-dialog.tsx b/web/components/onboarding/group-selector-dialog.tsx index 0a246403..9c489bb1 100644 --- a/web/components/onboarding/group-selector-dialog.tsx +++ b/web/components/onboarding/group-selector-dialog.tsx @@ -72,6 +72,7 @@ export default function GroupSelectorDialog(props: { ) : ( displayedGroups.map((group) => ( <PillButton + key={group.id} selected={memberGroupIds.includes(group.id)} onSelect={withTracking( () => From cd8245fbee8bd362c52369103544b06fdd5f3bbb Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 21:38:13 -0500 Subject: [PATCH 22/44] redirect to /home after login --- web/pages/index.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 4013f57a..91572a16 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,3 +1,6 @@ +import { useEffect } from 'react' +import Router from 'next/router' + import { Contract, getTrendingContracts } from 'web/lib/firebase/contracts' import { Page } from 'web/components/page' import { LandingPagePanel } from 'web/components/landing-page-panel' @@ -6,6 +9,7 @@ import { ManifoldLogo } from 'web/components/nav/manifold-logo' import { redirectIfLoggedIn } from 'web/lib/firebase/server-auth' import { useSaveReferral } from 'web/hooks/use-save-referral' import { SEO } from 'web/components/SEO' +import { useUser } from 'web/hooks/use-user' export const getServerSideProps = redirectIfLoggedIn('/home', async (_) => { const hotContracts = await getTrendingContracts() @@ -16,6 +20,7 @@ export default function Home(props: { hotContracts: Contract[] }) { const { hotContracts } = props useSaveReferral() + useRedirectAfterLogin() return ( <Page> @@ -35,3 +40,13 @@ export default function Home(props: { hotContracts: Contract[] }) { </Page> ) } + +const useRedirectAfterLogin = () => { + const user = useUser() + + useEffect(() => { + if (user) { + Router.replace('/home') + } + }, [user]) +} From e1f24f24a96fa8d811ebcaa3b10b19d9b67cb282 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 22:18:19 -0500 Subject: [PATCH 23/44] create market: use transaction --- functions/src/create-market.ts | 457 +++++++++++++++++---------------- 1 file changed, 233 insertions(+), 224 deletions(-) diff --git a/functions/src/create-market.ts b/functions/src/create-market.ts index d1483ca4..38852184 100644 --- a/functions/src/create-market.ts +++ b/functions/src/create-market.ts @@ -15,7 +15,7 @@ import { import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' -import { chargeUser, getContract, isProd } from './utils' +import { isProd } from './utils' import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api' import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy' @@ -36,7 +36,7 @@ import { getPseudoProbability } from '../../common/pseudo-numeric' import { JSONContent } from '@tiptap/core' import { uniq, zip } from 'lodash' import { Bet } from '../../common/bet' -import { FieldValue } from 'firebase-admin/firestore' +import { FieldValue, Transaction } from 'firebase-admin/firestore' const descScehma: z.ZodType<JSONContent> = z.lazy(() => z.intersection( @@ -107,229 +107,242 @@ export async function createMarketHelper(body: any, auth: AuthedUser) { visibility = 'public', } = validate(bodySchema, body) - let min, max, initialProb, isLogScale, answers + return await firestore.runTransaction(async (trans) => { + let min, max, initialProb, isLogScale, answers - if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { - let initialValue - ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) - if (max - min <= 0.01 || initialValue <= min || initialValue >= max) - throw new APIError(400, 'Invalid range.') + if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { + let initialValue + ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) + if (max - min <= 0.01 || initialValue <= min || initialValue >= max) + throw new APIError(400, 'Invalid range.') - initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100 + initialProb = + getPseudoProbability(initialValue, min, max, isLogScale) * 100 - if (initialProb < 1 || initialProb > 99) - if (outcomeType === 'PSEUDO_NUMERIC') + if (initialProb < 1 || initialProb > 99) + if (outcomeType === 'PSEUDO_NUMERIC') + throw new APIError( + 400, + `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` + ) + else throw new APIError(400, 'Invalid initial probability.') + } + + if (outcomeType === 'BINARY') { + ;({ initialProb } = validate(binarySchema, body)) + } + + if (outcomeType === 'MULTIPLE_CHOICE') { + ;({ answers } = validate(multipleChoiceSchema, body)) + } + + const userDoc = await trans.get(firestore.collection('users').doc(auth.uid)) + if (!userDoc.exists) { + throw new APIError(400, 'No user exists with the authenticated user ID.') + } + const user = userDoc.data() as User + + const ante = FIXED_ANTE + const deservesFreeMarket = + (user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX + // TODO: this is broken because it's not in a transaction + if (ante > user.balance && !deservesFreeMarket) + throw new APIError(400, `Balance must be at least ${ante}.`) + + let group: Group | null = null + if (groupId) { + const groupDocRef = firestore.collection('groups').doc(groupId) + const groupDoc = await trans.get(groupDocRef) + if (!groupDoc.exists) { + throw new APIError(400, 'No group exists with the given group ID.') + } + + group = groupDoc.data() as Group + const groupMembersSnap = await trans.get( + firestore.collection(`groups/${groupId}/groupMembers`) + ) + const groupMemberDocs = groupMembersSnap.docs.map( + (doc) => doc.data() as { userId: string; createdTime: number } + ) + if ( + !groupMemberDocs.map((m) => m.userId).includes(user.id) && + !group.anyoneCanJoin && + group.creatorId !== user.id + ) { throw new APIError( 400, - `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` + 'User must be a member/creator of the group or group must be open to add markets to it.' ) - else throw new APIError(400, 'Invalid initial probability.') - } + } + } + const slug = await getSlug(trans, question) + const contractRef = firestore.collection('contracts').doc() - if (outcomeType === 'BINARY') { - ;({ initialProb } = validate(binarySchema, body)) - } + console.log( + 'creating contract for', + user.username, + 'on', + question, + 'ante:', + ante || 0 + ) - if (outcomeType === 'MULTIPLE_CHOICE') { - ;({ answers } = validate(multipleChoiceSchema, body)) - } + // convert string descriptions into JSONContent + const newDescription = + !description || typeof description === 'string' + ? { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: description || ' ' }], + }, + ], + } + : description - const userDoc = await firestore.collection('users').doc(auth.uid).get() - if (!userDoc.exists) { - throw new APIError(400, 'No user exists with the authenticated user ID.') - } - const user = userDoc.data() as User + const contract = getNewContract( + contractRef.id, + slug, + user, + question, + outcomeType, + newDescription, + initialProb ?? 0, + ante, + closeTime.getTime(), + tags ?? [], + NUMERIC_BUCKET_COUNT, + min ?? 0, + max ?? 0, + isLogScale ?? false, + answers ?? [], + visibility + ) - const ante = FIXED_ANTE - const deservesFreeMarket = - (user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX - // TODO: this is broken because it's not in a transaction - if (ante > user.balance && !deservesFreeMarket) - throw new APIError(400, `Balance must be at least ${ante}.`) + const providerId = deservesFreeMarket + ? isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + : user.id - let group: Group | null = null - if (groupId) { - const groupDocRef = firestore.collection('groups').doc(groupId) - const groupDoc = await groupDocRef.get() - if (!groupDoc.exists) { - throw new APIError(400, 'No group exists with the given group ID.') + if (ante) { + const delta = FieldValue.increment(-ante) + const providerDoc = firestore.collection('users').doc(providerId) + await trans.update(providerDoc, { balance: delta, totalDeposits: delta }) } - group = groupDoc.data() as Group - const groupMembersSnap = await firestore - .collection(`groups/${groupId}/groupMembers`) - .get() - const groupMemberDocs = groupMembersSnap.docs.map( - (doc) => doc.data() as { userId: string; createdTime: number } - ) - if ( - !groupMemberDocs.map((m) => m.userId).includes(user.id) && - !group.anyoneCanJoin && - group.creatorId !== user.id - ) { - throw new APIError( - 400, - 'User must be a member/creator of the group or group must be open to add markets to it.' - ) - } - } - const slug = await getSlug(question) - const contractRef = firestore.collection('contracts').doc() - - console.log( - 'creating contract for', - user.username, - 'on', - question, - 'ante:', - ante || 0 - ) - - // convert string descriptions into JSONContent - const newDescription = - !description || typeof description === 'string' - ? { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: description || ' ' }], - }, - ], - } - : description - - const contract = getNewContract( - contractRef.id, - slug, - user, - question, - outcomeType, - newDescription, - initialProb ?? 0, - ante, - closeTime.getTime(), - tags ?? [], - NUMERIC_BUCKET_COUNT, - min ?? 0, - max ?? 0, - isLogScale ?? false, - answers ?? [], - visibility - ) - - const providerId = deservesFreeMarket - ? isProd() - ? HOUSE_LIQUIDITY_PROVIDER_ID - : DEV_HOUSE_LIQUIDITY_PROVIDER_ID - : user.id - - if (ante) await chargeUser(providerId, ante, true) - if (deservesFreeMarket) - await firestore - .collection('users') - .doc(user.id) - .update({ freeMarketsCreated: FieldValue.increment(1) }) - - await contractRef.create(contract) - - if (group != null) { - const groupContractsSnap = await firestore - .collection(`groups/${groupId}/groupContracts`) - .get() - const groupContracts = groupContractsSnap.docs.map( - (doc) => doc.data() as { contractId: string; createdTime: number } - ) - if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { - await createGroupLinks(group, [contractRef.id], auth.uid) - const groupContractRef = firestore - .collection(`groups/${groupId}/groupContracts`) - .doc(contract.id) - await groupContractRef.set({ - contractId: contract.id, - createdTime: Date.now(), + if (deservesFreeMarket) { + await trans.update(firestore.collection('users').doc(user.id), { + freeMarketsCreated: FieldValue.increment(1), }) } - } - if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { - const liquidityDoc = firestore - .collection(`contracts/${contract.id}/liquidity`) - .doc() + await contractRef.create(contract) - const lp = getCpmmInitialLiquidity( - providerId, - contract as CPMMBinaryContract, - liquidityDoc.id, - ante - ) - - await liquidityDoc.set(lp) - } else if (outcomeType === 'MULTIPLE_CHOICE') { - const betCol = firestore.collection(`contracts/${contract.id}/bets`) - const betDocs = (answers ?? []).map(() => betCol.doc()) - - const answerCol = firestore.collection(`contracts/${contract.id}/answers`) - const answerDocs = (answers ?? []).map((_, i) => - answerCol.doc(i.toString()) - ) - - const { bets, answerObjects } = getMultipleChoiceAntes( - user, - contract as MultipleChoiceContract, - answers ?? [], - betDocs.map((bd) => bd.id) - ) - - await Promise.all( - zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet)) - ) - await Promise.all( - zip(answerObjects, answerDocs).map(([answer, doc]) => - doc?.create(answer as Answer) + if (group != null) { + const groupContractsSnap = await trans.get( + firestore.collection(`groups/${groupId}/groupContracts`) + ) + const groupContracts = groupContractsSnap.docs.map( + (doc) => doc.data() as { contractId: string; createdTime: number } ) - ) - await contractRef.update({ answers: answerObjects }) - } else if (outcomeType === 'FREE_RESPONSE') { - const noneAnswerDoc = firestore - .collection(`contracts/${contract.id}/answers`) - .doc('0') - const noneAnswer = getNoneAnswer(contract.id, user) - await noneAnswerDoc.set(noneAnswer) + if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { + await createGroupLinks(trans, group, [contractRef.id], auth.uid) - const anteBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() + const groupContractRef = firestore + .collection(`groups/${groupId}/groupContracts`) + .doc(contract.id) - const anteBet = getFreeAnswerAnte( - providerId, - contract as FreeResponseContract, - anteBetDoc.id - ) - await anteBetDoc.set(anteBet) - } else if (outcomeType === 'NUMERIC') { - const anteBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() + await trans.set(groupContractRef, { + contractId: contract.id, + createdTime: Date.now(), + }) + } + } - const anteBet = getNumericAnte( - providerId, - contract as NumericContract, - ante, - anteBetDoc.id - ) + if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { + const liquidityDoc = firestore + .collection(`contracts/${contract.id}/liquidity`) + .doc() - await anteBetDoc.set(anteBet) - } + const lp = getCpmmInitialLiquidity( + providerId, + contract as CPMMBinaryContract, + liquidityDoc.id, + ante + ) - return contract + await trans.set(liquidityDoc, lp) + } else if (outcomeType === 'MULTIPLE_CHOICE') { + const betCol = firestore.collection(`contracts/${contract.id}/bets`) + const betDocs = (answers ?? []).map(() => betCol.doc()) + + const answerCol = firestore.collection(`contracts/${contract.id}/answers`) + const answerDocs = (answers ?? []).map((_, i) => + answerCol.doc(i.toString()) + ) + + const { bets, answerObjects } = getMultipleChoiceAntes( + user, + contract as MultipleChoiceContract, + answers ?? [], + betDocs.map((bd) => bd.id) + ) + + await Promise.all( + zip(bets, betDocs).map(([bet, doc]) => + doc ? trans.create(doc, bet as Bet) : undefined + ) + ) + await Promise.all( + zip(answerObjects, answerDocs).map(([answer, doc]) => + doc ? trans.create(doc, answer as Answer) : undefined + ) + ) + await trans.update(contractRef, { answers: answerObjects }) + } else if (outcomeType === 'FREE_RESPONSE') { + const noneAnswerDoc = firestore + .collection(`contracts/${contract.id}/answers`) + .doc('0') + + const noneAnswer = getNoneAnswer(contract.id, user) + await trans.set(noneAnswerDoc, noneAnswer) + + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + + const anteBet = getFreeAnswerAnte( + providerId, + contract as FreeResponseContract, + anteBetDoc.id + ) + await trans.set(anteBetDoc, anteBet) + } else if (outcomeType === 'NUMERIC') { + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() + + const anteBet = getNumericAnte( + providerId, + contract as NumericContract, + ante, + anteBetDoc.id + ) + + await trans.set(anteBetDoc, anteBet) + } + + return contract + }) } -const getSlug = async (question: string) => { +const getSlug = async (trans: Transaction, question: string) => { const proposedSlug = slugify(question) - const preexistingContract = await getContractFromSlug(proposedSlug) + const preexistingContract = await getContractFromSlug(trans, proposedSlug) return preexistingContract ? proposedSlug + '-' + randomString() @@ -338,46 +351,42 @@ const getSlug = async (question: string) => { const firestore = admin.firestore() -export async function getContractFromSlug(slug: string) { - const snap = await firestore - .collection('contracts') - .where('slug', '==', slug) - .get() +async function getContractFromSlug(trans: Transaction, slug: string) { + const snap = await trans.get( + firestore.collection('contracts').where('slug', '==', slug) + ) return snap.empty ? undefined : (snap.docs[0].data() as Contract) } async function createGroupLinks( + trans: Transaction, group: Group, contractIds: string[], userId: string ) { for (const contractId of contractIds) { - const contract = await getContract(contractId) + const contractRef = firestore.collection('contracts').doc(contractId) + const contract = (await trans.get(contractRef)).data() as Contract + if (!contract?.groupSlugs?.includes(group.slug)) { - await firestore - .collection('contracts') - .doc(contractId) - .update({ - groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), - }) + await trans.update(contractRef, { + groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), + }) } if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) { - await firestore - .collection('contracts') - .doc(contractId) - .update({ - groupLinks: [ - { - groupId: group.id, - name: group.name, - slug: group.slug, - userId, - createdTime: Date.now(), - } as GroupLink, - ...(contract?.groupLinks ?? []), - ], - }) + await trans.update(contractRef, { + groupLinks: [ + { + groupId: group.id, + name: group.name, + slug: group.slug, + userId, + createdTime: Date.now(), + } as GroupLink, + ...(contract?.groupLinks ?? []), + ], + }) } } } From 68075db3da63537c1b9baa3a500973c667852888 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Wed, 5 Oct 2022 22:20:51 -0500 Subject: [PATCH 24/44] card: reduce border size --- web/components/card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/card.tsx b/web/components/card.tsx index e9d7f8c5..ab0d692d 100644 --- a/web/components/card.tsx +++ b/web/components/card.tsx @@ -5,7 +5,7 @@ export function Card(props: JSX.IntrinsicElements['div']) { return ( <div className={clsx( - 'cursor-pointer rounded-lg border-2 bg-white transition-shadow hover:shadow-md focus:shadow-md', + 'cursor-pointer rounded-lg border bg-white transition-shadow hover:shadow-md focus:shadow-md', className )} {...rest} From 25ef17498a7204bacb94d9f3e59554d923a0e5ad Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 6 Oct 2022 09:26:35 -0400 Subject: [PATCH 25/44] Update groupContracts in db trigger --- common/group.ts | 1 + functions/src/on-update-contract.ts | 55 ++++++++++++++++++++++---- functions/src/scripts/update-groups.ts | 27 +++++++------ web/lib/firebase/groups.ts | 13 +----- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/common/group.ts b/common/group.ts index 8f5728d3..cb6660e8 100644 --- a/common/group.ts +++ b/common/group.ts @@ -39,3 +39,4 @@ export type GroupLink = { createdTime: number userId?: string } +export type GroupContractDoc = { contractId: string; createdTime: number } diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts index 301d6286..1e3418fa 100644 --- a/functions/src/on-update-contract.ts +++ b/functions/src/on-update-contract.ts @@ -2,6 +2,8 @@ import * as functions from 'firebase-functions' import { getUser } from './utils' import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification' import { Contract } from '../../common/contract' +import { GroupContractDoc } from '../../common/group' +import * as admin from 'firebase-admin' export const onUpdateContract = functions.firestore .document('contracts/{contractId}') @@ -9,17 +11,14 @@ export const onUpdateContract = functions.firestore const contract = change.after.data() as Contract const previousContract = change.before.data() as Contract const { eventId } = context - const { openCommentBounties, closeTime, question } = contract + const { closeTime, question } = contract - if ( - !previousContract.isResolved && - contract.isResolved && - (openCommentBounties ?? 0) > 0 - ) { + if (!previousContract.isResolved && contract.isResolved) { // No need to notify users of resolution, that's handled in resolve-market return - } - if ( + } else if (previousContract.groupSlugs !== contract.groupSlugs) { + await handleContractGroupUpdated(previousContract, contract) + } else if ( previousContract.closeTime !== closeTime || previousContract.question !== question ) { @@ -51,3 +50,43 @@ async function handleUpdatedCloseTime( contract ) } + +async function handleContractGroupUpdated( + previousContract: Contract, + contract: Contract +) { + const prevLength = previousContract.groupSlugs?.length ?? 0 + const newLength = contract.groupSlugs?.length ?? 0 + if (prevLength < newLength) { + // Contract was added to a new group + const groupId = contract.groupLinks?.find( + (link) => + !previousContract.groupLinks + ?.map((l) => l.groupId) + .includes(link.groupId) + )?.groupId + if (!groupId) throw new Error('Could not find new group id') + + await firestore + .collection(`groups/${groupId}/groupContracts`) + .doc(contract.id) + .set({ + contractId: contract.id, + createdTime: Date.now(), + } as GroupContractDoc) + } + if (prevLength > newLength) { + // Contract was removed from a group + const groupId = previousContract.groupLinks?.find( + (link) => + !contract.groupLinks?.map((l) => l.groupId).includes(link.groupId) + )?.groupId + if (!groupId) throw new Error('Could not find old group id') + + await firestore + .collection(`groups/${groupId}/groupContracts`) + .doc(contract.id) + .delete() + } +} +const firestore = admin.firestore() diff --git a/functions/src/scripts/update-groups.ts b/functions/src/scripts/update-groups.ts index fc402292..56a9f399 100644 --- a/functions/src/scripts/update-groups.ts +++ b/functions/src/scripts/update-groups.ts @@ -89,17 +89,20 @@ const getGroups = async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars async function updateTotalContractsAndMembers() { const groups = await getGroups() - for (const group of groups) { - log('updating group total contracts and members', group.slug) - const groupRef = admin.firestore().collection('groups').doc(group.id) - const totalMembers = (await groupRef.collection('groupMembers').get()).size - const totalContracts = (await groupRef.collection('groupContracts').get()) - .size - await groupRef.update({ - totalMembers, - totalContracts, + await Promise.all( + groups.map(async (group) => { + log('updating group total contracts and members', group.slug) + const groupRef = admin.firestore().collection('groups').doc(group.id) + const totalMembers = (await groupRef.collection('groupMembers').get()) + .size + const totalContracts = (await groupRef.collection('groupContracts').get()) + .size + await groupRef.update({ + totalMembers, + totalContracts, + }) }) - } + ) } // eslint-disable-next-line @typescript-eslint/no-unused-vars async function removeUnusedMemberAndContractFields() { @@ -117,6 +120,6 @@ async function removeUnusedMemberAndContractFields() { if (require.main === module) { initAdmin() // convertGroupFieldsToGroupDocuments() - // updateTotalContractsAndMembers() - removeUnusedMemberAndContractFields() + updateTotalContractsAndMembers() + // removeUnusedMemberAndContractFields() } diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 17e41c53..6bfc4e85 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -191,6 +191,7 @@ export async function leaveGroup(group: Group, userId: string): Promise<void> { return await deleteDoc(memberDoc) } +// TODO: This doesn't check if the user has permission to do this export async function addContractToGroup( group: Group, contract: Contract, @@ -211,15 +212,9 @@ export async function addContractToGroup( 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(), - }) } +// TODO: This doesn't check if the user has permission to do this export async function removeContractFromGroup( group: Group, contract: Contract @@ -234,10 +229,6 @@ export async function removeContractFromGroup( 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) { From e127f9646a08b6cf593018c738ecf9242626b075 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 6 Oct 2022 09:53:55 -0400 Subject: [PATCH 26/44] Default sort to best --- web/components/contract/contract-tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 9639a57a..9ce17396 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -80,7 +80,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { const { contract } = props const tips = useTipTxns({ contractId: contract.id }) const comments = useComments(contract.id) ?? props.comments - const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Newest', { + const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Best', { key: `contract-${contract.id}-comments-sort`, store: storageStore(safeLocalStorage()), }) From 26f04fb04a2302f907287401e5db6fb2095f46c4 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Thu, 6 Oct 2022 10:16:29 -0400 Subject: [PATCH 27/44] Save comment sort per user rather than per contract --- web/components/contract/contract-tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 9ce17396..fdfc5c3d 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -81,7 +81,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { const tips = useTipTxns({ contractId: contract.id }) const comments = useComments(contract.id) ?? props.comments const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Best', { - key: `contract-${contract.id}-comments-sort`, + key: `contract-comments-sort`, store: storageStore(safeLocalStorage()), }) const me = useUser() From b8d65acc3f75cc29af12bb56176a64bcb68acbdc Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 10:54:42 -0500 Subject: [PATCH 28/44] Revert "create market: use transaction" This reverts commit e1f24f24a96fa8d811ebcaa3b10b19d9b67cb282. --- functions/src/create-market.ts | 457 ++++++++++++++++----------------- 1 file changed, 224 insertions(+), 233 deletions(-) diff --git a/functions/src/create-market.ts b/functions/src/create-market.ts index 38852184..d1483ca4 100644 --- a/functions/src/create-market.ts +++ b/functions/src/create-market.ts @@ -15,7 +15,7 @@ import { import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' -import { isProd } from './utils' +import { chargeUser, getContract, isProd } from './utils' import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api' import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy' @@ -36,7 +36,7 @@ import { getPseudoProbability } from '../../common/pseudo-numeric' import { JSONContent } from '@tiptap/core' import { uniq, zip } from 'lodash' import { Bet } from '../../common/bet' -import { FieldValue, Transaction } from 'firebase-admin/firestore' +import { FieldValue } from 'firebase-admin/firestore' const descScehma: z.ZodType<JSONContent> = z.lazy(() => z.intersection( @@ -107,242 +107,229 @@ export async function createMarketHelper(body: any, auth: AuthedUser) { visibility = 'public', } = validate(bodySchema, body) - return await firestore.runTransaction(async (trans) => { - let min, max, initialProb, isLogScale, answers + let min, max, initialProb, isLogScale, answers - if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { - let initialValue - ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) - if (max - min <= 0.01 || initialValue <= min || initialValue >= max) - throw new APIError(400, 'Invalid range.') + if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { + let initialValue + ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) + if (max - min <= 0.01 || initialValue <= min || initialValue >= max) + throw new APIError(400, 'Invalid range.') - initialProb = - getPseudoProbability(initialValue, min, max, isLogScale) * 100 + initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100 - if (initialProb < 1 || initialProb > 99) - if (outcomeType === 'PSEUDO_NUMERIC') - throw new APIError( - 400, - `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` - ) - else throw new APIError(400, 'Invalid initial probability.') - } - - if (outcomeType === 'BINARY') { - ;({ initialProb } = validate(binarySchema, body)) - } - - if (outcomeType === 'MULTIPLE_CHOICE') { - ;({ answers } = validate(multipleChoiceSchema, body)) - } - - const userDoc = await trans.get(firestore.collection('users').doc(auth.uid)) - if (!userDoc.exists) { - throw new APIError(400, 'No user exists with the authenticated user ID.') - } - const user = userDoc.data() as User - - const ante = FIXED_ANTE - const deservesFreeMarket = - (user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX - // TODO: this is broken because it's not in a transaction - if (ante > user.balance && !deservesFreeMarket) - throw new APIError(400, `Balance must be at least ${ante}.`) - - let group: Group | null = null - if (groupId) { - const groupDocRef = firestore.collection('groups').doc(groupId) - const groupDoc = await trans.get(groupDocRef) - if (!groupDoc.exists) { - throw new APIError(400, 'No group exists with the given group ID.') - } - - group = groupDoc.data() as Group - const groupMembersSnap = await trans.get( - firestore.collection(`groups/${groupId}/groupMembers`) - ) - const groupMemberDocs = groupMembersSnap.docs.map( - (doc) => doc.data() as { userId: string; createdTime: number } - ) - if ( - !groupMemberDocs.map((m) => m.userId).includes(user.id) && - !group.anyoneCanJoin && - group.creatorId !== user.id - ) { + if (initialProb < 1 || initialProb > 99) + if (outcomeType === 'PSEUDO_NUMERIC') throw new APIError( 400, - 'User must be a member/creator of the group or group must be open to add markets to it.' + `Initial value is too ${initialProb < 1 ? 'low' : 'high'}` ) - } - } - const slug = await getSlug(trans, question) - const contractRef = firestore.collection('contracts').doc() + else throw new APIError(400, 'Invalid initial probability.') + } - console.log( - 'creating contract for', - user.username, - 'on', - question, - 'ante:', - ante || 0 - ) + if (outcomeType === 'BINARY') { + ;({ initialProb } = validate(binarySchema, body)) + } - // convert string descriptions into JSONContent - const newDescription = - !description || typeof description === 'string' - ? { - type: 'doc', - content: [ - { - type: 'paragraph', - content: [{ type: 'text', text: description || ' ' }], - }, - ], - } - : description + if (outcomeType === 'MULTIPLE_CHOICE') { + ;({ answers } = validate(multipleChoiceSchema, body)) + } - const contract = getNewContract( - contractRef.id, - slug, - user, - question, - outcomeType, - newDescription, - initialProb ?? 0, - ante, - closeTime.getTime(), - tags ?? [], - NUMERIC_BUCKET_COUNT, - min ?? 0, - max ?? 0, - isLogScale ?? false, - answers ?? [], - visibility - ) + const userDoc = await firestore.collection('users').doc(auth.uid).get() + if (!userDoc.exists) { + throw new APIError(400, 'No user exists with the authenticated user ID.') + } + const user = userDoc.data() as User - const providerId = deservesFreeMarket - ? isProd() - ? HOUSE_LIQUIDITY_PROVIDER_ID - : DEV_HOUSE_LIQUIDITY_PROVIDER_ID - : user.id + const ante = FIXED_ANTE + const deservesFreeMarket = + (user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX + // TODO: this is broken because it's not in a transaction + if (ante > user.balance && !deservesFreeMarket) + throw new APIError(400, `Balance must be at least ${ante}.`) - if (ante) { - const delta = FieldValue.increment(-ante) - const providerDoc = firestore.collection('users').doc(providerId) - await trans.update(providerDoc, { balance: delta, totalDeposits: delta }) + let group: Group | null = null + if (groupId) { + const groupDocRef = firestore.collection('groups').doc(groupId) + const groupDoc = await groupDocRef.get() + if (!groupDoc.exists) { + throw new APIError(400, 'No group exists with the given group ID.') } - if (deservesFreeMarket) { - await trans.update(firestore.collection('users').doc(user.id), { - freeMarketsCreated: FieldValue.increment(1), + group = groupDoc.data() as Group + const groupMembersSnap = await firestore + .collection(`groups/${groupId}/groupMembers`) + .get() + const groupMemberDocs = groupMembersSnap.docs.map( + (doc) => doc.data() as { userId: string; createdTime: number } + ) + if ( + !groupMemberDocs.map((m) => m.userId).includes(user.id) && + !group.anyoneCanJoin && + group.creatorId !== user.id + ) { + throw new APIError( + 400, + 'User must be a member/creator of the group or group must be open to add markets to it.' + ) + } + } + const slug = await getSlug(question) + const contractRef = firestore.collection('contracts').doc() + + console.log( + 'creating contract for', + user.username, + 'on', + question, + 'ante:', + ante || 0 + ) + + // convert string descriptions into JSONContent + const newDescription = + !description || typeof description === 'string' + ? { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: description || ' ' }], + }, + ], + } + : description + + const contract = getNewContract( + contractRef.id, + slug, + user, + question, + outcomeType, + newDescription, + initialProb ?? 0, + ante, + closeTime.getTime(), + tags ?? [], + NUMERIC_BUCKET_COUNT, + min ?? 0, + max ?? 0, + isLogScale ?? false, + answers ?? [], + visibility + ) + + const providerId = deservesFreeMarket + ? isProd() + ? HOUSE_LIQUIDITY_PROVIDER_ID + : DEV_HOUSE_LIQUIDITY_PROVIDER_ID + : user.id + + if (ante) await chargeUser(providerId, ante, true) + if (deservesFreeMarket) + await firestore + .collection('users') + .doc(user.id) + .update({ freeMarketsCreated: FieldValue.increment(1) }) + + await contractRef.create(contract) + + if (group != null) { + const groupContractsSnap = await firestore + .collection(`groups/${groupId}/groupContracts`) + .get() + const groupContracts = groupContractsSnap.docs.map( + (doc) => doc.data() as { contractId: string; createdTime: number } + ) + if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { + await createGroupLinks(group, [contractRef.id], auth.uid) + const groupContractRef = firestore + .collection(`groups/${groupId}/groupContracts`) + .doc(contract.id) + await groupContractRef.set({ + contractId: contract.id, + createdTime: Date.now(), }) } + } - await contractRef.create(contract) + if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { + const liquidityDoc = firestore + .collection(`contracts/${contract.id}/liquidity`) + .doc() - if (group != null) { - const groupContractsSnap = await trans.get( - firestore.collection(`groups/${groupId}/groupContracts`) - ) - const groupContracts = groupContractsSnap.docs.map( - (doc) => doc.data() as { contractId: string; createdTime: number } + const lp = getCpmmInitialLiquidity( + providerId, + contract as CPMMBinaryContract, + liquidityDoc.id, + ante + ) + + await liquidityDoc.set(lp) + } else if (outcomeType === 'MULTIPLE_CHOICE') { + const betCol = firestore.collection(`contracts/${contract.id}/bets`) + const betDocs = (answers ?? []).map(() => betCol.doc()) + + const answerCol = firestore.collection(`contracts/${contract.id}/answers`) + const answerDocs = (answers ?? []).map((_, i) => + answerCol.doc(i.toString()) + ) + + const { bets, answerObjects } = getMultipleChoiceAntes( + user, + contract as MultipleChoiceContract, + answers ?? [], + betDocs.map((bd) => bd.id) + ) + + await Promise.all( + zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet)) + ) + await Promise.all( + zip(answerObjects, answerDocs).map(([answer, doc]) => + doc?.create(answer as Answer) ) + ) + await contractRef.update({ answers: answerObjects }) + } else if (outcomeType === 'FREE_RESPONSE') { + const noneAnswerDoc = firestore + .collection(`contracts/${contract.id}/answers`) + .doc('0') - if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) { - await createGroupLinks(trans, group, [contractRef.id], auth.uid) + const noneAnswer = getNoneAnswer(contract.id, user) + await noneAnswerDoc.set(noneAnswer) - const groupContractRef = firestore - .collection(`groups/${groupId}/groupContracts`) - .doc(contract.id) + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() - await trans.set(groupContractRef, { - contractId: contract.id, - createdTime: Date.now(), - }) - } - } + const anteBet = getFreeAnswerAnte( + providerId, + contract as FreeResponseContract, + anteBetDoc.id + ) + await anteBetDoc.set(anteBet) + } else if (outcomeType === 'NUMERIC') { + const anteBetDoc = firestore + .collection(`contracts/${contract.id}/bets`) + .doc() - if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') { - const liquidityDoc = firestore - .collection(`contracts/${contract.id}/liquidity`) - .doc() + const anteBet = getNumericAnte( + providerId, + contract as NumericContract, + ante, + anteBetDoc.id + ) - const lp = getCpmmInitialLiquidity( - providerId, - contract as CPMMBinaryContract, - liquidityDoc.id, - ante - ) + await anteBetDoc.set(anteBet) + } - await trans.set(liquidityDoc, lp) - } else if (outcomeType === 'MULTIPLE_CHOICE') { - const betCol = firestore.collection(`contracts/${contract.id}/bets`) - const betDocs = (answers ?? []).map(() => betCol.doc()) - - const answerCol = firestore.collection(`contracts/${contract.id}/answers`) - const answerDocs = (answers ?? []).map((_, i) => - answerCol.doc(i.toString()) - ) - - const { bets, answerObjects } = getMultipleChoiceAntes( - user, - contract as MultipleChoiceContract, - answers ?? [], - betDocs.map((bd) => bd.id) - ) - - await Promise.all( - zip(bets, betDocs).map(([bet, doc]) => - doc ? trans.create(doc, bet as Bet) : undefined - ) - ) - await Promise.all( - zip(answerObjects, answerDocs).map(([answer, doc]) => - doc ? trans.create(doc, answer as Answer) : undefined - ) - ) - await trans.update(contractRef, { answers: answerObjects }) - } else if (outcomeType === 'FREE_RESPONSE') { - const noneAnswerDoc = firestore - .collection(`contracts/${contract.id}/answers`) - .doc('0') - - const noneAnswer = getNoneAnswer(contract.id, user) - await trans.set(noneAnswerDoc, noneAnswer) - - const anteBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - - const anteBet = getFreeAnswerAnte( - providerId, - contract as FreeResponseContract, - anteBetDoc.id - ) - await trans.set(anteBetDoc, anteBet) - } else if (outcomeType === 'NUMERIC') { - const anteBetDoc = firestore - .collection(`contracts/${contract.id}/bets`) - .doc() - - const anteBet = getNumericAnte( - providerId, - contract as NumericContract, - ante, - anteBetDoc.id - ) - - await trans.set(anteBetDoc, anteBet) - } - - return contract - }) + return contract } -const getSlug = async (trans: Transaction, question: string) => { +const getSlug = async (question: string) => { const proposedSlug = slugify(question) - const preexistingContract = await getContractFromSlug(trans, proposedSlug) + const preexistingContract = await getContractFromSlug(proposedSlug) return preexistingContract ? proposedSlug + '-' + randomString() @@ -351,42 +338,46 @@ const getSlug = async (trans: Transaction, question: string) => { const firestore = admin.firestore() -async function getContractFromSlug(trans: Transaction, slug: string) { - const snap = await trans.get( - firestore.collection('contracts').where('slug', '==', slug) - ) +export async function getContractFromSlug(slug: string) { + const snap = await firestore + .collection('contracts') + .where('slug', '==', slug) + .get() return snap.empty ? undefined : (snap.docs[0].data() as Contract) } async function createGroupLinks( - trans: Transaction, group: Group, contractIds: string[], userId: string ) { for (const contractId of contractIds) { - const contractRef = firestore.collection('contracts').doc(contractId) - const contract = (await trans.get(contractRef)).data() as Contract - + const contract = await getContract(contractId) if (!contract?.groupSlugs?.includes(group.slug)) { - await trans.update(contractRef, { - groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), - }) + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]), + }) } if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) { - await trans.update(contractRef, { - groupLinks: [ - { - groupId: group.id, - name: group.name, - slug: group.slug, - userId, - createdTime: Date.now(), - } as GroupLink, - ...(contract?.groupLinks ?? []), - ], - }) + await firestore + .collection('contracts') + .doc(contractId) + .update({ + groupLinks: [ + { + groupId: group.id, + name: group.name, + slug: group.slug, + userId, + createdTime: Date.now(), + } as GroupLink, + ...(contract?.groupLinks ?? []), + ], + }) } } } From 59de9799498a5710fc8d79ae18f8edd224d67af4 Mon Sep 17 00:00:00 2001 From: Pico2x <pico2x@gmail.com> Date: Thu, 6 Oct 2022 17:04:00 +0100 Subject: [PATCH 29/44] Refactor Pinned Items into a reusable component --- web/components/groups/group-overview.tsx | 203 +++++++++++++---------- web/components/pinned-select-modal.tsx | 8 +- 2 files changed, 118 insertions(+), 93 deletions(-) diff --git a/web/components/groups/group-overview.tsx b/web/components/groups/group-overview.tsx index 080453ca..d5cdaafa 100644 --- a/web/components/groups/group-overview.tsx +++ b/web/components/groups/group-overview.tsx @@ -145,8 +145,6 @@ function GroupOverviewPinned(props: { }) { const { group, posts, isEditable } = props const [pinned, setPinned] = useState<JSX.Element[]>([]) - const [open, setOpen] = useState(false) - const [editMode, setEditMode] = useState(false) useEffect(() => { async function getPinned() { @@ -185,100 +183,127 @@ function GroupOverviewPinned(props: { ...(selectedItems as { itemId: string; type: 'contract' | 'post' }[]), ], }) - setOpen(false) + } + + function onDeleteClicked(index: number) { + const newPinned = group.pinnedItems.filter((item) => { + return item.itemId !== group.pinnedItems[index].itemId + }) + updateGroup(group, { pinnedItems: newPinned }) } return isEditable || (group.pinnedItems && group.pinnedItems.length > 0) ? ( - pinned.length > 0 || isEditable ? ( - <div> - <Row className="mb-3 items-center justify-between"> - <SectionHeader label={'Pinned'} /> - {isEditable && ( - <Button - color="gray" - size="xs" - onClick={() => { - setEditMode(!editMode) - }} - > - {editMode ? ( - 'Done' - ) : ( - <> - <PencilIcon className="inline h-4 w-4" /> - Edit - </> - )} - </Button> - )} - </Row> - <div> - <Masonry - breakpointCols={{ default: 2, 768: 1 }} - className="-ml-4 flex w-auto" - columnClassName="pl-4 bg-clip-padding" - > - {pinned.length == 0 && !editMode && ( - <div className="flex flex-col items-center justify-center"> - <p className="text-center text-gray-400"> - No pinned items yet. Click the edit button to add some! - </p> - </div> - )} - {pinned.map((element, index) => ( - <div className="relative my-2"> - {element} + <PinnedItems + posts={posts} + group={group} + isEditable={isEditable} + pinned={pinned} + onDeleteClicked={onDeleteClicked} + onSubmit={onSubmit} + modalMessage={'Pin posts or markets to the overview of this group.'} + /> + ) : ( + <LoadingIndicator /> + ) +} - {editMode && ( - <CrossIcon - onClick={() => { - const newPinned = group.pinnedItems.filter((item) => { - return item.itemId !== group.pinnedItems[index].itemId - }) - updateGroup(group, { pinnedItems: newPinned }) - }} - /> - )} - </div> - ))} - {editMode && group.pinnedItems && pinned.length < 6 && ( - <div className=" py-2"> - <Row - className={ - 'relative gap-3 rounded-lg border-4 border-dotted p-2 hover:cursor-pointer hover:bg-gray-100' - } - > - <button - className="flex w-full justify-center" - onClick={() => setOpen(true)} - > - <PlusCircleIcon - className="h-12 w-12 text-gray-600" - aria-hidden="true" - /> - </button> - </Row> - </div> +export function PinnedItems(props: { + posts: Post[] + isEditable: boolean + pinned: JSX.Element[] + onDeleteClicked: (index: number) => void + onSubmit: (selectedItems: { itemId: string; type: string }[]) => void + group?: Group + modalMessage: string +}) { + const { + isEditable, + pinned, + onDeleteClicked, + onSubmit, + posts, + group, + modalMessage, + } = props + const [editMode, setEditMode] = useState(false) + const [open, setOpen] = useState(false) + + return pinned.length > 0 || isEditable ? ( + <div> + <Row className="mb-3 items-center justify-between"> + <SectionHeader label={'Pinned'} /> + {isEditable && ( + <Button + color="gray" + size="xs" + onClick={() => { + setEditMode(!editMode) + }} + > + {editMode ? ( + 'Done' + ) : ( + <> + <PencilIcon className="inline h-4 w-4" /> + Edit + </> )} - </Masonry> - </div> - <PinnedSelectModal - open={open} - group={group} - posts={posts} - setOpen={setOpen} - title="Pin a post or market" - description={ - <div className={'text-md my-4 text-gray-600'}> - Pin posts or markets to the overview of this group. + </Button> + )} + </Row> + <div> + <Masonry + breakpointCols={{ default: 2, 768: 1 }} + className="-ml-4 flex w-auto" + columnClassName="pl-4 bg-clip-padding" + > + {pinned.length == 0 && !editMode && ( + <div className="flex flex-col items-center justify-center"> + <p className="text-center text-gray-400"> + No pinned items yet. Click the edit button to add some! + </p> </div> - } - onSubmit={onSubmit} - /> + )} + {pinned.map((element, index) => ( + <div className="relative my-2"> + {element} + + {editMode && <CrossIcon onClick={() => onDeleteClicked(index)} />} + </div> + ))} + {editMode && pinned.length < 6 && ( + <div className=" py-2"> + <Row + className={ + 'relative gap-3 rounded-lg border-4 border-dotted p-2 hover:cursor-pointer hover:bg-gray-100' + } + > + <button + className="flex w-full justify-center" + onClick={() => setOpen(true)} + > + <PlusCircleIcon + className="h-12 w-12 text-gray-600" + aria-hidden="true" + /> + </button> + </Row> + </div> + )} + </Masonry> </div> - ) : ( - <LoadingIndicator /> - ) + <PinnedSelectModal + open={open} + group={group} + posts={posts} + setOpen={setOpen} + title="Pin a post or market" + description={ + <div className={'text-md my-4 text-gray-600'}>{modalMessage}</div> + } + onSubmit={onSubmit} + /> + </div> ) : ( <></> ) diff --git a/web/components/pinned-select-modal.tsx b/web/components/pinned-select-modal.tsx index e72deee2..c43c7534 100644 --- a/web/components/pinned-select-modal.tsx +++ b/web/components/pinned-select-modal.tsx @@ -20,8 +20,8 @@ export function PinnedSelectModal(props: { selectedItems: { itemId: string; type: string }[] ) => void | Promise<void> contractSearchOptions?: Partial<Parameters<typeof ContractSearch>[0]> - group: Group posts: Post[] + group?: Group }) { const { title, @@ -134,8 +134,8 @@ export function PinnedSelectModal(props: { highlightClassName: '!bg-indigo-100 outline outline-2 outline-indigo-300', }} - additionalFilter={{ groupSlug: group.slug }} - persistPrefix={`group-${group.slug}`} + additionalFilter={group ? { groupSlug: group.slug } : undefined} + persistPrefix={group ? `group-${group.slug}` : undefined} headerClassName="bg-white sticky" {...contractSearchOptions} /> @@ -152,7 +152,7 @@ export function PinnedSelectModal(props: { '!bg-indigo-100 outline outline-2 outline-indigo-300', }} /> - {posts.length === 0 && ( + {posts.length == 0 && ( <div className="text-center text-gray-500">No posts yet</div> )} </div> From 853e3e48967ac83173269edf698f45ba91e72fdd Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 6 Oct 2022 14:20:35 -0400 Subject: [PATCH 30/44] Mark @v with a (Bot) label --- web/components/user-link.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/components/user-link.tsx b/web/components/user-link.tsx index 4b05ccd0..c3a273fc 100644 --- a/web/components/user-link.tsx +++ b/web/components/user-link.tsx @@ -34,6 +34,17 @@ export function UserLink(props: { )} > {shortName} + {BOT_USERNAMES.includes(username) && <BotBadge />} </SiteLink> ) } + +const BOT_USERNAMES = ['v'] + +function BotBadge() { + return ( + <span className="ml-1.5 inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800"> + Bot + </span> + ) +} From 2f2c586d5de08b87b2857b04acfc34bc78fcabdb Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 6 Oct 2022 12:01:00 -0700 Subject: [PATCH 31/44] fix padding on daily movers --- web/components/contract/prob-change-table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/contract/prob-change-table.tsx b/web/components/contract/prob-change-table.tsx index 70eaf18c..c5eb9e55 100644 --- a/web/components/contract/prob-change-table.tsx +++ b/web/components/contract/prob-change-table.tsx @@ -37,12 +37,12 @@ export function ProbChangeTable(props: { return ( <Col className="mb-4 w-full gap-4 rounded-lg md:flex-row"> - <Col className="flex-1 gap-4"> + <Col className="flex-1"> {filteredPositiveChanges.map((contract) => ( <ContractCardProbChange key={contract.id} contract={contract} /> ))} </Col> - <Col className="flex-1 gap-4"> + <Col className="flex-1"> {filteredNegativeChanges.map((contract) => ( <ContractCardProbChange key={contract.id} contract={contract} /> ))} From 91da39370f08d27ec957285f5afea6fe8b697386 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 14:54:22 -0500 Subject: [PATCH 32/44] fix type errors --- functions/src/scripts/denormalize.ts | 3 +-- functions/src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/functions/src/scripts/denormalize.ts b/functions/src/scripts/denormalize.ts index d4feb425..3362e940 100644 --- a/functions/src/scripts/denormalize.ts +++ b/functions/src/scripts/denormalize.ts @@ -3,7 +3,6 @@ import { DocumentSnapshot, Transaction } from 'firebase-admin/firestore' import { isEqual, zip } from 'lodash' -import { UpdateSpec } from '../utils' export type DocumentValue = { doc: DocumentSnapshot @@ -54,7 +53,7 @@ export function getDiffUpdate(diff: DocumentDiff) { return { doc: diff.dest.doc.ref, fields: Object.fromEntries(zip(diff.dest.fields, diff.src.vals)), - } as UpdateSpec + } } export function applyDiff(transaction: Transaction, diff: DocumentDiff) { diff --git a/functions/src/utils.ts b/functions/src/utils.ts index efc22e53..91f4b293 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -47,7 +47,7 @@ export const writeAsync = async ( const batch = db.batch() for (const { doc, fields } of chunks[i]) { if (operationType === 'update') { - batch.update(doc, fields) + batch.update(doc, fields as any) } else { batch.set(doc, fields) } From 4162cca3ff4bceff8f93a9eca3e35b9679864d97 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 15:23:51 -0500 Subject: [PATCH 33/44] Wrap sprig init in check for window --- web/lib/service/sprig.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/lib/service/sprig.ts b/web/lib/service/sprig.ts index f89a9678..7a478d77 100644 --- a/web/lib/service/sprig.ts +++ b/web/lib/service/sprig.ts @@ -5,7 +5,7 @@ import { ENV_CONFIG } from 'common/envs/constants' import { PROD_CONFIG } from 'common/envs/prod' -if (ENV_CONFIG.domain === PROD_CONFIG.domain) { +if (ENV_CONFIG.domain === PROD_CONFIG.domain && typeof window !== 'undefined') { try { ;(function (l, e, a, p) { if (window.Sprig) return @@ -20,7 +20,8 @@ if (ENV_CONFIG.domain === PROD_CONFIG.domain) { a.async = 1 a.src = e + '?id=' + S.appId p = l.getElementsByTagName('script')[0] - p.parentNode.insertBefore(a, p) + ENV_CONFIG.domain === PROD_CONFIG.domain && + p.parentNode.insertBefore(a, p) })(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId) } catch (error) { console.log('Error initializing Sprig, please complain to Barak', error) From bc5af50b0ccd92dea103a74144b087d9006dc01b Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 6 Oct 2022 13:49:39 -0700 Subject: [PATCH 34/44] unindex date-docs from search engines --- web/components/NoSEO.tsx | 10 ++++++++++ web/pages/date-docs/[username].tsx | 2 ++ web/pages/date-docs/create.tsx | 2 ++ web/pages/date-docs/index.tsx | 2 ++ 4 files changed, 16 insertions(+) create mode 100644 web/components/NoSEO.tsx diff --git a/web/components/NoSEO.tsx b/web/components/NoSEO.tsx new file mode 100644 index 00000000..f72907c8 --- /dev/null +++ b/web/components/NoSEO.tsx @@ -0,0 +1,10 @@ +import Head from "next/head"; + +/** Exclude page from search results */ +export function NoSEO() { + return ( + <Head> + <meta name="robots" content="noindex,follow"/> + </Head> + ) +} \ No newline at end of file diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx index 350e79b7..dd7d5d73 100644 --- a/web/pages/date-docs/[username].tsx +++ b/web/pages/date-docs/[username].tsx @@ -22,6 +22,7 @@ import { PostCommentsActivity, RichEditPost } from '../post/[...slugs]' import { usePost } from 'web/hooks/use-post' import { useTipTxns } from 'web/hooks/use-tip-txns' import { useCommentsOnPost } from 'web/hooks/use-comments' +import { NoSEO } from 'web/components/NoSEO' export async function getStaticProps(props: { params: { username: string } }) { const { username } = props.params @@ -62,6 +63,7 @@ function DateDocPage(props: { creator: User; post: DateDoc }) { return ( <Page> + <NoSEO /> <Col className="mx-auto w-full max-w-xl gap-6 sm:mb-6"> <SiteLink href="/date-docs"> <Row className="items-center gap-2"> diff --git a/web/pages/date-docs/create.tsx b/web/pages/date-docs/create.tsx index 08442cc1..ed1df677 100644 --- a/web/pages/date-docs/create.tsx +++ b/web/pages/date-docs/create.tsx @@ -14,6 +14,7 @@ import dayjs from 'dayjs' import { MINUTE_MS } from 'common/util/time' import { Col } from 'web/components/layout/col' import { MAX_QUESTION_LENGTH } from 'common/contract' +import { NoSEO } from 'web/components/NoSEO' export default function CreateDateDocPage() { const user = useUser() @@ -64,6 +65,7 @@ export default function CreateDateDocPage() { return ( <Page> + <NoSEO /> <div className="mx-auto w-full max-w-3xl"> <div className="rounded-lg px-6 py-4 pb-4 sm:py-0"> <Row className="mb-8 items-center justify-between"> diff --git a/web/pages/date-docs/index.tsx b/web/pages/date-docs/index.tsx index 9ddeb57f..48e0bb13 100644 --- a/web/pages/date-docs/index.tsx +++ b/web/pages/date-docs/index.tsx @@ -12,6 +12,7 @@ import { Button } from 'web/components/button' import { SiteLink } from 'web/components/site-link' import { getUser, User } from 'web/lib/firebase/users' import { DateDocPost } from './[username]' +import { NoSEO } from 'web/components/NoSEO' export async function getStaticProps() { const dateDocs = await getDateDocs() @@ -40,6 +41,7 @@ export default function DatePage(props: { return ( <Page> + <NoSEO /> <div className="mx-auto w-full max-w-xl"> <Row className="items-center justify-between p-4 sm:p-0"> <Title className="!my-0 px-2 text-blue-500" text="Date docs" /> From ac37f94cf78f7985d71c0ef1790d8bbc77e36bd7 Mon Sep 17 00:00:00 2001 From: sipec <sipec@users.noreply.github.com> Date: Thu, 6 Oct 2022 20:50:29 +0000 Subject: [PATCH 35/44] Auto-prettification --- web/components/NoSEO.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/components/NoSEO.tsx b/web/components/NoSEO.tsx index f72907c8..53437b10 100644 --- a/web/components/NoSEO.tsx +++ b/web/components/NoSEO.tsx @@ -1,10 +1,10 @@ -import Head from "next/head"; +import Head from 'next/head' /** Exclude page from search results */ export function NoSEO() { return ( <Head> - <meta name="robots" content="noindex,follow"/> + <meta name="robots" content="noindex,follow" /> </Head> ) -} \ No newline at end of file +} From 7ca0fb72fcb70389884d93ea543f45a7ea2c7546 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 16:36:16 -0500 Subject: [PATCH 36/44] compute elasticity --- common/calculate-metrics.ts | 59 +++++++++++++++++++++++++++++++-- common/contract.ts | 1 + common/new-bet.ts | 5 ++- common/new-contract.ts | 1 + functions/src/update-metrics.ts | 2 ++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 7c2153c1..524dfadd 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -1,9 +1,15 @@ import { last, sortBy, sum, sumBy } from 'lodash' import { calculatePayout } from './calculate' -import { Bet } from './bet' -import { Contract } from './contract' +import { Bet, LimitBet } from './bet' +import { + Contract, + CPMMContract, + DPMContract, +} from './contract' import { PortfolioMetrics, User } from './user' import { DAY_MS } from './util/time' +import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet' +import { getCpmmProbability } from './calculate-cpmm' const computeInvestmentValue = ( bets: Bet[], @@ -40,6 +46,55 @@ export const computeInvestmentValueCustomProb = ( }) } +export const computeElasticity = ( + bets: Bet[], + contract: Contract, + betAmount = 50 +) => { + const { mechanism, outcomeType } = contract + return mechanism === 'cpmm-1' && + (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') + ? computeBinaryCpmmElasticity(bets, contract, betAmount) + : computeDpmElasticity(contract, betAmount) +} + +export const computeBinaryCpmmElasticity = ( + bets: Bet[], + contract: CPMMContract, + betAmount = 50 +) => { + const limitBets = bets + .filter( + (b) => + !b.isFilled && !b.isSold && !b.isRedemption && !b.sale && !b.isCancelled + ) + .sort((a, b) => a.createdTime - b.createdTime) + + const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo( + 'YES', + betAmount, + contract, + undefined, + limitBets as LimitBet[] + ) + const resultYes = getCpmmProbability(poolY, pY) + + const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo( + 'NO', + betAmount, + contract, + undefined, + limitBets as LimitBet[] + ) + const resultNo = getCpmmProbability(poolN, pN) + + return resultYes - resultNo +} + +export const computeDpmElasticity = (contract: DPMContract, betAmount = 50) => { + return getNewMultiBetInfo('', betAmount, contract).newBet.probAfter +} + const computeTotalPool = (userContracts: Contract[], startTime = 0) => { const periodFilteredContracts = userContracts.filter( (contract) => contract.createdTime >= startTime diff --git a/common/contract.ts b/common/contract.ts index 1255874d..2656b5d5 100644 --- a/common/contract.ts +++ b/common/contract.ts @@ -49,6 +49,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = { volume: number volume24Hours: number volume7Days: number + elasticity: number collectedFees: Fees diff --git a/common/new-bet.ts b/common/new-bet.ts index 91faf640..e9f5c554 100644 --- a/common/new-bet.ts +++ b/common/new-bet.ts @@ -17,8 +17,7 @@ import { import { CPMMBinaryContract, DPMBinaryContract, - FreeResponseContract, - MultipleChoiceContract, + DPMContract, NumericContract, PseudoNumericContract, } from './contract' @@ -325,7 +324,7 @@ export const getNewBinaryDpmBetInfo = ( export const getNewMultiBetInfo = ( outcome: string, amount: number, - contract: FreeResponseContract | MultipleChoiceContract + contract: DPMContract ) => { const { pool, totalShares, totalBets } = contract diff --git a/common/new-contract.ts b/common/new-contract.ts index 3580b164..8ab44d2e 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -70,6 +70,7 @@ export function getNewContract( volume: 0, volume24Hours: 0, volume7Days: 0, + elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.56, collectedFees: { creatorFee: 0, diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts index 70c7c742..24dc07e7 100644 --- a/functions/src/update-metrics.ts +++ b/functions/src/update-metrics.ts @@ -14,6 +14,7 @@ import { calculateNewPortfolioMetrics, calculateNewProfit, calculateProbChanges, + computeElasticity, computeVolume, } from '../../common/calculate-metrics' import { getProbability } from '../../common/calculate' @@ -103,6 +104,7 @@ export async function updateMetricsCore() { fields: { volume24Hours: computeVolume(contractBets, now - DAY_MS), volume7Days: computeVolume(contractBets, now - DAY_MS * 7), + elasticity: computeElasticity(contractBets, contract), ...cpmmFields, }, } From a63405ca7c4d43a8f317a0d7028f7c4a5cd7be86 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 16:47:52 -0500 Subject: [PATCH 37/44] change dpm elasticity --- common/calculate-metrics.ts | 15 +++++++-------- common/new-contract.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 524dfadd..bf588345 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -1,11 +1,7 @@ import { last, sortBy, sum, sumBy } from 'lodash' import { calculatePayout } from './calculate' import { Bet, LimitBet } from './bet' -import { - Contract, - CPMMContract, - DPMContract, -} from './contract' +import { Contract, CPMMContract, DPMContract } from './contract' import { PortfolioMetrics, User } from './user' import { DAY_MS } from './util/time' import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet' @@ -61,7 +57,7 @@ export const computeElasticity = ( export const computeBinaryCpmmElasticity = ( bets: Bet[], contract: CPMMContract, - betAmount = 50 + betAmount: number ) => { const limitBets = bets .filter( @@ -91,8 +87,11 @@ export const computeBinaryCpmmElasticity = ( return resultYes - resultNo } -export const computeDpmElasticity = (contract: DPMContract, betAmount = 50) => { - return getNewMultiBetInfo('', betAmount, contract).newBet.probAfter +export const computeDpmElasticity = ( + contract: DPMContract, + betAmount: number +) => { + return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter } const computeTotalPool = (userContracts: Contract[], startTime = 0) => { diff --git a/common/new-contract.ts b/common/new-contract.ts index 8ab44d2e..9a73e2ea 100644 --- a/common/new-contract.ts +++ b/common/new-contract.ts @@ -70,7 +70,7 @@ export function getNewContract( volume: 0, volume24Hours: 0, volume7Days: 0, - elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.56, + elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75, collectedFees: { creatorFee: 0, From adb809f9733fe54a7a3862f75aa0ab35e78bb638 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 6 Oct 2022 15:19:37 -0700 Subject: [PATCH 38/44] Fix google lighthouse issues (#1013) --- web/components/nav/manifold-logo.tsx | 1 + web/components/nav/more-button.tsx | 4 ++-- web/pages/_app.tsx | 5 +---- web/pages/_document.tsx | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/web/components/nav/manifold-logo.tsx b/web/components/nav/manifold-logo.tsx index ec15d54b..b6dbc885 100644 --- a/web/components/nav/manifold-logo.tsx +++ b/web/components/nav/manifold-logo.tsx @@ -22,6 +22,7 @@ export function ManifoldLogo(props: { src={darkBackground ? '/logo-white.svg' : '/logo.svg'} width={45} height={45} + alt="" /> {!hideText && diff --git a/web/components/nav/more-button.tsx b/web/components/nav/more-button.tsx index 5e6653f3..9847541c 100644 --- a/web/components/nav/more-button.tsx +++ b/web/components/nav/more-button.tsx @@ -11,13 +11,13 @@ function SidebarButton(props: { }) { const { text, children } = props return ( - <a className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"> + <button className="group flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"> <props.icon className="-ml-1 mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500" aria-hidden="true" /> <span className="truncate">{text}</span> {children} - </a> + </button> ) } diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 48ad5a9a..7a96b2e2 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -74,10 +74,7 @@ function MyApp({ Component, pageProps }: AppProps<ManifoldPageProps>) { content="https://manifold.markets/logo-bg-white.png" key="image2" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1, maximum-scale=1" - /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> </Head> <AuthProvider serverUser={pageProps.auth}> <QueryClientProvider client={queryClient}> diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index b8cb657c..f2c46854 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -3,7 +3,7 @@ import { ENV_CONFIG } from 'common/envs/constants' export default function Document() { return ( - <Html data-theme="mantic" className="min-h-screen"> + <Html lang="en" data-theme="mantic" className="min-h-screen"> <Head> <link rel="icon" href={ENV_CONFIG.faviconPath} /> <link From d9c8925ea0a7e5c354307d4e4f1b4f24e2d8aad7 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Thu, 6 Oct 2022 15:20:46 -0700 Subject: [PATCH 39/44] don't hide free response panel on open resolve --- web/components/answers/answers-panel.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 08b1373f..6b35f74e 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -157,11 +157,9 @@ export function AnswersPanel(props: { <div className="pb-4 text-gray-500">No answers yet...</div> )} - {outcomeType === 'FREE_RESPONSE' && - tradingAllowed(contract) && - (!resolveOption || resolveOption === 'CANCEL') && ( - <CreateAnswerPanel contract={contract} /> - )} + {outcomeType === 'FREE_RESPONSE' && tradingAllowed(contract) && ( + <CreateAnswerPanel contract={contract} /> + )} {(user?.id === creatorId || (isAdmin && needsAdminToResolve(contract))) && !resolution && ( From 80622dc7ee59269d7d69100984f219a2243184a0 Mon Sep 17 00:00:00 2001 From: mantikoros <sgrugett@gmail.com> Date: Thu, 6 Oct 2022 18:23:27 -0500 Subject: [PATCH 40/44] liquidity sort --- web/components/contract-search.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 78eacd36..8d871d65 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -48,6 +48,7 @@ export const SORTS = [ { label: 'Daily trending', value: 'daily-score' }, { label: '24h volume', value: '24-hour-vol' }, { label: 'Most popular', value: 'most-popular' }, + { label: 'Liquidity', value: 'liquidity' }, { label: 'Last updated', value: 'last-updated' }, { label: 'Closing soon', value: 'close-date' }, { label: 'Resolve date', value: 'resolve-date' }, From 77e0631ea45aba6cff3a1b1e3d7268873faacabd Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 18:03:44 -0500 Subject: [PATCH 41/44] Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'. --- web/components/feed/feed-bets.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index 900265cb..e164f6fa 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -64,11 +64,11 @@ export function BetStatusText(props: { }, [challengeSlug, contract.id]) const bought = amount >= 0 ? 'bought' : 'sold' + const money = formatMoney(Math.abs(amount)) const outOfTotalAmount = bet.limitProb !== undefined && bet.orderAmount !== undefined - ? ` / ${formatMoney(bet.orderAmount)}` + ? ` of ${bet.isCancelled ? money : formatMoney(bet.orderAmount)}` : '' - const money = formatMoney(Math.abs(amount)) const hadPoolMatch = (bet.limitProb === undefined || @@ -105,7 +105,6 @@ export function BetStatusText(props: { {!hideOutcome && ( <> {' '} - of{' '} <OutcomeLabel outcome={outcome} value={(bet as any).value} From d846b9fb3064fd82b698aaa3683921fa28d12e24 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 18:36:27 -0500 Subject: [PATCH 42/44] Date doc: Toggle to disable creating a prediction market --- functions/src/create-post.ts | 30 +++++++++++++++++------------- web/pages/date-docs/[username].tsx | 20 +++++++++++--------- web/pages/date-docs/create.tsx | 26 +++++++++++++++++++------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index 96e3c66a..d1864ac2 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -71,19 +71,23 @@ export const createpost = newEndpoint({}, async (req, auth) => { if (question) { const closeTime = Date.now() + DAY_MS * 30 * 3 - const result = await createMarketHelper( - { - question, - closeTime, - outcomeType: 'BINARY', - visibility: 'unlisted', - initialProb: 50, - // Dating group! - groupId: 'j3ZE8fkeqiKmRGumy3O1', - }, - auth - ) - contractSlug = result.slug + try { + const result = await createMarketHelper( + { + question, + closeTime, + outcomeType: 'BINARY', + visibility: 'unlisted', + initialProb: 50, + // Dating group! + groupId: 'j3ZE8fkeqiKmRGumy3O1', + }, + auth + ) + contractSlug = result.slug + } catch (e) { + console.error(e) + } } const post: Post = removeUndefinedProps({ diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx index dd7d5d73..6f6eaf5e 100644 --- a/web/pages/date-docs/[username].tsx +++ b/web/pages/date-docs/[username].tsx @@ -142,15 +142,17 @@ export function DateDocPost(props: { ) : ( <Content content={content} /> )} - <div className="mt-4 w-full max-w-lg self-center rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-3"> - <iframe - height="405" - src={marketUrl} - title="" - frameBorder="0" - className="w-full rounded-xl bg-white p-10" - ></iframe> - </div> + {contractSlug && ( + <div className="mt-4 w-full max-w-lg self-center rounded-xl bg-gradient-to-r from-blue-200 via-purple-200 to-indigo-300 p-3"> + <iframe + height="405" + src={marketUrl} + title="" + frameBorder="0" + className="w-full rounded-xl bg-white p-10" + ></iframe> + </div> + )} </Col> ) } diff --git a/web/pages/date-docs/create.tsx b/web/pages/date-docs/create.tsx index ed1df677..a0fe8922 100644 --- a/web/pages/date-docs/create.tsx +++ b/web/pages/date-docs/create.tsx @@ -15,6 +15,8 @@ import { MINUTE_MS } from 'common/util/time' import { Col } from 'web/components/layout/col' import { MAX_QUESTION_LENGTH } from 'common/contract' import { NoSEO } from 'web/components/NoSEO' +import ShortToggle from 'web/components/widgets/short-toggle' +import { removeUndefinedProps } from 'common/util/object' export default function CreateDateDocPage() { const user = useUser() @@ -26,6 +28,7 @@ export default function CreateDateDocPage() { const title = `${user?.name}'s Date Doc` const subtitle = 'Manifold dating docs' const [birthday, setBirthday] = useState<undefined | string>(undefined) + const [createMarket, setCreateMarket] = useState(true) const [question, setQuestion] = useState( 'Will I find a partner in the next 3 months?' ) @@ -38,7 +41,11 @@ export default function CreateDateDocPage() { const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined const isValid = - user && birthday && editor && editor.isEmpty === false && question + user && + birthday && + editor && + editor.isEmpty === false && + (question || !createMarket) async function saveDateDoc() { if (!user || !editor || !birthdayTime) return @@ -46,15 +53,15 @@ export default function CreateDateDocPage() { const newPost: Omit< DateDoc, 'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug' - > & { question: string } = { + > & { question?: string } = removeUndefinedProps({ title, subtitle, content: editor.getJSON(), bounty: 0, birthday: birthdayTime, type: 'date-doc', - question, - } + question: createMarket ? question : undefined, + }) const result = await createPost(newPost) @@ -106,9 +113,13 @@ export default function CreateDateDocPage() { </Col> <Col className="gap-4"> - <div className=""> - Finally, we'll create an (unlisted) prediction market! - </div> + <Row className="items-center gap-4"> + <ShortToggle + on={createMarket} + setOn={(on) => setCreateMarket(on)} + /> + Create an (unlisted) prediction market attached to the date doc + </Row> <Col className="gap-2"> <Textarea @@ -116,6 +127,7 @@ export default function CreateDateDocPage() { maxLength={MAX_QUESTION_LENGTH} value={question} onChange={(e) => setQuestion(e.target.value || '')} + disabled={!createMarket} /> <div className="ml-2 text-gray-500">Cost: M$100</div> </Col> From 0dc8753a921366fb3d69814dc005c2d4a1264f46 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Thu, 6 Oct 2022 18:50:53 -0500 Subject: [PATCH 43/44] Listen for date doc changes --- web/hooks/use-post.ts | 14 ++++++++++++-- web/lib/firebase/posts.ts | 13 ++++++++++++- web/pages/date-docs/index.tsx | 4 +++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/web/hooks/use-post.ts b/web/hooks/use-post.ts index ff7bf6b9..1fd69888 100644 --- a/web/hooks/use-post.ts +++ b/web/hooks/use-post.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { Post } from 'common/post' -import { listenForPost } from 'web/lib/firebase/posts' +import { DateDoc, Post } from 'common/post' +import { listenForDateDocs, listenForPost } from 'web/lib/firebase/posts' export const usePost = (postId: string | undefined) => { const [post, setPost] = useState<Post | null | undefined>() @@ -37,3 +37,13 @@ export const usePosts = (postIds: string[]) => { ) .sort((a, b) => b.createdTime - a.createdTime) } + +export const useDateDocs = () => { + const [dateDocs, setDateDocs] = useState<DateDoc[]>() + + useEffect(() => { + return listenForDateDocs(setDateDocs) + }, []) + + return dateDocs +} diff --git a/web/lib/firebase/posts.ts b/web/lib/firebase/posts.ts index 22b9d095..343243cd 100644 --- a/web/lib/firebase/posts.ts +++ b/web/lib/firebase/posts.ts @@ -7,7 +7,13 @@ import { where, } from 'firebase/firestore' import { DateDoc, Post } from 'common/post' -import { coll, getValue, getValues, listenForValue } from './utils' +import { + coll, + getValue, + getValues, + listenForValue, + listenForValues, +} from './utils' import { getUserByUsername } from './users' export const posts = coll<Post>('posts') @@ -51,6 +57,11 @@ export async function getDateDocs() { return getValues<DateDoc>(q) } +export function listenForDateDocs(setDateDocs: (dateDocs: DateDoc[]) => void) { + const q = query(posts, where('type', '==', 'date-doc')) + return listenForValues<DateDoc>(q, setDateDocs) +} + export async function getDateDoc(username: string) { const user = await getUserByUsername(username) if (!user) return null diff --git a/web/pages/date-docs/index.tsx b/web/pages/date-docs/index.tsx index 48e0bb13..f25746ee 100644 --- a/web/pages/date-docs/index.tsx +++ b/web/pages/date-docs/index.tsx @@ -13,6 +13,7 @@ import { SiteLink } from 'web/components/site-link' import { getUser, User } from 'web/lib/firebase/users' import { DateDocPost } from './[username]' import { NoSEO } from 'web/components/NoSEO' +import { useDateDocs } from 'web/hooks/use-post' export async function getStaticProps() { const dateDocs = await getDateDocs() @@ -34,9 +35,10 @@ export default function DatePage(props: { dateDocs: DateDoc[] docCreators: User[] }) { - const { dateDocs, docCreators } = props + const { docCreators } = props const user = useUser() + const dateDocs = useDateDocs() ?? props.dateDocs const hasDoc = dateDocs.some((d) => d.creatorId === user?.id) return ( From 42a7d04b4dc344c55920be706000012b763dc5f6 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Thu, 6 Oct 2022 20:17:26 -0400 Subject: [PATCH 44/44] Tag ArbitrageBot with bot badge --- web/components/user-link.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/user-link.tsx b/web/components/user-link.tsx index c3a273fc..d7f660ae 100644 --- a/web/components/user-link.tsx +++ b/web/components/user-link.tsx @@ -39,7 +39,7 @@ export function UserLink(props: { ) } -const BOT_USERNAMES = ['v'] +const BOT_USERNAMES = ['v', 'ArbitrageBot'] function BotBadge() { return (