From a10605e74c26580b6a5df7160939e913645c74eb Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 21 Sep 2022 00:02:10 -0700 Subject: [PATCH] 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 --- .../contract/contract-leaderboard.tsx | 12 +- web/components/contract/contract-tabs.tsx | 194 +++++++----------- web/pages/[username]/[contractSlug].tsx | 114 ++++------ web/pages/embed/[username]/[contractSlug].tsx | 19 +- 4 files changed, 125 insertions(+), 214 deletions(-) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 4d25ffa4..6cd2ae62 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -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') diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 245a8d7d..008eb584 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -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 = (
@@ -61,15 +53,11 @@ export function ContractTabs(props: { tabs={[ { title: 'Comments', - content: ( - - ), + content: , }, { title: capitalize(PAST_BETS), - content: ( - - ), + content: , }, ...(!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 + } 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 ( <> - + {sortedAnswers.map((answer) => ( +
+
+ ))}
General Comments
- - comment.answerOutcome === undefined && - comment.betId === undefined - )} - tips={tips} - /> + + {generalComments.map((comment) => ( + + ))} ) } else { + const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_') + const topLevelComments = commentsByParent['_'] ?? [] return ( - + <> + + {sortBy(topLevelComments, (c) => -c.createdTime).map((parent) => ( + 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 ( - <> - - {topLevelComments.map((parent) => ( - 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) => ( -
-
- ))} - - ) -} +}) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 3682e700..38df2fbf 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -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 @@ -109,9 +85,7 @@ export default function ContractPage(props: { return } - return ( - - ) + return } // 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[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 = return ( - + }> {showConfetti && ( )} @@ -228,7 +199,7 @@ export function ContractPageContent( )} @@ -271,22 +242,13 @@ export function ContractPageContent( <>
- +
)} - + {!user ? ( @@ -307,26 +269,28 @@ export function ContractPageContent( ) } -function RecommendedContractsWidget(props: { contract: Contract }) { - const { contract } = props - const user = useUser() - const [recommendations, setRecommendations] = useState([]) - useEffect(() => { - if (user) { - getRecommendedContracts(contract, user.id, 6).then(setRecommendations) +const RecommendedContractsWidget = memo( + function RecommendedContractsWidget(props: { contract: Contract }) { + const { contract } = props + const user = useUser() + const [recommendations, setRecommendations] = useState([]) + 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 } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [contract.id, user?.id]) - if (recommendations.length === 0) { - return null + return ( + + + <ContractsGrid + contracts={recommendations} + trackingPostfix=" recommended" + /> + </Col> + ) } - 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> - ) -} +) diff --git a/web/pages/embed/[username]/[contractSlug].tsx b/web/pages/embed/[username]/[contractSlug].tsx index 62dd1ae1..75a9ad05 100644 --- a/web/pages/embed/[username]/[contractSlug].tsx +++ b/web/pages/embed/[username]/[contractSlug].tsx @@ -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