Merge branch 'manifoldmarkets:main' into main
This commit is contained in:
commit
eea173a016
|
@ -4,8 +4,14 @@ import * as utc from 'dayjs/plugin/utc'
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
|
||||||
import { getPrivateUser } from './utils'
|
import { getPrivateUser } from './utils'
|
||||||
import { User } from '../../common/user'
|
import { User } from 'common/user'
|
||||||
import { sendPersonalFollowupEmail, sendWelcomeEmail } from './emails'
|
import {
|
||||||
|
sendCreatorGuideEmail,
|
||||||
|
sendInterestingMarketsEmail,
|
||||||
|
sendPersonalFollowupEmail,
|
||||||
|
sendWelcomeEmail,
|
||||||
|
} from './emails'
|
||||||
|
import { getTrendingContracts } from './weekly-markets-emails'
|
||||||
|
|
||||||
export const onCreateUser = functions
|
export const onCreateUser = functions
|
||||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
|
@ -19,4 +25,21 @@ export const onCreateUser = functions
|
||||||
|
|
||||||
const followupSendTime = dayjs().add(48, 'hours').toString()
|
const followupSendTime = dayjs().add(48, 'hours').toString()
|
||||||
await sendPersonalFollowupEmail(user, privateUser, followupSendTime)
|
await sendPersonalFollowupEmail(user, privateUser, followupSendTime)
|
||||||
|
|
||||||
|
const guideSendTime = dayjs().add(96, 'hours').toString()
|
||||||
|
await sendCreatorGuideEmail(user, privateUser, guideSendTime)
|
||||||
|
|
||||||
|
// skip email if weekly email is about to go out
|
||||||
|
const day = dayjs().utc().day()
|
||||||
|
if (day === 0 || (day === 1 && dayjs().utc().hour() <= 19)) return
|
||||||
|
|
||||||
|
const contracts = await getTrendingContracts()
|
||||||
|
const marketsSendTime = dayjs().add(24, 'hours').toString()
|
||||||
|
|
||||||
|
await sendInterestingMarketsEmail(
|
||||||
|
user,
|
||||||
|
privateUser,
|
||||||
|
contracts,
|
||||||
|
marketsSendTime
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Contract, CPMMBinaryContract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { ContractComment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import { PAST_BETS, User } from 'common/user'
|
import { PAST_BETS, User } from 'common/user'
|
||||||
import {
|
import {
|
||||||
|
@ -11,13 +11,9 @@ import { ContractBetsTable, BetsSummary } from '../bets-list'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import { Tabs } from '../layout/tabs'
|
import { Tabs } from '../layout/tabs'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { tradingAllowed } from 'web/lib/firebase/contracts'
|
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
import { useComments } from 'web/hooks/use-comments'
|
import { useComments } from 'web/hooks/use-comments'
|
||||||
import { useLiquidity } from 'web/hooks/use-liquidity'
|
import { useLiquidity } from 'web/hooks/use-liquidity'
|
||||||
import { BetSignUpPrompt } from '../sign-up-prompt'
|
|
||||||
import { PlayMoneyDisclaimer } from '../play-money-disclaimer'
|
|
||||||
import BetButton from '../bet-button'
|
|
||||||
import { capitalize } from 'lodash'
|
import { capitalize } from 'lodash'
|
||||||
import {
|
import {
|
||||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
@ -123,7 +119,6 @@ export function ContractTabs(props: {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
currentPageForAnalytics={'contract'}
|
currentPageForAnalytics={'contract'}
|
||||||
tabs={[
|
tabs={[
|
||||||
|
@ -147,20 +142,5 @@ export function ContractTabs(props: {
|
||||||
]),
|
]),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!user ? (
|
|
||||||
<Col className="mt-4 max-w-sm items-center xl:hidden">
|
|
||||||
<BetSignUpPrompt />
|
|
||||||
<PlayMoneyDisclaimer />
|
|
||||||
</Col>
|
|
||||||
) : (
|
|
||||||
outcomeType === 'BINARY' &&
|
|
||||||
tradingAllowed(contract) && (
|
|
||||||
<BetButton
|
|
||||||
contract={contract as CPMMBinaryContract}
|
|
||||||
className="mb-2 !mt-0 xl:hidden"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
ContractCommentInput,
|
ContractCommentInput,
|
||||||
FeedComment,
|
FeedComment,
|
||||||
getMostRecentCommentableBet,
|
|
||||||
} from 'web/components/feed/feed-comments'
|
} from 'web/components/feed/feed-comments'
|
||||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
@ -49,28 +48,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
|
|
||||||
const answerElementId = `answer-${answer.id}`
|
const answerElementId = `answer-${answer.id}`
|
||||||
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
||||||
const isFreeResponseContractPage = !!commentsByCurrentUser
|
|
||||||
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
|
||||||
betsByCurrentUser,
|
|
||||||
commentsByCurrentUser,
|
|
||||||
user,
|
|
||||||
answer.number.toString()
|
|
||||||
)
|
|
||||||
const [usersMostRecentBetTimeAtLoad, setUsersMostRecentBetTimeAtLoad] =
|
|
||||||
useState<number | undefined>(
|
|
||||||
!user ? undefined : mostRecentCommentableBet?.createdTime ?? 0
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user && usersMostRecentBetTimeAtLoad === undefined)
|
|
||||||
setUsersMostRecentBetTimeAtLoad(
|
|
||||||
mostRecentCommentableBet?.createdTime ?? 0
|
|
||||||
)
|
|
||||||
}, [
|
|
||||||
mostRecentCommentableBet?.createdTime,
|
|
||||||
user,
|
|
||||||
usersMostRecentBetTimeAtLoad,
|
|
||||||
])
|
|
||||||
|
|
||||||
const scrollAndOpenReplyInput = useEvent(
|
const scrollAndOpenReplyInput = useEvent(
|
||||||
(comment?: ContractComment, answer?: Answer) => {
|
(comment?: ContractComment, answer?: Answer) => {
|
||||||
|
@ -85,19 +62,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Only show one comment input for a bet at a time
|
|
||||||
if (
|
|
||||||
betsByCurrentUser.length > 1 &&
|
|
||||||
// inputRef?.textContent?.length === 0 && //TODO: editor.isEmpty
|
|
||||||
betsByCurrentUser.sort((a, b) => b.createdTime - a.createdTime)[0]
|
|
||||||
?.outcome !== answer.number.toString()
|
|
||||||
)
|
|
||||||
setShowReply(false)
|
|
||||||
// Even if we pass memoized bets this still runs on every render, which we don't want
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [betsByCurrentUser.length, user, answer.number])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.asPath.endsWith(`#${answerElementId}`)) {
|
if (router.asPath.endsWith(`#${answerElementId}`)) {
|
||||||
setHighlighted(true)
|
setHighlighted(true)
|
||||||
|
@ -105,10 +69,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
}, [answerElementId, router.asPath])
|
}, [answerElementId, router.asPath])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col className="relative flex-1 items-stretch gap-3">
|
||||||
className={'relative flex-1 items-stretch gap-3'}
|
|
||||||
key={answer.id + 'comment'}
|
|
||||||
>
|
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'gap-3 space-x-3 pt-4 transition-all duration-1000',
|
'gap-3 space-x-3 pt-4 transition-all duration-1000',
|
||||||
|
@ -133,28 +94,23 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
<span className="whitespace-pre-line text-lg">
|
<span className="whitespace-pre-line text-lg">
|
||||||
<Linkify text={text} />
|
<Linkify text={text} />
|
||||||
</span>
|
</span>
|
||||||
|
<div className="sm:hidden">
|
||||||
{isFreeResponseContractPage && (
|
|
||||||
<div className={'sm:hidden'}>
|
|
||||||
<button
|
<button
|
||||||
className={'text-xs font-bold text-gray-500 hover:underline'}
|
className="text-xs font-bold text-gray-500 hover:underline"
|
||||||
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
|
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
|
||||||
>
|
>
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
{isFreeResponseContractPage && (
|
<div className="justify-initial hidden sm:block">
|
||||||
<div className={'justify-initial hidden sm:block'}>
|
|
||||||
<button
|
<button
|
||||||
className={'text-xs font-bold text-gray-500 hover:underline'}
|
className="text-xs font-bold text-gray-500 hover:underline"
|
||||||
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
|
onClick={() => scrollAndOpenReplyInput(undefined, answer)}
|
||||||
>
|
>
|
||||||
Reply
|
Reply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Col className="gap-3 pl-1">
|
<Col className="gap-3 pl-1">
|
||||||
|
@ -170,7 +126,7 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
{showReply && (
|
{showReply && (
|
||||||
<div className={'relative ml-7'}>
|
<div className="relative ml-7">
|
||||||
<span
|
<span
|
||||||
className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
className="absolute -left-1 -ml-[1px] mt-[1.25rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { Col } from './layout/col'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
import { InfoTooltip } from './info-tooltip'
|
import { InfoTooltip } from './info-tooltip'
|
||||||
import { BETTORS, PRESENT_BET } from 'common/user'
|
import { BETTORS, PRESENT_BET } from 'common/user'
|
||||||
|
import { buildArray } from 'common/util/array'
|
||||||
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
|
|
||||||
export function LiquidityPanel(props: { contract: CPMMContract }) {
|
export function LiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
const { contract } = props
|
const { contract } = props
|
||||||
|
@ -28,16 +30,19 @@ export function LiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
setShowWithdrawal(true)
|
setShowWithdrawal(true)
|
||||||
}, [showWithdrawal, lpShares])
|
}, [showWithdrawal, lpShares])
|
||||||
|
|
||||||
|
const isCreator = user?.id === contract.creatorId
|
||||||
|
const isAdmin = useAdmin()
|
||||||
|
|
||||||
|
if (!showWithdrawal && !isAdmin && !isCreator) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={buildArray(
|
||||||
{
|
(isCreator || isAdmin) && {
|
||||||
title: 'Subsidize',
|
title: (isAdmin ? '[Admin] ' : '') + 'Subsidize',
|
||||||
content: <AddLiquidityPanel contract={contract} />,
|
content: <AddLiquidityPanel contract={contract} />,
|
||||||
},
|
},
|
||||||
...(showWithdrawal
|
showWithdrawal && {
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: 'Withdraw',
|
title: 'Withdraw',
|
||||||
content: (
|
content: (
|
||||||
<WithdrawLiquidityPanel
|
<WithdrawLiquidityPanel
|
||||||
|
@ -46,13 +51,11 @@ export function LiquidityPanel(props: { contract: CPMMContract }) {
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
title: 'Pool',
|
title: 'Pool',
|
||||||
content: <ViewLiquidityPanel contract={contract} />,
|
content: <ViewLiquidityPanel contract={contract} />,
|
||||||
},
|
}
|
||||||
]}
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,10 @@ import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { usePrefetch } from 'web/hooks/use-prefetch'
|
import { usePrefetch } from 'web/hooks/use-prefetch'
|
||||||
import { useAdmin } from 'web/hooks/use-admin'
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
|
import { BetSignUpPrompt } from 'web/components/sign-up-prompt'
|
||||||
|
import { PlayMoneyDisclaimer } from 'web/components/play-money-disclaimer'
|
||||||
|
import BetButton from 'web/components/bet-button'
|
||||||
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
|
@ -205,18 +209,6 @@ export function ContractPageContent(
|
||||||
setShowConfetti(shouldSeeConfetti)
|
setShowConfetti(shouldSeeConfetti)
|
||||||
}, [contract, user])
|
}, [contract, user])
|
||||||
|
|
||||||
const [recommendedContracts, setRecommendedContracts] = useState<Contract[]>(
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
|
||||||
if (contract && user) {
|
|
||||||
getRecommendedContracts(contract, user.id, 6).then(
|
|
||||||
setRecommendedContracts
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [contract.id, user?.id])
|
|
||||||
|
|
||||||
const { isResolved, question, outcomeType } = contract
|
const { isResolved, question, outcomeType } = contract
|
||||||
|
|
||||||
const allowTrade = tradingAllowed(contract)
|
const allowTrade = tradingAllowed(contract)
|
||||||
|
@ -300,17 +292,46 @@ export function ContractPageContent(
|
||||||
tips={tips}
|
tips={tips}
|
||||||
comments={comments}
|
comments={comments}
|
||||||
/>
|
/>
|
||||||
|
{!user ? (
|
||||||
|
<Col className="mt-4 max-w-sm items-center xl:hidden">
|
||||||
|
<BetSignUpPrompt />
|
||||||
|
<PlayMoneyDisclaimer />
|
||||||
</Col>
|
</Col>
|
||||||
|
) : (
|
||||||
{recommendedContracts.length > 0 && (
|
outcomeType === 'BINARY' &&
|
||||||
<Col className="mt-2 gap-2 px-2 sm:px-0">
|
allowTrade && (
|
||||||
<Title className="text-gray-700" text="Recommended" />
|
<BetButton
|
||||||
<ContractsGrid
|
contract={contract as CPMMBinaryContract}
|
||||||
contracts={recommendedContracts}
|
className="mb-2 !mt-0 xl:hidden"
|
||||||
trackingPostfix=" recommended"
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
)
|
||||||
)}
|
)}
|
||||||
|
</Col>
|
||||||
|
<RecommendedContractsWidget contract={contract} />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RecommendedContractsWidget(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
const user = useUser()
|
||||||
|
const [recommendations, setRecommendations] = useState<Contract[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
getRecommendedContracts(contract, user.id, 6).then(setRecommendations)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [contract.id, user?.id])
|
||||||
|
if (recommendations.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Col className="mt-2 gap-2 px-2 sm:px-0">
|
||||||
|
<Title className="text-gray-700" text="Recommended" />
|
||||||
|
<ContractsGrid
|
||||||
|
contracts={recommendations}
|
||||||
|
trackingPostfix=" recommended"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user