diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index 6e0bfef6..3de99c34 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -11,7 +11,6 @@ import { AnswerItem } from './answer-item' import { CreateAnswerPanel } from './create-answer-panel' import { AnswerResolvePanel } from './answer-resolve-panel' import { Spacer } from '../layout/spacer' -import { ActivityItem } from '../feed/activity-items' import { User } from 'common/user' import { getOutcomeProbability } from 'common/calculate' import { Answer } from 'common/answer' @@ -176,7 +175,6 @@ function getAnswerItems( type: 'answer' as const, contract, answer, - items: [] as ActivityItem[], user, } }) @@ -186,7 +184,6 @@ function getAnswerItems( function OpenAnswer(props: { contract: FreeResponseContract | MultipleChoiceContract answer: Answer - items: ActivityItem[] type: string }) { const { answer, contract } = props diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index 3270408b..3217da3d 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -394,13 +394,11 @@ export function BetsSummary(props: { const { hasShares, invested, profitPercent, payout, profit, totalShares } = getContractBetMetrics(contract, bets) - const excludeSalesAndAntes = bets.filter( - (b) => !b.isAnte && !b.isSold && !b.sale - ) - const yesWinnings = sumBy(excludeSalesAndAntes, (bet) => + const excludeSales = bets.filter((b) => !b.isSold && !b.sale) + const yesWinnings = sumBy(excludeSales, (bet) => calculatePayout(contract, bet, 'YES') ) - const noWinnings = sumBy(excludeSalesAndAntes, (bet) => + const noWinnings = sumBy(excludeSales, (bet) => calculatePayout(contract, bet, 'NO') ) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 77af001e..cc253433 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -107,7 +107,6 @@ export function ContractTopTrades(props: { comment={commentsById[topCommentId]} tips={tips[topCommentId]} betsBySameUser={[betsById[topCommentId]]} - smallAvatar={false} />
@@ -123,12 +122,7 @@ export function ContractTopTrades(props: { <> <div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4"> - <FeedBet - contract={contract} - bet={betsById[topBetId]} - hideOutcome={false} - smallAvatar={false} - /> + <FeedBet contract={contract} bet={betsById[topBetId]} /> </div> <div className="mt-2 text-sm text-gray-500"> {topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}! diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 9e9f62bf..417de12b 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -1,15 +1,24 @@ import { Bet } from 'common/bet' -import { Contract } from 'common/contract' +import { Contract, CPMMBinaryContract } from 'common/contract' import { ContractComment } from 'common/comment' import { User } from 'common/user' -import { ContractActivity } from '../feed/contract-activity' +import { + ContractCommentsActivity, + ContractBetsActivity, + FreeResponseContractCommentsActivity, +} from '../feed/contract-activity' import { ContractBetsTable, BetsSummary } from '../bets-list' import { Spacer } from '../layout/spacer' import { Tabs } from '../layout/tabs' import { Col } from '../layout/col' +import { tradingAllowed } from 'web/lib/firebase/contracts' import { CommentTipMap } from 'web/hooks/use-tip-txns' +import { useBets } from 'web/hooks/use-bets' import { useComments } from 'web/hooks/use-comments' import { useLiquidity } from 'web/hooks/use-liquidity' +import { BetSignUpPrompt } from '../sign-up-prompt' +import { PlayMoneyDisclaimer } from '../play-money-disclaimer' +import BetButton from '../bet-button' export function ContractTabs(props: { contract: Contract @@ -18,68 +27,69 @@ export function ContractTabs(props: { comments: ContractComment[] tips: CommentTipMap }) { - const { contract, user, bets, tips } = props + const { contract, user, tips } = props const { outcomeType } = contract - const userBets = user && bets.filter((bet) => bet.userId === user.id) + const bets = useBets(contract.id) ?? props.bets + const lps = useLiquidity(contract.id) ?? [] + + 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 liquidityProvisions = - useLiquidity(contract.id)?.filter((l) => !l.isAnte && l.amount > 0) ?? [] + const visibleLps = lps.filter((l) => !l.isAnte && l.amount > 0) // Load comments here, so the badge count will be correct const updatedComments = useComments(contract.id) const comments = updatedComments ?? props.comments const betActivity = ( - <ContractActivity + <ContractBetsActivity contract={contract} - bets={bets} - liquidityProvisions={liquidityProvisions} - comments={comments} - tips={tips} - user={user} - mode="bets" - betRowClassName="!mt-0 xl:hidden" + bets={visibleBets} + lps={visibleLps} /> ) - const commentActivity = ( - <> - <ContractActivity - contract={contract} - bets={bets} - liquidityProvisions={liquidityProvisions} - comments={comments} - tips={tips} - user={user} - mode={ - contract.outcomeType === 'FREE_RESPONSE' - ? 'free-response-comment-answer-groups' - : 'comments' - } - betRowClassName="!mt-0 xl:hidden" - /> - {outcomeType === 'FREE_RESPONSE' && ( + const generalBets = outcomeType === 'FREE_RESPONSE' ? [] : visibleBets + const generalComments = comments.filter( + (comment) => + comment.answerOutcome === undefined && + (outcomeType === 'FREE_RESPONSE' ? comment.betId === undefined : true) + ) + + const commentActivity = + outcomeType === 'FREE_RESPONSE' ? ( + <> + <FreeResponseContractCommentsActivity + contract={contract} + bets={visibleBets} + comments={comments} + tips={tips} + user={user} + /> <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'} /> - <ContractActivity + <ContractCommentsActivity contract={contract} - bets={bets} - liquidityProvisions={liquidityProvisions} - comments={comments} + bets={generalBets} + comments={generalComments} tips={tips} user={user} - mode={'comments'} - betRowClassName="!mt-0 xl:hidden" /> </Col> - )} - </> - ) + </> + ) : ( + <ContractCommentsActivity + contract={contract} + bets={visibleBets} + comments={comments} + tips={tips} + user={user} + /> + ) const yourTrades = ( <div> @@ -96,19 +106,39 @@ export function ContractTabs(props: { ) return ( - <Tabs - currentPageForAnalytics={'contract'} - tabs={[ - { - title: 'Comments', - content: commentActivity, - badge: `${comments.length}`, - }, - { title: 'Bets', content: betActivity, badge: `${visibleBets.length}` }, - ...(!user || !userBets?.length - ? [] - : [{ title: 'Your bets', content: yourTrades }]), - ]} - /> + <> + <Tabs + currentPageForAnalytics={'contract'} + tabs={[ + { + title: 'Comments', + content: commentActivity, + badge: `${comments.length}`, + }, + { + title: 'Bets', + content: betActivity, + badge: `${visibleBets.length}`, + }, + ...(!user || !userBets?.length + ? [] + : [{ title: 'Your bets', content: yourTrades }]), + ]} + /> + {!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" + /> + ) + )} + </> ) } diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 6af58caa..5f056f8b 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -236,9 +236,10 @@ const useUploadMutation = (editor: Editor | null) => export function RichContent(props: { content: JSONContent | string + className?: string smallImage?: boolean }) { - const { content, smallImage } = props + const { className, content, smallImage } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } }, extensions: [ @@ -254,19 +255,24 @@ export function RichContent(props: { }) useEffect(() => void editor?.commands?.setContent(content), [editor, content]) - return <EditorContent editor={editor} /> + return <EditorContent className={className} editor={editor} /> } // backwards compatibility: we used to store content as strings export function Content(props: { content: JSONContent | string + className?: string smallImage?: boolean }) { - const { content } = props + const { className, content } = props return typeof content === 'string' ? ( - <div className="whitespace-pre-line font-light leading-relaxed"> - <Linkify text={content} /> - </div> + <Linkify + className={clsx( + className, + 'whitespace-pre-line font-light leading-relaxed' + )} + text={content} + /> ) : ( <RichContent {...props} /> ) diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts deleted file mode 100644 index bcbb6721..00000000 --- a/web/components/feed/activity-items.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { uniq, sortBy } from 'lodash' - -import { Answer } from 'common/answer' -import { Bet } from 'common/bet' -import { getOutcomeProbability } from 'common/calculate' -import { ContractComment } from 'common/comment' -import { Contract, FreeResponseContract } from 'common/contract' -import { User } from 'common/user' -import { CommentTipMap } from 'web/hooks/use-tip-txns' -import { LiquidityProvision } from 'common/liquidity-provision' - -export type ActivityItem = - | DescriptionItem - | QuestionItem - | BetItem - | AnswerGroupItem - | CloseItem - | ResolveItem - | CommentInputItem - | CommentThreadItem - | LiquidityItem - -type BaseActivityItem = { - id: string - contract: Contract -} - -export type CommentInputItem = BaseActivityItem & { - type: 'commentInput' - betsByCurrentUser: Bet[] - commentsByCurrentUser: ContractComment[] -} - -export type DescriptionItem = BaseActivityItem & { - type: 'description' -} - -export type QuestionItem = BaseActivityItem & { - type: 'question' - contractPath?: string -} - -export type BetItem = BaseActivityItem & { - type: 'bet' - bet: Bet - hideOutcome: boolean - smallAvatar: boolean - hideComment?: boolean -} - -export type CommentThreadItem = BaseActivityItem & { - type: 'commentThread' - parentComment: ContractComment - comments: ContractComment[] - tips: CommentTipMap - bets: Bet[] -} - -export type AnswerGroupItem = BaseActivityItem & { - type: 'answergroup' - user: User | undefined | null - answer: Answer - comments: ContractComment[] - tips: CommentTipMap - bets: Bet[] -} - -export type CloseItem = BaseActivityItem & { - type: 'close' -} - -export type ResolveItem = BaseActivityItem & { - type: 'resolve' -} - -export type LiquidityItem = BaseActivityItem & { - type: 'liquidity' - liquidity: LiquidityProvision - hideOutcome: boolean - smallAvatar: boolean - hideComment?: boolean -} - -function getAnswerAndCommentInputGroups( - contract: FreeResponseContract, - bets: Bet[], - comments: ContractComment[], - tips: CommentTipMap, - user: User | undefined | null -) { - let outcomes = uniq(bets.map((bet) => bet.outcome)) - outcomes = sortBy(outcomes, (outcome) => - getOutcomeProbability(contract, outcome) - ) - - const answerGroups = outcomes - .map((outcome) => { - const answer = contract.answers?.find( - (answer) => answer.id === outcome - ) as Answer - - return { - id: outcome, - type: 'answergroup' as const, - contract, - user, - answer, - comments, - tips, - bets, - } - }) - .filter((group) => group.answer) as ActivityItem[] - return answerGroups -} - -function getCommentThreads( - bets: Bet[], - comments: ContractComment[], - tips: CommentTipMap, - contract: Contract -) { - const parentComments = comments.filter((comment) => !comment.replyToCommentId) - - const items = parentComments.map((comment) => ({ - type: 'commentThread' as const, - id: comment.id, - contract: contract, - comments: comments, - parentComment: comment, - bets: bets, - tips, - })) - - return items -} - -function commentIsGeneralComment(comment: ContractComment, contract: Contract) { - return ( - comment.answerOutcome === undefined && - (contract.outcomeType === 'FREE_RESPONSE' - ? comment.betId === undefined - : true) - ) -} - -export function getSpecificContractActivityItems( - contract: Contract, - bets: Bet[], - comments: ContractComment[], - liquidityProvisions: LiquidityProvision[], - tips: CommentTipMap, - user: User | null | undefined, - options: { - mode: 'comments' | 'bets' | 'free-response-comment-answer-groups' - } -) { - const { mode } = options - let items = [] as ActivityItem[] - - switch (mode) { - case 'bets': - // Remove first bet (which is the ante): - if (contract.outcomeType === 'FREE_RESPONSE') bets = bets.slice(1) - items.push( - ...bets.map((bet) => ({ - type: 'bet' as const, - id: bet.id + '-' + bet.isSold, - bet, - contract, - hideOutcome: false, - smallAvatar: false, - hideComment: true, - })) - ) - items.push( - ...liquidityProvisions.map((liquidity) => ({ - type: 'liquidity' as const, - id: liquidity.id, - contract, - liquidity, - hideOutcome: false, - smallAvatar: false, - })) - ) - items = sortBy(items, (item) => - item.type === 'bet' - ? item.bet.createdTime - : item.type === 'liquidity' - ? item.liquidity.createdTime - : undefined - ) - break - - case 'comments': { - const nonFreeResponseComments = comments.filter((comment) => - commentIsGeneralComment(comment, contract) - ) - const nonFreeResponseBets = - contract.outcomeType === 'FREE_RESPONSE' ? [] : bets - items.push( - ...getCommentThreads( - nonFreeResponseBets, - nonFreeResponseComments, - tips, - contract - ) - ) - - items.push({ - type: 'commentInput', - id: 'commentInput', - contract, - betsByCurrentUser: nonFreeResponseBets.filter( - (bet) => bet.userId === user?.id - ), - commentsByCurrentUser: nonFreeResponseComments.filter( - (comment) => comment.userId === user?.id - ), - }) - break - } - case 'free-response-comment-answer-groups': - items.push( - ...getAnswerAndCommentInputGroups( - contract as FreeResponseContract, - bets, - comments, - tips, - user - ) - ) - break - } - - return items.reverse() -} diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index 3cc0acb0..744f06aa 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -1,55 +1,144 @@ -import { Contract } from 'web/lib/firebase/contracts' +import { Contract, FreeResponseContract } from 'common/contract' import { ContractComment } from 'common/comment' +import { Answer } from 'common/answer' import { Bet } from 'common/bet' -import { useBets } from 'web/hooks/use-bets' -import { getSpecificContractActivityItems } from './activity-items' -import { FeedItems } from './feed-items' +import { getOutcomeProbability } from 'common/calculate' +import { FeedBet } from './feed-bets' +import { FeedLiquidity } from './feed-liquidity' +import { FeedAnswerCommentGroup } from './feed-answer-comment-group' +import { FeedCommentThread, CommentInput } from './feed-comments' import { User } from 'common/user' -import { useContractWithPreload } from 'web/hooks/use-contract' import { CommentTipMap } from 'web/hooks/use-tip-txns' import { LiquidityProvision } from 'common/liquidity-provision' +import { groupBy, sortBy, uniq } from 'lodash' +import { Col } from 'web/components/layout/col' -export function ContractActivity(props: { +export function ContractBetsActivity(props: { contract: Contract bets: Bet[] - comments: ContractComment[] - liquidityProvisions: LiquidityProvision[] - tips: CommentTipMap - user: User | null | undefined - mode: 'comments' | 'bets' | 'free-response-comment-answer-groups' - contractPath?: string - className?: string - betRowClassName?: string + lps: LiquidityProvision[] }) { - const { user, mode, tips, className, betRowClassName, liquidityProvisions } = - props + const { contract, bets, lps } = props - const contract = useContractWithPreload(props.contract) ?? props.contract - const comments = props.comments - const updatedBets = useBets(contract.id, { - filterChallenges: false, - filterRedemptions: true, - }) - const bets = (updatedBets ?? props.bets).filter( - (bet) => !bet.isRedemption && bet.amount !== 0 - ) - const items = getSpecificContractActivityItems( - contract, - bets, - comments, - liquidityProvisions, - tips, - user, - { mode } + const items = [ + ...bets.map((bet) => ({ + type: 'bet' as const, + id: bet.id + '-' + bet.isSold, + bet, + })), + ...lps.map((lp) => ({ + type: 'liquidity' as const, + id: lp.id, + lp, + })), + ] + + const sortedItems = sortBy(items, (item) => + item.type === 'bet' + ? -item.bet.createdTime + : item.type === 'liquidity' + ? -item.lp.createdTime + : undefined ) return ( - <FeedItems - contract={contract} - items={items} - className={className} - betRowClassName={betRowClassName} - user={user} - /> + <Col className="gap-4"> + {sortedItems.map((item) => + item.type === 'bet' ? ( + <FeedBet key={item.id} contract={contract} bet={item.bet} /> + ) : ( + <FeedLiquidity key={item.id} liquidity={item.lp} /> + ) + )} + </Col> + ) +} + +export function ContractCommentsActivity(props: { + contract: Contract + bets: Bet[] + comments: ContractComment[] + tips: CommentTipMap + user: User | null | undefined +}) { + const { bets, contract, comments, user, tips } = props + const betsByUserId = groupBy(bets, (bet) => bet.userId) + const commentsByUserId = groupBy(comments, (c) => c.userId) + const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_') + const topLevelComments = sortBy( + commentsByParentId['_'] ?? [], + (c) => -c.createdTime + ) + + return ( + <> + <CommentInput + className="mb-5" + contract={contract} + betsByCurrentUser={(user && betsByUserId[user.id]) ?? []} + commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []} + /> + {topLevelComments.map((parent) => ( + <FeedCommentThread + key={parent.id} + user={user} + contract={contract} + parentComment={parent} + threadComments={commentsByParentId[parent.id] ?? []} + tips={tips} + bets={bets} + betsByUserId={betsByUserId} + commentsByUserId={commentsByUserId} + /> + ))} + </> + ) +} + +export function FreeResponseContractCommentsActivity(props: { + contract: FreeResponseContract + bets: Bet[] + comments: ContractComment[] + tips: CommentTipMap + user: User | null | undefined +}) { + const { bets, contract, comments, user, tips } = props + + let outcomes = uniq(bets.map((bet) => bet.outcome)) + outcomes = sortBy( + outcomes, + (outcome) => -getOutcomeProbability(contract, outcome) + ) + + const answers = outcomes + .map((outcome) => { + return contract.answers.find((answer) => answer.id === outcome) as Answer + }) + .filter((answer) => answer != null) + + const betsByUserId = groupBy(bets, (bet) => bet.userId) + const commentsByUserId = groupBy(comments, (c) => c.userId) + const commentsByOutcome = groupBy(comments, (c) => c.answerOutcome ?? '_') + + return ( + <> + {answers.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} + user={user} + answer={answer} + answerComments={commentsByOutcome[answer.number.toString()] ?? []} + tips={tips} + betsByUserId={betsByUserId} + commentsByUserId={commentsByUserId} + /> + </div> + ))} + </> ) } diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index c4e69655..d4401b8c 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import { ENV_CONFIG } from 'common/envs/constants' import { copyToClipboard } from 'web/lib/util/copy' import { DateTimeTooltip } from 'web/components/datetime-tooltip' import Link from 'next/link' @@ -21,9 +20,10 @@ export function CopyLinkDateTimeComponent(props: { event: React.MouseEvent<HTMLAnchorElement, MouseEvent> ) { event.preventDefault() - const elementLocation = `https://${ENV_CONFIG.domain}/${prefix}/${slug}#${elementId}` - - copyToClipboard(elementLocation) + const commentUrl = new URL(window.location.href) + commentUrl.pathname = `/${prefix}/${slug}` + commentUrl.hash = elementId + copyToClipboard(commentUrl.toString()) setShowToast(true) setTimeout(() => setShowToast(false), 2000) } diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 86686f1f..2df8cb4a 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -1,5 +1,6 @@ import { Answer } from 'common/answer' import { Bet } from 'common/bet' +import { FreeResponseContract } from 'common/contract' import { ContractComment } from 'common/comment' import React, { useEffect, useState } from 'react' import { Col } from 'web/components/layout/col' @@ -10,25 +11,34 @@ import { Linkify } from 'web/components/linkify' import clsx from 'clsx' import { CommentInput, - CommentRepliesList, + FeedComment, getMostRecentCommentableBet, } from 'web/components/feed/feed-comments' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { useRouter } from 'next/router' -import { groupBy } from 'lodash' +import { Dictionary } from 'lodash' import { User } from 'common/user' import { useEvent } from 'web/hooks/use-event' import { CommentTipMap } from 'web/hooks/use-tip-txns' export function FeedAnswerCommentGroup(props: { - contract: any + contract: FreeResponseContract user: User | undefined | null answer: Answer - comments: ContractComment[] + answerComments: ContractComment[] tips: CommentTipMap - bets: Bet[] + betsByUserId: Dictionary<Bet[]> + commentsByUserId: Dictionary<ContractComment[]> }) { - const { answer, contract, comments, tips, bets, user } = props + const { + answer, + contract, + answerComments, + tips, + betsByUserId, + commentsByUserId, + user, + } = props const { username, avatarUrl, name, text } = answer const [replyToUser, setReplyToUser] = @@ -38,11 +48,6 @@ export function FeedAnswerCommentGroup(props: { const router = useRouter() const answerElementId = `answer-${answer.id}` - const betsByUserId = groupBy(bets, (bet) => bet.userId) - const commentsByUserId = groupBy(comments, (comment) => comment.userId) - const commentsList = comments.filter( - (comment) => comment.answerOutcome === answer.number.toString() - ) const betsByCurrentUser = (user && betsByUserId[user.id]) ?? [] const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? [] const isFreeResponseContractPage = !!commentsByCurrentUser @@ -101,10 +106,13 @@ export function FeedAnswerCommentGroup(props: { }, [answerElementId, router.asPath]) return ( - <Col className={'relative flex-1 gap-3'} key={answer.id + 'comment'}> + <Col + className={'relative flex-1 items-stretch gap-3'} + key={answer.id + 'comment'} + > <Row className={clsx( - 'flex gap-3 space-x-3 pt-4 transition-all duration-1000', + 'gap-3 space-x-3 pt-4 transition-all duration-1000', highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : '' )} id={answerElementId} @@ -150,21 +158,23 @@ export function FeedAnswerCommentGroup(props: { )} </Col> </Row> - <CommentRepliesList - contract={contract} - commentsList={commentsList} - betsByUserId={betsByUserId} - smallAvatar={true} - bets={bets} - tips={tips} - scrollAndOpenReplyInput={scrollAndOpenReplyInput} - treatFirstIndexEqually={true} - /> - + <Col className="gap-3 pl-1"> + {answerComments.map((comment) => ( + <FeedComment + key={comment.id} + indent={true} + contract={contract} + comment={comment} + tips={tips[comment.id]} + betsBySameUser={betsByUserId[comment.userId] ?? []} + onReplyClick={scrollAndOpenReplyInput} + /> + ))} + </Col> {showReply && ( - <div className={'ml-6'}> + <div className={'relative ml-7'}> <span - className="absolute -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" /> <CommentInput diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index e4200593..b7aeb321 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -16,13 +16,8 @@ import { SiteLink } from 'web/components/site-link' import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' import { Challenge } from 'common/challenge' -export function FeedBet(props: { - contract: Contract - bet: Bet - hideOutcome: boolean - smallAvatar: boolean -}) { - const { contract, bet, hideOutcome, smallAvatar } = props +export function FeedBet(props: { contract: Contract; bet: Bet }) { + const { contract, bet } = props const { userId, createdTime } = bet const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01') @@ -33,21 +28,11 @@ export function FeedBet(props: { const isSelf = user?.id === userId return ( - <Row className={'flex w-full items-center gap-2 pt-3'}> + <Row className="items-center gap-2 pt-3"> {isSelf ? ( - <Avatar - className={clsx(smallAvatar && 'ml-1')} - size={smallAvatar ? 'sm' : undefined} - avatarUrl={user.avatarUrl} - username={user.username} - /> + <Avatar avatarUrl={user.avatarUrl} username={user.username} /> ) : bettor ? ( - <Avatar - className={clsx(smallAvatar && 'ml-1')} - size={smallAvatar ? 'sm' : undefined} - avatarUrl={bettor.avatarUrl} - username={bettor.username} - /> + <Avatar avatarUrl={bettor.avatarUrl} username={bettor.username} /> ) : ( <EmptyAvatar className="mx-1" /> )} @@ -56,7 +41,6 @@ export function FeedBet(props: { contract={contract} isSelf={isSelf} bettor={bettor} - hideOutcome={hideOutcome} className="flex-1" /> </Row> diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index d74be119..9e6c3cd5 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -3,7 +3,7 @@ import { ContractComment } from 'common/comment' import { User } from 'common/user' import { Contract } from 'common/contract' import React, { useEffect, useState } from 'react' -import { minBy, maxBy, groupBy, partition, sumBy, Dictionary } from 'lodash' +import { minBy, maxBy, partition, sumBy, Dictionary } from 'lodash' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { useRouter } from 'next/router' @@ -31,62 +31,72 @@ import { Content, TextEditor, useTextEditor } from '../editor' import { Editor } from '@tiptap/react' export function FeedCommentThread(props: { + user: User | null | undefined contract: Contract - comments: ContractComment[] + threadComments: ContractComment[] tips: CommentTipMap parentComment: ContractComment bets: Bet[] - smallAvatar?: boolean + betsByUserId: Dictionary<Bet[]> + commentsByUserId: Dictionary<ContractComment[]> }) { - const { contract, comments, bets, tips, smallAvatar, parentComment } = props + const { + user, + contract, + threadComments, + commentsByUserId, + bets, + betsByUserId, + tips, + parentComment, + } = props const [showReply, setShowReply] = useState(false) - const [replyToUser, setReplyToUser] = useState<{ - id: string - username: string - }>() - const betsByUserId = groupBy(bets, (bet) => bet.userId) - const user = useUser() - const commentsList = comments.filter( - (comment) => - parentComment.id && comment.replyToCommentId === parentComment.id - ) - commentsList.unshift(parentComment) + const [replyTo, setReplyTo] = useState<{ id: string; username: string }>() function scrollAndOpenReplyInput(comment: ContractComment) { - setReplyToUser({ id: comment.userId, username: comment.userUsername }) + setReplyTo({ id: comment.userId, username: comment.userUsername }) setShowReply(true) } return ( - <Col className={'w-full gap-3 pr-1'}> + <Col className="relative w-full items-stretch gap-3 pb-4"> <span - className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" + className="absolute top-5 left-4 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" aria-hidden="true" /> - <CommentRepliesList - contract={contract} - commentsList={commentsList} - betsByUserId={betsByUserId} - tips={tips} - smallAvatar={smallAvatar} - bets={bets} - scrollAndOpenReplyInput={scrollAndOpenReplyInput} - /> + {[parentComment].concat(threadComments).map((comment, commentIdx) => ( + <FeedComment + key={comment.id} + indent={commentIdx != 0} + contract={contract} + comment={comment} + tips={tips[comment.id]} + betsBySameUser={betsByUserId[comment.userId] ?? []} + onReplyClick={scrollAndOpenReplyInput} + probAtCreatedTime={ + contract.outcomeType === 'BINARY' + ? minBy(bets, (bet) => { + return bet.createdTime < comment.createdTime + ? comment.createdTime - bet.createdTime + : comment.createdTime + })?.probAfter + : undefined + } + /> + ))} {showReply && ( - <Col className={'-pb-2 ml-6'}> + <Col className="-pb-2 relative ml-6"> <span - className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" + className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" aria-hidden="true" /> <CommentInput contract={contract} betsByCurrentUser={(user && betsByUserId[user.id]) ?? []} - commentsByCurrentUser={comments.filter( - (c) => c.userId === user?.id - )} + commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []} parentCommentId={parentComment.id} - replyToUser={replyToUser} - parentAnswerOutcome={comments[0].answerOutcome} + replyToUser={replyTo} + parentAnswerOutcome={parentComment.answerOutcome} onSubmitComment={() => setShowReply(false)} /> </Col> @@ -95,74 +105,13 @@ export function FeedCommentThread(props: { ) } -export function CommentRepliesList(props: { - contract: Contract - commentsList: ContractComment[] - betsByUserId: Dictionary<Bet[]> - tips: CommentTipMap - scrollAndOpenReplyInput: (comment: ContractComment) => void - bets: Bet[] - treatFirstIndexEqually?: boolean - smallAvatar?: boolean -}) { - const { - contract, - commentsList, - betsByUserId, - tips, - smallAvatar, - bets, - scrollAndOpenReplyInput, - treatFirstIndexEqually, - } = props - return ( - <> - {commentsList.map((comment, commentIdx) => ( - <div - key={comment.id} - id={comment.id} - className={clsx( - 'relative', - !treatFirstIndexEqually && commentIdx === 0 ? '' : 'ml-6' - )} - > - {/*draw a gray line from the comment to the left:*/} - {(treatFirstIndexEqually || commentIdx != 0) && ( - <span - className="absolute -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" - aria-hidden="true" - /> - )} - <FeedComment - contract={contract} - comment={comment} - tips={tips[comment.id]} - betsBySameUser={betsByUserId[comment.userId] ?? []} - onReplyClick={scrollAndOpenReplyInput} - probAtCreatedTime={ - contract.outcomeType === 'BINARY' - ? minBy(bets, (bet) => { - return bet.createdTime < comment.createdTime - ? comment.createdTime - bet.createdTime - : comment.createdTime - })?.probAfter - : undefined - } - smallAvatar={smallAvatar} - /> - </div> - ))} - </> - ) -} - export function FeedComment(props: { contract: Contract comment: ContractComment tips: CommentTips betsBySameUser: Bet[] + indent?: boolean probAtCreatedTime?: number - smallAvatar?: boolean onReplyClick?: (comment: ContractComment) => void }) { const { @@ -170,6 +119,7 @@ export function FeedComment(props: { comment, tips, betsBySameUser, + indent, probAtCreatedTime, onReplyClick, } = props @@ -203,19 +153,23 @@ export function FeedComment(props: { return ( <Row + id={comment.id} className={clsx( - 'flex space-x-1.5 sm:space-x-3', - highlighted ? `-m-1 rounded bg-indigo-500/[0.2] p-2` : '' + 'relative', + indent ? 'ml-6' : '', + highlighted ? `-m-1.5 rounded bg-indigo-500/[0.2] p-1.5` : '' )} > - <Avatar - className={'ml-1'} - size={'sm'} - username={userUsername} - avatarUrl={userAvatarUrl} - /> - <div className="min-w-0 flex-1"> - <div className="mt-0.5 pl-0.5 text-sm text-gray-500"> + {/*draw a gray line from the comment to the left:*/} + {indent ? ( + <span + className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200" + aria-hidden="true" + /> + ) : null} + <Avatar size="sm" username={userUsername} avatarUrl={userAvatarUrl} /> + <div className="ml-1.5 min-w-0 flex-1 pl-0.5 sm:ml-3"> + <div className="mt-0.5 text-sm text-gray-500"> <UserLink className="text-gray-500" username={userUsername} @@ -233,21 +187,19 @@ export function FeedComment(props: { /> </> )} - <> - {bought} {money} - {contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && ( - <> - {' '} - of{' '} - <OutcomeLabel - outcome={betOutcome ? betOutcome : ''} - value={(matchedBet as any).value} - contract={contract} - truncate="short" - /> - </> - )} - </> + {bought} {money} + {contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && ( + <> + {' '} + of{' '} + <OutcomeLabel + outcome={betOutcome ? betOutcome : ''} + value={(matchedBet as any).value} + contract={contract} + truncate="short" + /> + </> + )} <CopyLinkDateTimeComponent prefix={contract.creatorUsername} slug={contract.slug} @@ -255,9 +207,11 @@ export function FeedComment(props: { elementId={comment.id} /> </div> - <div className="mt-2 text-[15px] text-gray-700"> - <Content content={content || text} smallImage /> - </div> + <Content + className="mt-2 text-[15px] text-gray-700" + content={content || text} + smallImage + /> <Row className="mt-2 items-center gap-6 text-xs text-gray-500"> <Tipper comment={comment} tips={tips ?? {}} /> {onReplyClick && ( @@ -322,6 +276,7 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] commentsByCurrentUser: ContractComment[] + className?: string replyToUser?: { id: string; username: string } // Reply to a free response answer parentAnswerOutcome?: string @@ -333,6 +288,7 @@ export function CommentInput(props: { contract, betsByCurrentUser, commentsByCurrentUser, + className, parentAnswerOutcome, parentCommentId, replyToUser, @@ -387,60 +343,51 @@ export function CommentInput(props: { if (user?.isBannedFromPosting) return <></> return ( - <> - <Row className={'mb-2 gap-1 sm:gap-2'}> - <div className={'mt-2'}> - <Avatar - avatarUrl={user?.avatarUrl} - username={user?.username} - size={'sm'} - className={'ml-1'} - /> - </div> - <div className={'min-w-0 flex-1'}> - <div className="pl-0.5 text-sm"> - <div className="mb-1 text-gray-500"> - {mostRecentCommentableBet && ( - <BetStatusText - contract={contract} - bet={mostRecentCommentableBet} - isSelf={true} - hideOutcome={ - isNumeric || contract.outcomeType === 'FREE_RESPONSE' - } - /> - )} - {!mostRecentCommentableBet && - user && - userPosition > 0 && - !isNumeric && ( - <> - {"You're"} - <CommentStatus - outcome={outcome} - contract={contract} - prob={ - contract.outcomeType === 'BINARY' - ? getProbability(contract) - : undefined - } - /> - </> - )} - </div> - <CommentInputTextArea - editor={editor} - upload={upload} - replyToUser={replyToUser} - user={user} - submitComment={submitComment} - isSubmitting={isSubmitting} - presetId={id} + <Row className={clsx(className, 'mb-2 gap-1 sm:gap-2')}> + <Avatar + avatarUrl={user?.avatarUrl} + username={user?.username} + size="sm" + className="mt-2" + /> + <div className="min-w-0 flex-1 pl-0.5 text-sm"> + <div className="mb-1 text-gray-500"> + {mostRecentCommentableBet && ( + <BetStatusText + contract={contract} + bet={mostRecentCommentableBet} + isSelf={true} + hideOutcome={ + isNumeric || contract.outcomeType === 'FREE_RESPONSE' + } /> - </div> + )} + {!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && ( + <> + {"You're"} + <CommentStatus + outcome={outcome} + contract={contract} + prob={ + contract.outcomeType === 'BINARY' + ? getProbability(contract) + : undefined + } + /> + </> + )} </div> - </Row> - </> + <CommentInputTextArea + editor={editor} + upload={upload} + replyToUser={replyToUser} + user={user} + submitComment={submitComment} + isSubmitting={isSubmitting} + presetId={id} + /> + </div> + </Row> ) } @@ -516,23 +463,21 @@ export function CommentInputTextArea(props: { return ( <> - <div> - <TextEditor editor={editor} upload={upload}> - {user && !isSubmitting && ( - <button - className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300" - disabled={!editor || editor.isEmpty} - onClick={submit} - > - <PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" /> - </button> - )} + <TextEditor editor={editor} upload={upload}> + {user && !isSubmitting && ( + <button + className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300" + disabled={!editor || editor.isEmpty} + onClick={submit} + > + <PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" /> + </button> + )} - {isSubmitting && ( - <LoadingIndicator spinnerClassName={'border-gray-500'} /> - )} - </TextEditor> - </div> + {isSubmitting && ( + <LoadingIndicator spinnerClassName={'border-gray-500'} /> + )} + </TextEditor> <Row> {!user && ( <button @@ -557,10 +502,6 @@ function getBettorsLargestPositionBeforeTime( noShares = 0, noFloorShares = 0 - const emptyReturn = { - userPosition: 0, - outcome: '', - } const previousBets = bets.filter( (prevBet) => prevBet.createdTime < createdTime && !prevBet.isAnte ) @@ -584,7 +525,7 @@ function getBettorsLargestPositionBeforeTime( } } if (bets.length === 0) { - return emptyReturn + return { userPosition: 0, outcome: '' } } const [yesBets, noBets] = partition( diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx deleted file mode 100644 index 4a121120..00000000 --- a/web/components/feed/feed-items.tsx +++ /dev/null @@ -1,279 +0,0 @@ -// From https://tailwindui.com/components/application-ui/lists/feeds -import React from 'react' -import { - BanIcon, - CheckIcon, - LockClosedIcon, - XIcon, -} from '@heroicons/react/solid' -import clsx from 'clsx' - -import { OutcomeLabel } from '../outcome-label' -import { - Contract, - contractPath, - tradingAllowed, -} from 'web/lib/firebase/contracts' -import { BinaryResolutionOrChance } from '../contract/contract-card' -import { SiteLink } from '../site-link' -import { Col } from '../layout/col' -import { UserLink } from '../user-page' -import BetButton from '../bet-button' -import { Avatar } from '../avatar' -import { ActivityItem } from './activity-items' -import { useUser } from 'web/hooks/use-user' -import { trackClick } from 'web/lib/firebase/tracking' -import { DAY_MS } from 'common/util/time' -import NewContractBadge from '../new-contract-badge' -import { RelativeTimestamp } from '../relative-timestamp' -import { FeedAnswerCommentGroup } from 'web/components/feed/feed-answer-comment-group' -import { - FeedCommentThread, - CommentInput, -} from 'web/components/feed/feed-comments' -import { FeedBet } from 'web/components/feed/feed-bets' -import { CPMMBinaryContract, NumericContract } from 'common/contract' -import { FeedLiquidity } from './feed-liquidity' -import { BetSignUpPrompt } from '../sign-up-prompt' -import { User } from 'common/user' -import { PlayMoneyDisclaimer } from '../play-money-disclaimer' -import { contractMetrics } from 'common/contract-details' - -export function FeedItems(props: { - contract: Contract - items: ActivityItem[] - className?: string - betRowClassName?: string - user: User | null | undefined -}) { - const { contract, items, className, betRowClassName, user } = props - const { outcomeType } = contract - - return ( - <div className={clsx('flow-root', className)}> - <div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}> - {items.map((item, activityItemIdx) => ( - <div key={item.id} className={'relative pb-4'}> - {activityItemIdx !== items.length - 1 || - item.type === 'answergroup' ? ( - <span - className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200" - aria-hidden="true" - /> - ) : null} - <div className="relative flex-col items-start space-x-3"> - <FeedItem item={item} /> - </div> - </div> - ))} - </div> - - {!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={clsx('mb-2', betRowClassName)} - /> - ) - )} - </div> - ) -} - -export function FeedItem(props: { item: ActivityItem }) { - const { item } = props - - switch (item.type) { - case 'question': - return <FeedQuestion {...item} /> - case 'description': - return <FeedDescription {...item} /> - case 'bet': - return <FeedBet {...item} /> - case 'liquidity': - return <FeedLiquidity {...item} /> - case 'answergroup': - return <FeedAnswerCommentGroup {...item} /> - case 'close': - return <FeedClose {...item} /> - case 'resolve': - return <FeedResolve {...item} /> - case 'commentInput': - return <CommentInput {...item} /> - case 'commentThread': - return <FeedCommentThread {...item} /> - } -} - -export function FeedQuestion(props: { - contract: Contract - contractPath?: string -}) { - const { contract } = props - const { - creatorName, - creatorUsername, - question, - outcomeType, - volume, - createdTime, - isResolved, - } = contract - const { volumeLabel } = contractMetrics(contract) - const isBinary = outcomeType === 'BINARY' - const isNew = createdTime > Date.now() - DAY_MS && !isResolved - const user = useUser() - - return ( - <div className={'flex gap-2'}> - <Avatar - username={contract.creatorUsername} - avatarUrl={contract.creatorAvatarUrl} - /> - <div className="min-w-0 flex-1 py-1.5"> - <div className="mb-2 text-sm text-gray-500"> - <UserLink - className="text-gray-900" - name={creatorName} - username={creatorUsername} - />{' '} - asked - {/* Currently hidden on mobile; ideally we'd fit this in somewhere. */} - <div className="relative -top-2 float-right "> - {isNew || volume === 0 ? ( - <NewContractBadge /> - ) : ( - <span className="hidden text-gray-400 sm:inline"> - {volumeLabel} - </span> - )} - </div> - </div> - <Col className="items-start justify-between gap-2 sm:flex-row sm:gap-4"> - <SiteLink - href={ - props.contractPath ? props.contractPath : contractPath(contract) - } - onClick={() => user && trackClick(user.id, contract.id)} - className="text-lg text-indigo-700 sm:text-xl" - > - {question} - </SiteLink> - {isBinary && ( - <BinaryResolutionOrChance - className="items-center" - contract={contract} - /> - )} - </Col> - </div> - </div> - ) -} - -function FeedDescription(props: { contract: Contract }) { - const { contract } = props - const { creatorName, creatorUsername } = contract - - return ( - <> - <Avatar - username={contract.creatorUsername} - avatarUrl={contract.creatorAvatarUrl} - /> - <div className="min-w-0 flex-1 py-1.5"> - <div className="text-sm text-gray-500"> - <UserLink - className="text-gray-900" - name={creatorName} - username={creatorUsername} - />{' '} - created this market <RelativeTimestamp time={contract.createdTime} /> - </div> - </div> - </> - ) -} - -function OutcomeIcon(props: { outcome?: string }) { - const { outcome } = props - switch (outcome) { - case 'YES': - return <CheckIcon className="h-5 w-5 text-gray-500" aria-hidden="true" /> - case 'NO': - return <XIcon className="h-5 w-5 text-gray-500" aria-hidden="true" /> - case 'CANCEL': - return <BanIcon className="h-5 w-5 text-gray-500" aria-hidden="true" /> - default: - return <CheckIcon className="h-5 w-5 text-gray-500" aria-hidden="true" /> - } -} - -function FeedResolve(props: { contract: Contract }) { - const { contract } = props - const { creatorName, creatorUsername } = contract - - const resolution = contract.resolution || 'CANCEL' - - const resolutionValue = (contract as NumericContract).resolutionValue - - return ( - <> - <div> - <div className="relative px-1"> - <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"> - <OutcomeIcon outcome={resolution} /> - </div> - </div> - </div> - <div className="min-w-0 flex-1 py-1.5"> - <div className="text-sm text-gray-500"> - <UserLink - className="text-gray-900" - name={creatorName} - username={creatorUsername} - />{' '} - resolved this market to{' '} - <OutcomeLabel - outcome={resolution} - value={resolutionValue} - contract={contract} - truncate="long" - />{' '} - <RelativeTimestamp time={contract.resolutionTime || 0} /> - </div> - </div> - </> - ) -} - -function FeedClose(props: { contract: Contract }) { - const { contract } = props - - return ( - <> - <div> - <div className="relative px-1"> - <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"> - <LockClosedIcon - className="h-5 w-5 text-gray-500" - aria-hidden="true" - /> - </div> - </div> - </div> - <div className="min-w-0 flex-1 py-1.5"> - <div className="text-sm text-gray-500"> - Trading closed in this market{' '} - <RelativeTimestamp time={contract.closeTime || 0} /> - </div> - </div> - </> - ) -} diff --git a/web/components/feed/feed-liquidity.tsx b/web/components/feed/feed-liquidity.tsx index 0ed06046..3a9ffdeb 100644 --- a/web/components/feed/feed-liquidity.tsx +++ b/web/components/feed/feed-liquidity.tsx @@ -3,7 +3,6 @@ import { User } from 'common/user' import { useUser, useUserById } from 'web/hooks/use-user' import { Row } from 'web/components/layout/row' import { Avatar, EmptyAvatar } from 'web/components/avatar' -import clsx from 'clsx' import { formatMoney } from 'common/util/format' import { RelativeTimestamp } from 'web/components/relative-timestamp' import React from 'react' @@ -11,10 +10,10 @@ import { UserLink } from '../user-page' import { LiquidityProvision } from 'common/liquidity-provision' export function FeedLiquidity(props: { + className?: string liquidity: LiquidityProvision - smallAvatar: boolean }) { - const { liquidity, smallAvatar } = props + const { liquidity } = props const { userId, createdTime } = liquidity const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01') @@ -26,21 +25,11 @@ export function FeedLiquidity(props: { return ( <> - <Row className={'flex w-full gap-2 pt-3'}> + <Row className="flex w-full gap-2 pt-3"> {isSelf ? ( - <Avatar - className={clsx(smallAvatar && 'ml-1')} - size={smallAvatar ? 'sm' : undefined} - avatarUrl={user.avatarUrl} - username={user.username} - /> + <Avatar avatarUrl={user.avatarUrl} username={user.username} /> ) : bettor ? ( - <Avatar - className={clsx(smallAvatar && 'ml-1')} - size={smallAvatar ? 'sm' : undefined} - avatarUrl={bettor.avatarUrl} - username={bettor.username} - /> + <Avatar avatarUrl={bettor.avatarUrl} username={bettor.username} /> ) : ( <div className="relative px-1"> <EmptyAvatar /> diff --git a/web/components/linkify.tsx b/web/components/linkify.tsx index b4f05165..a24ab0b6 100644 --- a/web/components/linkify.tsx +++ b/web/components/linkify.tsx @@ -1,10 +1,15 @@ +import clsx from 'clsx' import { Fragment } from 'react' import { SiteLink } from './site-link' // Return a JSX span, linkifying @username, #hashtags, and https://... // TODO: Use a markdown parser instead of rolling our own here. -export function Linkify(props: { text: string; gray?: boolean }) { - const { text, gray } = props +export function Linkify(props: { + text: string + className?: string + gray?: boolean +}) { + const { text, className, gray } = props // Replace "m1234" with "ϻ1234" // const mRegex = /(\W|^)m(\d+)/g // text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`) @@ -38,7 +43,7 @@ export function Linkify(props: { text: string; gray?: boolean }) { ) }) return ( - <span className="break-anywhere"> + <span className={clsx(className, 'break-anywhere')}> {text.split(regex).map((part, i) => ( <Fragment key={i}> {part}