De-feedify contract tab contents (#808)
* De-feedify contract bets list * De-feedify contract comments lists * Clean up a bunch of duplicated work in the comments list stuff * Remove wrapper markup from comment replies list * Fix sort order on comments I broke * Kill now unhelpful `CommentRepliesList` wrapper component * More random cleanup * More cleanup and fix some styling I had broken * Make bet calculations less wrong * Keep up to date with master * Make copy link component copy better URL * Make highlighted comments align properly * Make user header left align with content on comments * Fix some free response UI stuff up
This commit is contained in:
parent
1e3a0ca3d9
commit
7debc4925e
|
@ -11,7 +11,6 @@ import { AnswerItem } from './answer-item'
|
||||||
import { CreateAnswerPanel } from './create-answer-panel'
|
import { CreateAnswerPanel } from './create-answer-panel'
|
||||||
import { AnswerResolvePanel } from './answer-resolve-panel'
|
import { AnswerResolvePanel } from './answer-resolve-panel'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
import { ActivityItem } from '../feed/activity-items'
|
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { getOutcomeProbability } from 'common/calculate'
|
import { getOutcomeProbability } from 'common/calculate'
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
|
@ -176,7 +175,6 @@ function getAnswerItems(
|
||||||
type: 'answer' as const,
|
type: 'answer' as const,
|
||||||
contract,
|
contract,
|
||||||
answer,
|
answer,
|
||||||
items: [] as ActivityItem[],
|
|
||||||
user,
|
user,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -186,7 +184,6 @@ function getAnswerItems(
|
||||||
function OpenAnswer(props: {
|
function OpenAnswer(props: {
|
||||||
contract: FreeResponseContract | MultipleChoiceContract
|
contract: FreeResponseContract | MultipleChoiceContract
|
||||||
answer: Answer
|
answer: Answer
|
||||||
items: ActivityItem[]
|
|
||||||
type: string
|
type: string
|
||||||
}) {
|
}) {
|
||||||
const { answer, contract } = props
|
const { answer, contract } = props
|
||||||
|
|
|
@ -394,13 +394,11 @@ export function BetsSummary(props: {
|
||||||
const { hasShares, invested, profitPercent, payout, profit, totalShares } =
|
const { hasShares, invested, profitPercent, payout, profit, totalShares } =
|
||||||
getContractBetMetrics(contract, bets)
|
getContractBetMetrics(contract, bets)
|
||||||
|
|
||||||
const excludeSalesAndAntes = bets.filter(
|
const excludeSales = bets.filter((b) => !b.isSold && !b.sale)
|
||||||
(b) => !b.isAnte && !b.isSold && !b.sale
|
const yesWinnings = sumBy(excludeSales, (bet) =>
|
||||||
)
|
|
||||||
const yesWinnings = sumBy(excludeSalesAndAntes, (bet) =>
|
|
||||||
calculatePayout(contract, bet, 'YES')
|
calculatePayout(contract, bet, 'YES')
|
||||||
)
|
)
|
||||||
const noWinnings = sumBy(excludeSalesAndAntes, (bet) =>
|
const noWinnings = sumBy(excludeSales, (bet) =>
|
||||||
calculatePayout(contract, bet, 'NO')
|
calculatePayout(contract, bet, 'NO')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,6 @@ export function ContractTopTrades(props: {
|
||||||
comment={commentsById[topCommentId]}
|
comment={commentsById[topCommentId]}
|
||||||
tips={tips[topCommentId]}
|
tips={tips[topCommentId]}
|
||||||
betsBySameUser={[betsById[topCommentId]]}
|
betsBySameUser={[betsById[topCommentId]]}
|
||||||
smallAvatar={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-sm text-gray-500">
|
<div className="mt-2 text-sm text-gray-500">
|
||||||
|
@ -123,12 +122,7 @@ export function ContractTopTrades(props: {
|
||||||
<>
|
<>
|
||||||
<Title text="💸 Smartest money" className="!mt-0" />
|
<Title text="💸 Smartest money" className="!mt-0" />
|
||||||
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
<div className="relative flex items-start space-x-3 rounded-md bg-gray-50 px-2 py-4">
|
||||||
<FeedBet
|
<FeedBet contract={contract} bet={betsById[topBetId]} />
|
||||||
contract={contract}
|
|
||||||
bet={betsById[topBetId]}
|
|
||||||
hideOutcome={false}
|
|
||||||
smallAvatar={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-sm text-gray-500">
|
<div className="mt-2 text-sm text-gray-500">
|
||||||
{topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}!
|
{topBettor?.name} made {formatMoney(profitById[topBetId] || 0)}!
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract, CPMMBinaryContract } from 'common/contract'
|
||||||
import { ContractComment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import { User } from 'common/user'
|
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 { 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 { useBets } from 'web/hooks/use-bets'
|
||||||
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'
|
||||||
|
|
||||||
export function ContractTabs(props: {
|
export function ContractTabs(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -18,68 +27,69 @@ export function ContractTabs(props: {
|
||||||
comments: ContractComment[]
|
comments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
}) {
|
}) {
|
||||||
const { contract, user, bets, tips } = props
|
const { contract, user, tips } = props
|
||||||
const { outcomeType } = contract
|
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(
|
const visibleBets = bets.filter(
|
||||||
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
|
(bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
|
||||||
)
|
)
|
||||||
|
const visibleLps = lps.filter((l) => !l.isAnte && l.amount > 0)
|
||||||
const liquidityProvisions =
|
|
||||||
useLiquidity(contract.id)?.filter((l) => !l.isAnte && l.amount > 0) ?? []
|
|
||||||
|
|
||||||
// Load comments here, so the badge count will be correct
|
// Load comments here, so the badge count will be correct
|
||||||
const updatedComments = useComments(contract.id)
|
const updatedComments = useComments(contract.id)
|
||||||
const comments = updatedComments ?? props.comments
|
const comments = updatedComments ?? props.comments
|
||||||
|
|
||||||
const betActivity = (
|
const betActivity = (
|
||||||
<ContractActivity
|
<ContractBetsActivity
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={visibleBets}
|
||||||
liquidityProvisions={liquidityProvisions}
|
lps={visibleLps}
|
||||||
comments={comments}
|
|
||||||
tips={tips}
|
|
||||||
user={user}
|
|
||||||
mode="bets"
|
|
||||||
betRowClassName="!mt-0 xl:hidden"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const commentActivity = (
|
const generalBets = outcomeType === 'FREE_RESPONSE' ? [] : visibleBets
|
||||||
<>
|
const generalComments = comments.filter(
|
||||||
<ContractActivity
|
(comment) =>
|
||||||
contract={contract}
|
comment.answerOutcome === undefined &&
|
||||||
bets={bets}
|
(outcomeType === 'FREE_RESPONSE' ? comment.betId === undefined : true)
|
||||||
liquidityProvisions={liquidityProvisions}
|
)
|
||||||
comments={comments}
|
|
||||||
tips={tips}
|
const commentActivity =
|
||||||
user={user}
|
outcomeType === 'FREE_RESPONSE' ? (
|
||||||
mode={
|
<>
|
||||||
contract.outcomeType === 'FREE_RESPONSE'
|
<FreeResponseContractCommentsActivity
|
||||||
? 'free-response-comment-answer-groups'
|
contract={contract}
|
||||||
: 'comments'
|
bets={visibleBets}
|
||||||
}
|
comments={comments}
|
||||||
betRowClassName="!mt-0 xl:hidden"
|
tips={tips}
|
||||||
/>
|
user={user}
|
||||||
{outcomeType === 'FREE_RESPONSE' && (
|
/>
|
||||||
<Col className={'mt-8 flex w-full '}>
|
<Col className={'mt-8 flex w-full '}>
|
||||||
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div>
|
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div>
|
||||||
<div className={'mb-4 w-full border-b border-gray-200'} />
|
<div className={'mb-4 w-full border-b border-gray-200'} />
|
||||||
<ContractActivity
|
<ContractCommentsActivity
|
||||||
contract={contract}
|
contract={contract}
|
||||||
bets={bets}
|
bets={generalBets}
|
||||||
liquidityProvisions={liquidityProvisions}
|
comments={generalComments}
|
||||||
comments={comments}
|
|
||||||
tips={tips}
|
tips={tips}
|
||||||
user={user}
|
user={user}
|
||||||
mode={'comments'}
|
|
||||||
betRowClassName="!mt-0 xl:hidden"
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
</>
|
||||||
</>
|
) : (
|
||||||
)
|
<ContractCommentsActivity
|
||||||
|
contract={contract}
|
||||||
|
bets={visibleBets}
|
||||||
|
comments={comments}
|
||||||
|
tips={tips}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
const yourTrades = (
|
const yourTrades = (
|
||||||
<div>
|
<div>
|
||||||
|
@ -96,19 +106,39 @@ export function ContractTabs(props: {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<>
|
||||||
currentPageForAnalytics={'contract'}
|
<Tabs
|
||||||
tabs={[
|
currentPageForAnalytics={'contract'}
|
||||||
{
|
tabs={[
|
||||||
title: 'Comments',
|
{
|
||||||
content: commentActivity,
|
title: 'Comments',
|
||||||
badge: `${comments.length}`,
|
content: commentActivity,
|
||||||
},
|
badge: `${comments.length}`,
|
||||||
{ title: 'Bets', content: betActivity, badge: `${visibleBets.length}` },
|
},
|
||||||
...(!user || !userBets?.length
|
{
|
||||||
? []
|
title: 'Bets',
|
||||||
: [{ title: 'Your bets', content: yourTrades }]),
|
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"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,9 +236,10 @@ const useUploadMutation = (editor: Editor | null) =>
|
||||||
|
|
||||||
export function RichContent(props: {
|
export function RichContent(props: {
|
||||||
content: JSONContent | string
|
content: JSONContent | string
|
||||||
|
className?: string
|
||||||
smallImage?: boolean
|
smallImage?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { content, smallImage } = props
|
const { className, content, smallImage } = props
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editorProps: { attributes: { class: proseClass } },
|
editorProps: { attributes: { class: proseClass } },
|
||||||
extensions: [
|
extensions: [
|
||||||
|
@ -254,19 +255,24 @@ export function RichContent(props: {
|
||||||
})
|
})
|
||||||
useEffect(() => void editor?.commands?.setContent(content), [editor, content])
|
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
|
// backwards compatibility: we used to store content as strings
|
||||||
export function Content(props: {
|
export function Content(props: {
|
||||||
content: JSONContent | string
|
content: JSONContent | string
|
||||||
|
className?: string
|
||||||
smallImage?: boolean
|
smallImage?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { content } = props
|
const { className, content } = props
|
||||||
return typeof content === 'string' ? (
|
return typeof content === 'string' ? (
|
||||||
<div className="whitespace-pre-line font-light leading-relaxed">
|
<Linkify
|
||||||
<Linkify text={content} />
|
className={clsx(
|
||||||
</div>
|
className,
|
||||||
|
'whitespace-pre-line font-light leading-relaxed'
|
||||||
|
)}
|
||||||
|
text={content}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RichContent {...props} />
|
<RichContent {...props} />
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -1,55 +1,144 @@
|
||||||
import { Contract } from 'web/lib/firebase/contracts'
|
import { Contract, FreeResponseContract } from 'common/contract'
|
||||||
import { ContractComment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
|
import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
import { useBets } from 'web/hooks/use-bets'
|
import { getOutcomeProbability } from 'common/calculate'
|
||||||
import { getSpecificContractActivityItems } from './activity-items'
|
import { FeedBet } from './feed-bets'
|
||||||
import { FeedItems } from './feed-items'
|
import { FeedLiquidity } from './feed-liquidity'
|
||||||
|
import { FeedAnswerCommentGroup } from './feed-answer-comment-group'
|
||||||
|
import { FeedCommentThread, CommentInput } from './feed-comments'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { useContractWithPreload } from 'web/hooks/use-contract'
|
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
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
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: ContractComment[]
|
lps: LiquidityProvision[]
|
||||||
liquidityProvisions: LiquidityProvision[]
|
|
||||||
tips: CommentTipMap
|
|
||||||
user: User | null | undefined
|
|
||||||
mode: 'comments' | 'bets' | 'free-response-comment-answer-groups'
|
|
||||||
contractPath?: string
|
|
||||||
className?: string
|
|
||||||
betRowClassName?: string
|
|
||||||
}) {
|
}) {
|
||||||
const { user, mode, tips, className, betRowClassName, liquidityProvisions } =
|
const { contract, bets, lps } = props
|
||||||
props
|
|
||||||
|
|
||||||
const contract = useContractWithPreload(props.contract) ?? props.contract
|
const items = [
|
||||||
const comments = props.comments
|
...bets.map((bet) => ({
|
||||||
const updatedBets = useBets(contract.id, {
|
type: 'bet' as const,
|
||||||
filterChallenges: false,
|
id: bet.id + '-' + bet.isSold,
|
||||||
filterRedemptions: true,
|
bet,
|
||||||
})
|
})),
|
||||||
const bets = (updatedBets ?? props.bets).filter(
|
...lps.map((lp) => ({
|
||||||
(bet) => !bet.isRedemption && bet.amount !== 0
|
type: 'liquidity' as const,
|
||||||
)
|
id: lp.id,
|
||||||
const items = getSpecificContractActivityItems(
|
lp,
|
||||||
contract,
|
})),
|
||||||
bets,
|
]
|
||||||
comments,
|
|
||||||
liquidityProvisions,
|
const sortedItems = sortBy(items, (item) =>
|
||||||
tips,
|
item.type === 'bet'
|
||||||
user,
|
? -item.bet.createdTime
|
||||||
{ mode }
|
: item.type === 'liquidity'
|
||||||
|
? -item.lp.createdTime
|
||||||
|
: undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeedItems
|
<Col className="gap-4">
|
||||||
contract={contract}
|
{sortedItems.map((item) =>
|
||||||
items={items}
|
item.type === 'bet' ? (
|
||||||
className={className}
|
<FeedBet key={item.id} contract={contract} bet={item.bet} />
|
||||||
betRowClassName={betRowClassName}
|
) : (
|
||||||
user={user}
|
<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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
|
||||||
import { copyToClipboard } from 'web/lib/util/copy'
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
import { DateTimeTooltip } from 'web/components/datetime-tooltip'
|
import { DateTimeTooltip } from 'web/components/datetime-tooltip'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
@ -21,9 +20,10 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const elementLocation = `https://${ENV_CONFIG.domain}/${prefix}/${slug}#${elementId}`
|
const commentUrl = new URL(window.location.href)
|
||||||
|
commentUrl.pathname = `/${prefix}/${slug}`
|
||||||
copyToClipboard(elementLocation)
|
commentUrl.hash = elementId
|
||||||
|
copyToClipboard(commentUrl.toString())
|
||||||
setShowToast(true)
|
setShowToast(true)
|
||||||
setTimeout(() => setShowToast(false), 2000)
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { Bet } from 'common/bet'
|
import { Bet } from 'common/bet'
|
||||||
|
import { FreeResponseContract } from 'common/contract'
|
||||||
import { ContractComment } from 'common/comment'
|
import { ContractComment } from 'common/comment'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
|
@ -10,25 +11,34 @@ import { Linkify } from 'web/components/linkify'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import {
|
||||||
CommentInput,
|
CommentInput,
|
||||||
CommentRepliesList,
|
FeedComment,
|
||||||
getMostRecentCommentableBet,
|
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'
|
||||||
import { groupBy } from 'lodash'
|
import { Dictionary } from 'lodash'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { useEvent } from 'web/hooks/use-event'
|
import { useEvent } from 'web/hooks/use-event'
|
||||||
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
||||||
|
|
||||||
export function FeedAnswerCommentGroup(props: {
|
export function FeedAnswerCommentGroup(props: {
|
||||||
contract: any
|
contract: FreeResponseContract
|
||||||
user: User | undefined | null
|
user: User | undefined | null
|
||||||
answer: Answer
|
answer: Answer
|
||||||
comments: ContractComment[]
|
answerComments: ContractComment[]
|
||||||
tips: CommentTipMap
|
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 { username, avatarUrl, name, text } = answer
|
||||||
|
|
||||||
const [replyToUser, setReplyToUser] =
|
const [replyToUser, setReplyToUser] =
|
||||||
|
@ -38,11 +48,6 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const answerElementId = `answer-${answer.id}`
|
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 betsByCurrentUser = (user && betsByUserId[user.id]) ?? []
|
||||||
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
const commentsByCurrentUser = (user && commentsByUserId[user.id]) ?? []
|
||||||
const isFreeResponseContractPage = !!commentsByCurrentUser
|
const isFreeResponseContractPage = !!commentsByCurrentUser
|
||||||
|
@ -101,10 +106,13 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
}, [answerElementId, router.asPath])
|
}, [answerElementId, router.asPath])
|
||||||
|
|
||||||
return (
|
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
|
<Row
|
||||||
className={clsx(
|
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` : ''
|
highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : ''
|
||||||
)}
|
)}
|
||||||
id={answerElementId}
|
id={answerElementId}
|
||||||
|
@ -150,21 +158,23 @@ export function FeedAnswerCommentGroup(props: {
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<CommentRepliesList
|
<Col className="gap-3 pl-1">
|
||||||
contract={contract}
|
{answerComments.map((comment) => (
|
||||||
commentsList={commentsList}
|
<FeedComment
|
||||||
betsByUserId={betsByUserId}
|
key={comment.id}
|
||||||
smallAvatar={true}
|
indent={true}
|
||||||
bets={bets}
|
contract={contract}
|
||||||
tips={tips}
|
comment={comment}
|
||||||
scrollAndOpenReplyInput={scrollAndOpenReplyInput}
|
tips={tips[comment.id]}
|
||||||
treatFirstIndexEqually={true}
|
betsBySameUser={betsByUserId[comment.userId] ?? []}
|
||||||
/>
|
onReplyClick={scrollAndOpenReplyInput}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
{showReply && (
|
{showReply && (
|
||||||
<div className={'ml-6'}>
|
<div className={'relative ml-7'}>
|
||||||
<span
|
<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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<CommentInput
|
<CommentInput
|
||||||
|
|
|
@ -16,13 +16,8 @@ import { SiteLink } from 'web/components/site-link'
|
||||||
import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
|
import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges'
|
||||||
import { Challenge } from 'common/challenge'
|
import { Challenge } from 'common/challenge'
|
||||||
|
|
||||||
export function FeedBet(props: {
|
export function FeedBet(props: { contract: Contract; bet: Bet }) {
|
||||||
contract: Contract
|
const { contract, bet } = props
|
||||||
bet: Bet
|
|
||||||
hideOutcome: boolean
|
|
||||||
smallAvatar: boolean
|
|
||||||
}) {
|
|
||||||
const { contract, bet, hideOutcome, smallAvatar } = props
|
|
||||||
const { userId, createdTime } = bet
|
const { userId, createdTime } = bet
|
||||||
|
|
||||||
const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01')
|
const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01')
|
||||||
|
@ -33,21 +28,11 @@ export function FeedBet(props: {
|
||||||
const isSelf = user?.id === userId
|
const isSelf = user?.id === userId
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={'flex w-full items-center gap-2 pt-3'}>
|
<Row className="items-center gap-2 pt-3">
|
||||||
{isSelf ? (
|
{isSelf ? (
|
||||||
<Avatar
|
<Avatar avatarUrl={user.avatarUrl} username={user.username} />
|
||||||
className={clsx(smallAvatar && 'ml-1')}
|
|
||||||
size={smallAvatar ? 'sm' : undefined}
|
|
||||||
avatarUrl={user.avatarUrl}
|
|
||||||
username={user.username}
|
|
||||||
/>
|
|
||||||
) : bettor ? (
|
) : bettor ? (
|
||||||
<Avatar
|
<Avatar avatarUrl={bettor.avatarUrl} username={bettor.username} />
|
||||||
className={clsx(smallAvatar && 'ml-1')}
|
|
||||||
size={smallAvatar ? 'sm' : undefined}
|
|
||||||
avatarUrl={bettor.avatarUrl}
|
|
||||||
username={bettor.username}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyAvatar className="mx-1" />
|
<EmptyAvatar className="mx-1" />
|
||||||
)}
|
)}
|
||||||
|
@ -56,7 +41,6 @@ export function FeedBet(props: {
|
||||||
contract={contract}
|
contract={contract}
|
||||||
isSelf={isSelf}
|
isSelf={isSelf}
|
||||||
bettor={bettor}
|
bettor={bettor}
|
||||||
hideOutcome={hideOutcome}
|
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ContractComment } from 'common/comment'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import React, { useEffect, useState } from 'react'
|
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 { useUser } from 'web/hooks/use-user'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
@ -31,62 +31,72 @@ import { Content, TextEditor, useTextEditor } from '../editor'
|
||||||
import { Editor } from '@tiptap/react'
|
import { Editor } from '@tiptap/react'
|
||||||
|
|
||||||
export function FeedCommentThread(props: {
|
export function FeedCommentThread(props: {
|
||||||
|
user: User | null | undefined
|
||||||
contract: Contract
|
contract: Contract
|
||||||
comments: ContractComment[]
|
threadComments: ContractComment[]
|
||||||
tips: CommentTipMap
|
tips: CommentTipMap
|
||||||
parentComment: ContractComment
|
parentComment: ContractComment
|
||||||
bets: Bet[]
|
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 [showReply, setShowReply] = useState(false)
|
||||||
const [replyToUser, setReplyToUser] = useState<{
|
const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
|
||||||
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)
|
|
||||||
|
|
||||||
function scrollAndOpenReplyInput(comment: ContractComment) {
|
function scrollAndOpenReplyInput(comment: ContractComment) {
|
||||||
setReplyToUser({ id: comment.userId, username: comment.userUsername })
|
setReplyTo({ id: comment.userId, username: comment.userUsername })
|
||||||
setShowReply(true)
|
setShowReply(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={'w-full gap-3 pr-1'}>
|
<Col className="relative w-full items-stretch gap-3 pb-4">
|
||||||
<span
|
<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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<CommentRepliesList
|
{[parentComment].concat(threadComments).map((comment, commentIdx) => (
|
||||||
contract={contract}
|
<FeedComment
|
||||||
commentsList={commentsList}
|
key={comment.id}
|
||||||
betsByUserId={betsByUserId}
|
indent={commentIdx != 0}
|
||||||
tips={tips}
|
contract={contract}
|
||||||
smallAvatar={smallAvatar}
|
comment={comment}
|
||||||
bets={bets}
|
tips={tips[comment.id]}
|
||||||
scrollAndOpenReplyInput={scrollAndOpenReplyInput}
|
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 && (
|
{showReply && (
|
||||||
<Col className={'-pb-2 ml-6'}>
|
<Col className="-pb-2 relative ml-6">
|
||||||
<span
|
<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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<CommentInput
|
<CommentInput
|
||||||
contract={contract}
|
contract={contract}
|
||||||
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
||||||
commentsByCurrentUser={comments.filter(
|
commentsByCurrentUser={(user && commentsByUserId[user.id]) ?? []}
|
||||||
(c) => c.userId === user?.id
|
|
||||||
)}
|
|
||||||
parentCommentId={parentComment.id}
|
parentCommentId={parentComment.id}
|
||||||
replyToUser={replyToUser}
|
replyToUser={replyTo}
|
||||||
parentAnswerOutcome={comments[0].answerOutcome}
|
parentAnswerOutcome={parentComment.answerOutcome}
|
||||||
onSubmitComment={() => setShowReply(false)}
|
onSubmitComment={() => setShowReply(false)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</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: {
|
export function FeedComment(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
comment: ContractComment
|
comment: ContractComment
|
||||||
tips: CommentTips
|
tips: CommentTips
|
||||||
betsBySameUser: Bet[]
|
betsBySameUser: Bet[]
|
||||||
|
indent?: boolean
|
||||||
probAtCreatedTime?: number
|
probAtCreatedTime?: number
|
||||||
smallAvatar?: boolean
|
|
||||||
onReplyClick?: (comment: ContractComment) => void
|
onReplyClick?: (comment: ContractComment) => void
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
|
@ -170,6 +119,7 @@ export function FeedComment(props: {
|
||||||
comment,
|
comment,
|
||||||
tips,
|
tips,
|
||||||
betsBySameUser,
|
betsBySameUser,
|
||||||
|
indent,
|
||||||
probAtCreatedTime,
|
probAtCreatedTime,
|
||||||
onReplyClick,
|
onReplyClick,
|
||||||
} = props
|
} = props
|
||||||
|
@ -203,19 +153,23 @@ export function FeedComment(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
|
id={comment.id}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex space-x-1.5 sm:space-x-3',
|
'relative',
|
||||||
highlighted ? `-m-1 rounded bg-indigo-500/[0.2] p-2` : ''
|
indent ? 'ml-6' : '',
|
||||||
|
highlighted ? `-m-1.5 rounded bg-indigo-500/[0.2] p-1.5` : ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Avatar
|
{/*draw a gray line from the comment to the left:*/}
|
||||||
className={'ml-1'}
|
{indent ? (
|
||||||
size={'sm'}
|
<span
|
||||||
username={userUsername}
|
className="absolute -left-1 -ml-[1px] mt-[0.8rem] h-2 w-0.5 rotate-90 bg-gray-200"
|
||||||
avatarUrl={userAvatarUrl}
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
) : null}
|
||||||
<div className="mt-0.5 pl-0.5 text-sm text-gray-500">
|
<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
|
<UserLink
|
||||||
className="text-gray-500"
|
className="text-gray-500"
|
||||||
username={userUsername}
|
username={userUsername}
|
||||||
|
@ -233,21 +187,19 @@ export function FeedComment(props: {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<>
|
{bought} {money}
|
||||||
{bought} {money}
|
{contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && (
|
||||||
{contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && (
|
<>
|
||||||
<>
|
{' '}
|
||||||
{' '}
|
of{' '}
|
||||||
of{' '}
|
<OutcomeLabel
|
||||||
<OutcomeLabel
|
outcome={betOutcome ? betOutcome : ''}
|
||||||
outcome={betOutcome ? betOutcome : ''}
|
value={(matchedBet as any).value}
|
||||||
value={(matchedBet as any).value}
|
contract={contract}
|
||||||
contract={contract}
|
truncate="short"
|
||||||
truncate="short"
|
/>
|
||||||
/>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
</>
|
|
||||||
<CopyLinkDateTimeComponent
|
<CopyLinkDateTimeComponent
|
||||||
prefix={contract.creatorUsername}
|
prefix={contract.creatorUsername}
|
||||||
slug={contract.slug}
|
slug={contract.slug}
|
||||||
|
@ -255,9 +207,11 @@ export function FeedComment(props: {
|
||||||
elementId={comment.id}
|
elementId={comment.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-[15px] text-gray-700">
|
<Content
|
||||||
<Content content={content || text} smallImage />
|
className="mt-2 text-[15px] text-gray-700"
|
||||||
</div>
|
content={content || text}
|
||||||
|
smallImage
|
||||||
|
/>
|
||||||
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
|
||||||
<Tipper comment={comment} tips={tips ?? {}} />
|
<Tipper comment={comment} tips={tips ?? {}} />
|
||||||
{onReplyClick && (
|
{onReplyClick && (
|
||||||
|
@ -322,6 +276,7 @@ export function CommentInput(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
betsByCurrentUser: Bet[]
|
betsByCurrentUser: Bet[]
|
||||||
commentsByCurrentUser: ContractComment[]
|
commentsByCurrentUser: ContractComment[]
|
||||||
|
className?: string
|
||||||
replyToUser?: { id: string; username: string }
|
replyToUser?: { id: string; username: string }
|
||||||
// Reply to a free response answer
|
// Reply to a free response answer
|
||||||
parentAnswerOutcome?: string
|
parentAnswerOutcome?: string
|
||||||
|
@ -333,6 +288,7 @@ export function CommentInput(props: {
|
||||||
contract,
|
contract,
|
||||||
betsByCurrentUser,
|
betsByCurrentUser,
|
||||||
commentsByCurrentUser,
|
commentsByCurrentUser,
|
||||||
|
className,
|
||||||
parentAnswerOutcome,
|
parentAnswerOutcome,
|
||||||
parentCommentId,
|
parentCommentId,
|
||||||
replyToUser,
|
replyToUser,
|
||||||
|
@ -387,60 +343,51 @@ export function CommentInput(props: {
|
||||||
if (user?.isBannedFromPosting) return <></>
|
if (user?.isBannedFromPosting) return <></>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Row className={clsx(className, 'mb-2 gap-1 sm:gap-2')}>
|
||||||
<Row className={'mb-2 gap-1 sm:gap-2'}>
|
<Avatar
|
||||||
<div className={'mt-2'}>
|
avatarUrl={user?.avatarUrl}
|
||||||
<Avatar
|
username={user?.username}
|
||||||
avatarUrl={user?.avatarUrl}
|
size="sm"
|
||||||
username={user?.username}
|
className="mt-2"
|
||||||
size={'sm'}
|
/>
|
||||||
className={'ml-1'}
|
<div className="min-w-0 flex-1 pl-0.5 text-sm">
|
||||||
/>
|
<div className="mb-1 text-gray-500">
|
||||||
</div>
|
{mostRecentCommentableBet && (
|
||||||
<div className={'min-w-0 flex-1'}>
|
<BetStatusText
|
||||||
<div className="pl-0.5 text-sm">
|
contract={contract}
|
||||||
<div className="mb-1 text-gray-500">
|
bet={mostRecentCommentableBet}
|
||||||
{mostRecentCommentableBet && (
|
isSelf={true}
|
||||||
<BetStatusText
|
hideOutcome={
|
||||||
contract={contract}
|
isNumeric || contract.outcomeType === 'FREE_RESPONSE'
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
{!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && (
|
||||||
|
<>
|
||||||
|
{"You're"}
|
||||||
|
<CommentStatus
|
||||||
|
outcome={outcome}
|
||||||
|
contract={contract}
|
||||||
|
prob={
|
||||||
|
contract.outcomeType === 'BINARY'
|
||||||
|
? getProbability(contract)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<TextEditor editor={editor} upload={upload}>
|
||||||
<TextEditor editor={editor} upload={upload}>
|
{user && !isSubmitting && (
|
||||||
{user && !isSubmitting && (
|
<button
|
||||||
<button
|
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
|
||||||
className="btn btn-ghost btn-sm px-2 disabled:bg-inherit disabled:text-gray-300"
|
disabled={!editor || editor.isEmpty}
|
||||||
disabled={!editor || editor.isEmpty}
|
onClick={submit}
|
||||||
onClick={submit}
|
>
|
||||||
>
|
<PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" />
|
||||||
<PaperAirplaneIcon className="m-0 h-[25px] min-w-[22px] rotate-90 p-0" />
|
</button>
|
||||||
</button>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isSubmitting && (
|
{isSubmitting && (
|
||||||
<LoadingIndicator spinnerClassName={'border-gray-500'} />
|
<LoadingIndicator spinnerClassName={'border-gray-500'} />
|
||||||
)}
|
)}
|
||||||
</TextEditor>
|
</TextEditor>
|
||||||
</div>
|
|
||||||
<Row>
|
<Row>
|
||||||
{!user && (
|
{!user && (
|
||||||
<button
|
<button
|
||||||
|
@ -557,10 +502,6 @@ function getBettorsLargestPositionBeforeTime(
|
||||||
noShares = 0,
|
noShares = 0,
|
||||||
noFloorShares = 0
|
noFloorShares = 0
|
||||||
|
|
||||||
const emptyReturn = {
|
|
||||||
userPosition: 0,
|
|
||||||
outcome: '',
|
|
||||||
}
|
|
||||||
const previousBets = bets.filter(
|
const previousBets = bets.filter(
|
||||||
(prevBet) => prevBet.createdTime < createdTime && !prevBet.isAnte
|
(prevBet) => prevBet.createdTime < createdTime && !prevBet.isAnte
|
||||||
)
|
)
|
||||||
|
@ -584,7 +525,7 @@ function getBettorsLargestPositionBeforeTime(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bets.length === 0) {
|
if (bets.length === 0) {
|
||||||
return emptyReturn
|
return { userPosition: 0, outcome: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const [yesBets, noBets] = partition(
|
const [yesBets, noBets] = partition(
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ import { User } from 'common/user'
|
||||||
import { useUser, useUserById } from 'web/hooks/use-user'
|
import { useUser, useUserById } from 'web/hooks/use-user'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||||
import clsx from 'clsx'
|
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
import { RelativeTimestamp } from 'web/components/relative-timestamp'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -11,10 +10,10 @@ import { UserLink } from '../user-page'
|
||||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
import { LiquidityProvision } from 'common/liquidity-provision'
|
||||||
|
|
||||||
export function FeedLiquidity(props: {
|
export function FeedLiquidity(props: {
|
||||||
|
className?: string
|
||||||
liquidity: LiquidityProvision
|
liquidity: LiquidityProvision
|
||||||
smallAvatar: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const { liquidity, smallAvatar } = props
|
const { liquidity } = props
|
||||||
const { userId, createdTime } = liquidity
|
const { userId, createdTime } = liquidity
|
||||||
|
|
||||||
const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01')
|
const isBeforeJune2022 = dayjs(createdTime).isBefore('2022-06-01')
|
||||||
|
@ -26,21 +25,11 @@ export function FeedLiquidity(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row className={'flex w-full gap-2 pt-3'}>
|
<Row className="flex w-full gap-2 pt-3">
|
||||||
{isSelf ? (
|
{isSelf ? (
|
||||||
<Avatar
|
<Avatar avatarUrl={user.avatarUrl} username={user.username} />
|
||||||
className={clsx(smallAvatar && 'ml-1')}
|
|
||||||
size={smallAvatar ? 'sm' : undefined}
|
|
||||||
avatarUrl={user.avatarUrl}
|
|
||||||
username={user.username}
|
|
||||||
/>
|
|
||||||
) : bettor ? (
|
) : bettor ? (
|
||||||
<Avatar
|
<Avatar avatarUrl={bettor.avatarUrl} username={bettor.username} />
|
||||||
className={clsx(smallAvatar && 'ml-1')}
|
|
||||||
size={smallAvatar ? 'sm' : undefined}
|
|
||||||
avatarUrl={bettor.avatarUrl}
|
|
||||||
username={bettor.username}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="relative px-1">
|
<div className="relative px-1">
|
||||||
<EmptyAvatar />
|
<EmptyAvatar />
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { SiteLink } from './site-link'
|
import { SiteLink } from './site-link'
|
||||||
|
|
||||||
// Return a JSX span, linkifying @username, #hashtags, and https://...
|
// Return a JSX span, linkifying @username, #hashtags, and https://...
|
||||||
// TODO: Use a markdown parser instead of rolling our own here.
|
// TODO: Use a markdown parser instead of rolling our own here.
|
||||||
export function Linkify(props: { text: string; gray?: boolean }) {
|
export function Linkify(props: {
|
||||||
const { text, gray } = props
|
text: string
|
||||||
|
className?: string
|
||||||
|
gray?: boolean
|
||||||
|
}) {
|
||||||
|
const { text, className, gray } = props
|
||||||
// Replace "m1234" with "ϻ1234"
|
// Replace "m1234" with "ϻ1234"
|
||||||
// const mRegex = /(\W|^)m(\d+)/g
|
// const mRegex = /(\W|^)m(\d+)/g
|
||||||
// text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`)
|
// text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`)
|
||||||
|
@ -38,7 +43,7 @@ export function Linkify(props: { text: string; gray?: boolean }) {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<span className="break-anywhere">
|
<span className={clsx(className, 'break-anywhere')}>
|
||||||
{text.split(regex).map((part, i) => (
|
{text.split(regex).map((part, i) => (
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
{part}
|
{part}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user