More work on contract page and tabs (#912)

* Consolidate comment thread component code

* Move `visibleBets` work down into bets tab

* Remove unnecessary cruft from contract page props

* Don't load all comments in contract page static props anymore

* Tidy up props a bit

* Memoize bets tab

* Memoize recommended contracts widget
This commit is contained in:
Marshall Polaris 2022-09-21 00:02:10 -07:00 committed by GitHub
parent c7f29af2ee
commit a10605e74c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 214 deletions

View File

@ -1,10 +1,10 @@
import { Bet } from 'common/bet'
import { ContractComment } from 'common/comment'
import { resolvedPayout } from 'common/calculate'
import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format'
import { groupBy, mapValues, sumBy, sortBy, keyBy } from 'lodash'
import { useState, useMemo, useEffect } from 'react'
import { useComments } from 'web/hooks/use-comments'
import { listUsers, User } from 'web/lib/firebase/users'
import { FeedBet } from '../feed/feed-bets'
import { FeedComment } from '../feed/feed-comments'
@ -61,12 +61,10 @@ export function ContractLeaderboard(props: {
) : null
}
export function ContractTopTrades(props: {
contract: Contract
bets: Bet[]
comments: ContractComment[]
}) {
const { contract, bets, comments } = props
export function ContractTopTrades(props: { contract: Contract; bets: Bet[] }) {
const { contract, bets } = props
// todo: this stuff should be calced in DB at resolve time
const comments = useComments(contract.id)
const commentsById = keyBy(comments, 'id')
const betsById = keyBy(bets, 'id')

View File

@ -5,19 +5,19 @@ import { FeedBet } from '../feed/feed-bets'
import { FeedLiquidity } from '../feed/feed-liquidity'
import { FeedAnswerCommentGroup } from '../feed/feed-answer-comment-group'
import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { groupBy, sortBy } from 'lodash'
import { Bet } from 'common/bet'
import { Contract, FreeResponseContract } from 'common/contract'
import { ContractComment } from 'common/comment'
import { PAST_BETS, User } from 'common/user'
import { Contract } from 'common/contract'
import { PAST_BETS } from 'common/user'
import { ContractBetsTable, BetsSummary } from '../bets-list'
import { Spacer } from '../layout/spacer'
import { Tabs } from '../layout/tabs'
import { Col } from '../layout/col'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { useComments } from 'web/hooks/use-comments'
import { useLiquidity } from 'web/hooks/use-liquidity'
import { useTipTxns } from 'web/hooks/use-tip-txns'
import { useUser } from 'web/hooks/use-user'
import { capitalize } from 'lodash'
import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
@ -25,21 +25,13 @@ import {
} from 'common/antes'
import { useIsMobile } from 'web/hooks/use-is-mobile'
export function ContractTabs(props: {
contract: Contract
user: User | null | undefined
bets: Bet[]
comments: ContractComment[]
}) {
const { contract, user, bets, comments } = props
export function ContractTabs(props: { contract: Contract; bets: Bet[] }) {
const { contract, bets } = props
const isMobile = useIsMobile()
const user = useUser()
const userBets =
user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id)
const visibleBets = bets.filter(
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
)
const yourTrades = (
<div>
@ -61,15 +53,11 @@ export function ContractTabs(props: {
tabs={[
{
title: 'Comments',
content: (
<CommentsTabContent contract={contract} comments={comments} />
),
content: <CommentsTabContent contract={contract} />,
},
{
title: capitalize(PAST_BETS),
content: (
<ContractBetsActivity contract={contract} bets={visibleBets} />
),
content: <BetsTabContent contract={contract} bets={bets} />,
},
...(!user || !userBets?.length
? []
@ -86,46 +74,87 @@ export function ContractTabs(props: {
const CommentsTabContent = memo(function CommentsTabContent(props: {
contract: Contract
comments: ContractComment[]
}) {
const { contract, comments } = props
const { contract } = props
const tips = useTipTxns({ contractId: contract.id })
const updatedComments = useComments(contract.id) ?? comments
const comments = useComments(contract.id)
if (comments == null) {
return <LoadingIndicator />
}
if (contract.outcomeType === 'FREE_RESPONSE') {
const generalComments = comments.filter(
(c) => c.answerOutcome === undefined && c.betId === undefined
)
const sortedAnswers = sortBy(
contract.answers,
(a) => -getOutcomeProbability(contract, a.id)
)
const commentsByOutcome = groupBy(
comments,
(c) => c.answerOutcome ?? c.betOutcome ?? '_'
)
return (
<>
<FreeResponseContractCommentsActivity
contract={contract}
comments={updatedComments}
tips={tips}
{sortedAnswers.map((answer) => (
<div key={answer.id} className="relative pb-4">
<span
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
aria-hidden="true"
/>
<Col className="mt-8 flex w-full">
<div className="text-md mt-8 mb-2 text-left">General Comments</div>
<div className="mb-4 w-full border-b border-gray-200" />
<ContractCommentsActivity
<FeedAnswerCommentGroup
contract={contract}
comments={updatedComments.filter(
(comment) =>
comment.answerOutcome === undefined &&
comment.betId === undefined
answer={answer}
answerComments={sortBy(
commentsByOutcome[answer.number.toString()] ?? [],
(c) => c.createdTime
)}
tips={tips}
/>
</div>
))}
<Col className="mt-8 flex w-full">
<div className="text-md mt-8 mb-2 text-left">General Comments</div>
<div className="mb-4 w-full border-b border-gray-200" />
<ContractCommentInput className="mb-5" contract={contract} />
{generalComments.map((comment) => (
<FeedCommentThread
key={comment.id}
contract={contract}
parentComment={comment}
threadComments={[]}
tips={tips}
/>
))}
</Col>
</>
)
} else {
const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_')
const topLevelComments = commentsByParent['_'] ?? []
return (
<ContractCommentsActivity
<>
<ContractCommentInput className="mb-5" contract={contract} />
{sortBy(topLevelComments, (c) => -c.createdTime).map((parent) => (
<FeedCommentThread
key={parent.id}
contract={contract}
comments={comments}
parentComment={parent}
threadComments={sortBy(
commentsByParent[parent.id] ?? [],
(c) => c.createdTime
)}
tips={tips}
/>
))}
</>
)
}
})
function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
const BetsTabContent = memo(function BetsTabContent(props: {
contract: Contract
bets: Bet[]
}) {
const { contract, bets } = props
const [page, setPage] = useState(0)
const ITEMS_PER_PAGE = 50
@ -133,6 +162,9 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
const end = start + ITEMS_PER_PAGE
const lps = useLiquidity(contract.id) ?? []
const visibleBets = bets.filter(
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
)
const visibleLps = lps.filter(
(l) =>
!l.isAnte &&
@ -142,7 +174,7 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
)
const items = [
...bets.map((bet) => ({
...visibleBets.map((bet) => ({
type: 'bet' as const,
id: bet.id + '-' + bet.isSold,
bet,
@ -184,74 +216,4 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
/>
</>
)
}
function ContractCommentsActivity(props: {
contract: Contract
comments: ContractComment[]
tips: CommentTipMap
}) {
const { contract, comments, tips } = props
const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_')
const topLevelComments = sortBy(
commentsByParentId['_'] ?? [],
(c) => -c.createdTime
)
return (
<>
<ContractCommentInput className="mb-5" contract={contract} />
{topLevelComments.map((parent) => (
<FeedCommentThread
key={parent.id}
contract={contract}
parentComment={parent}
threadComments={sortBy(
commentsByParentId[parent.id] ?? [],
(c) => c.createdTime
)}
tips={tips}
/>
))}
</>
)
}
function FreeResponseContractCommentsActivity(props: {
contract: FreeResponseContract
comments: ContractComment[]
tips: CommentTipMap
}) {
const { contract, comments, tips } = props
const sortedAnswers = sortBy(
contract.answers,
(answer) => -getOutcomeProbability(contract, answer.number.toString())
)
const commentsByOutcome = groupBy(
comments,
(c) => c.answerOutcome ?? c.betOutcome ?? '_'
)
return (
<>
{sortedAnswers.map((answer) => (
<div key={answer.id} className="relative pb-4">
<span
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
aria-hidden="true"
/>
<FeedAnswerCommentGroup
contract={contract}
answer={answer}
answerComments={sortBy(
commentsByOutcome[answer.number.toString()] ?? [],
(c) => c.createdTime
)}
tips={tips}
/>
</div>
))}
</>
)
}
})

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'
import React, { memo, useEffect, useMemo, useState } from 'react'
import { ArrowLeftIcon } from '@heroicons/react/outline'
import { useContractWithPreload } from 'web/hooks/use-contract'
@ -17,7 +17,6 @@ import {
import { SEO } from 'web/components/SEO'
import { Page } from 'web/components/page'
import { Bet, listAllBets } from 'web/lib/firebase/bets'
import { listAllComments } from 'web/lib/firebase/comments'
import Custom404 from '../404'
import { AnswersPanel } from 'web/components/answers/answers-panel'
import { fromPropz, usePropz } from 'web/hooks/use-propz'
@ -32,8 +31,6 @@ import { CPMMBinaryContract } from 'common/contract'
import { AlertBox } from 'web/components/alert-box'
import { useTracking } from 'web/hooks/use-tracking'
import { useSaveReferral } from 'web/hooks/use-save-referral'
import { User } from 'common/user'
import { ContractComment } from 'common/comment'
import { getOpenGraphProps } from 'common/contract-details'
import { ContractDescription } from 'web/components/contract/contract-description'
import {
@ -54,25 +51,14 @@ export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
params: { username: string; contractSlug: string }
}) {
const { username, contractSlug } = props.params
const { contractSlug } = props.params
const contract = (await getContractFromSlug(contractSlug)) || null
const contractId = contract?.id
const [bets, comments] = await Promise.all([
contractId ? listAllBets(contractId) : [],
contractId ? listAllComments(contractId) : [],
])
const bets = contractId ? await listAllBets(contractId) : []
return {
props: {
contract,
username,
slug: contractSlug,
// Limit the data sent to the client. Client will still load all bets and comments directly.
bets: bets.slice(0, 5000),
comments: comments.slice(0, 1000),
},
// Limit the data sent to the client. Client will still load all bets directly.
props: { contract, bets: bets.slice(0, 5000) },
revalidate: 5, // regenerate after five seconds
}
}
@ -83,21 +69,11 @@ export async function getStaticPaths() {
export default function ContractPage(props: {
contract: Contract | null
username: string
bets: Bet[]
comments: ContractComment[]
slug: string
backToHome?: () => void
}) {
props = usePropz(props, getStaticPropz) ?? {
contract: null,
username: '',
comments: [],
bets: [],
slug: '',
}
props = usePropz(props, getStaticPropz) ?? { contract: null, bets: [] }
const user = useUser()
const inIframe = useIsIframe()
if (inIframe) {
return <ContractEmbedPage {...props} />
@ -109,9 +85,7 @@ export default function ContractPage(props: {
return <Custom404 />
}
return (
<ContractPageContent key={contract.id} {...{ ...props, contract, user }} />
)
return <ContractPageContent key={contract.id} {...{ ...props, contract }} />
}
// requires an admin to resolve a week after market closes
@ -119,12 +93,10 @@ export function needsAdminToResolve(contract: Contract) {
return !contract.isResolved && dayjs().diff(contract.closeTime, 'day') > 7
}
export function ContractPageSidebar(props: {
user: User | null | undefined
contract: Contract
}) {
const { contract, user } = props
export function ContractPageSidebar(props: { contract: Contract }) {
const { contract } = props
const { creatorId, isResolved, outcomeType } = contract
const user = useUser()
const isCreator = user?.id === creatorId
const isBinary = outcomeType === 'BINARY'
const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC'
@ -173,11 +145,11 @@ export function ContractPageSidebar(props: {
export function ContractPageContent(
props: Parameters<typeof ContractPage>[0] & {
contract: Contract
user?: User | null
}
) {
const { backToHome, comments, user } = props
const { backToHome } = props
const contract = useContractWithPreload(props.contract) ?? props.contract
const user = useUser()
usePrefetch(user?.id)
useTracking(
'view market',
@ -217,9 +189,8 @@ export function ContractPageContent(
contractId: contract.id,
})
const rightSidebar = <ContractPageSidebar user={user} contract={contract} />
return (
<Page rightSidebar={rightSidebar}>
<Page rightSidebar={<ContractPageSidebar contract={contract} />}>
{showConfetti && (
<FullscreenConfetti recycle={false} numberOfPieces={300} />
)}
@ -228,7 +199,7 @@ export function ContractPageContent(
<SEO
title={question}
description={ogCardProps.description}
url={`/${props.username}/${props.slug}`}
url={`/${contract.creatorUsername}/${contract.slug}`}
ogCardProps={ogCardProps}
/>
)}
@ -271,22 +242,13 @@ export function ContractPageContent(
<>
<div className="grid grid-cols-1 sm:grid-cols-2">
<ContractLeaderboard contract={contract} bets={bets} />
<ContractTopTrades
contract={contract}
bets={bets}
comments={comments}
/>
<ContractTopTrades contract={contract} bets={bets} />
</div>
<Spacer h={12} />
</>
)}
<ContractTabs
contract={contract}
user={user}
bets={bets}
comments={comments}
/>
<ContractTabs contract={contract} bets={bets} />
{!user ? (
<Col className="mt-4 max-w-sm items-center xl:hidden">
<BetSignUpPrompt />
@ -307,6 +269,7 @@ export function ContractPageContent(
)
}
const RecommendedContractsWidget = memo(
function RecommendedContractsWidget(props: { contract: Contract }) {
const { contract } = props
const user = useUser()
@ -330,3 +293,4 @@ function RecommendedContractsWidget(props: { contract: Contract }) {
</Col>
)
}
)

View File

@ -34,20 +34,14 @@ export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
params: { username: string; contractSlug: string }
}) {
const { username, contractSlug } = props.params
const { contractSlug } = props.params
const contract = (await getContractFromSlug(contractSlug)) || null
const contractId = contract?.id
const bets = contractId ? await listAllBets(contractId) : []
return {
props: {
contract,
username,
slug: contractSlug,
bets,
},
props: { contract, bets },
revalidate: 60, // regenerate after a minute
}
}
@ -58,16 +52,9 @@ export async function getStaticPaths() {
export default function ContractEmbedPage(props: {
contract: Contract | null
username: string
bets: Bet[]
slug: string
}) {
props = usePropz(props, getStaticPropz) ?? {
contract: null,
username: '',
bets: [],
slug: '',
}
props = usePropz(props, getStaticPropz) ?? { contract: null, bets: [] }
const contract = useContractWithPreload(props.contract)
const { bets } = props