Show a mini-feed of comments & bets below each FR answer (#52)
* Take out <ul> from Feed * Show a mini-feed under each FR answer * Expand row on click * Show feed item for FR answer submission * Fix build
This commit is contained in:
parent
a2c1107e10
commit
952b7be94a
|
@ -8,13 +8,12 @@ import { Col } from '../layout/col'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
import { Avatar } from '../avatar'
|
import { Avatar } from '../avatar'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { BuyButton } from '../yes-no-selector'
|
import { BuyButton } from '../yes-no-selector'
|
||||||
import { formatPercent } from '../../../common/util/format'
|
import { formatPercent } from '../../../common/util/format'
|
||||||
import { getOutcomeProbability } from '../../../common/calculate'
|
import { getOutcomeProbability } from '../../../common/calculate'
|
||||||
import { tradingAllowed } from '../../lib/firebase/contracts'
|
import { tradingAllowed } from '../../lib/firebase/contracts'
|
||||||
import { AnswerBetPanel } from './answer-bet-panel'
|
import { AnswerBetPanel } from './answer-bet-panel'
|
||||||
|
import { ContractFeed } from '../contract-feed'
|
||||||
|
|
||||||
export function AnswerItem(props: {
|
export function AnswerItem(props: {
|
||||||
answer: Answer
|
answer: Answer
|
||||||
|
@ -35,10 +34,9 @@ export function AnswerItem(props: {
|
||||||
onDeselect,
|
onDeselect,
|
||||||
} = props
|
} = props
|
||||||
const { resolution, resolutions, totalShares } = contract
|
const { resolution, resolutions, totalShares } = contract
|
||||||
const { username, avatarUrl, name, createdTime, number, text } = answer
|
const { username, avatarUrl, name, number, text } = answer
|
||||||
const isChosen = chosenProb !== undefined
|
const isChosen = chosenProb !== undefined
|
||||||
|
|
||||||
const createdDate = dayjs(createdTime).format('MMM D')
|
|
||||||
const prob = getOutcomeProbability(totalShares, answer.id)
|
const prob = getOutcomeProbability(totalShares, answer.id)
|
||||||
const roundedProb = Math.round(prob * 100)
|
const roundedProb = Math.round(prob * 100)
|
||||||
const probPercent = formatPercent(prob)
|
const probPercent = formatPercent(prob)
|
||||||
|
@ -48,41 +46,44 @@ export function AnswerItem(props: {
|
||||||
const [isBetting, setIsBetting] = useState(false)
|
const [isBetting, setIsBetting] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'p-4 sm:flex-row rounded gap-4',
|
'flex flex-col gap-4 rounded p-4 sm:flex-row',
|
||||||
wasResolvedTo
|
wasResolvedTo
|
||||||
? resolution === 'MKT'
|
? resolution === 'MKT'
|
||||||
? 'bg-blue-50 mb-2'
|
? 'mb-2 bg-blue-50'
|
||||||
: 'bg-green-50 mb-8'
|
: 'mb-8 bg-green-50'
|
||||||
: chosenProb === undefined
|
: chosenProb === undefined
|
||||||
? 'bg-gray-50'
|
? 'bg-gray-50'
|
||||||
: showChoice === 'radio'
|
: showChoice === 'radio'
|
||||||
? 'bg-green-50'
|
? 'bg-green-50'
|
||||||
: 'bg-blue-50'
|
: 'bg-blue-50',
|
||||||
|
isBetting ? '' : 'cursor-pointer hover:bg-gray-100'
|
||||||
)}
|
)}
|
||||||
|
onClick={() => !isBetting && setIsBetting(true)}
|
||||||
>
|
>
|
||||||
<Col className="gap-3 flex-1">
|
<Col className="flex-1 gap-3">
|
||||||
<div className="whitespace-pre-line break-words">{text}</div>
|
<div className="whitespace-pre-line break-words">{text}</div>
|
||||||
|
|
||||||
<Row className="text-gray-500 text-sm gap-2 items-center">
|
<Row className="items-center gap-2 text-sm text-gray-500">
|
||||||
<SiteLink className="relative" href={`/${username}`}>
|
<SiteLink className="relative" href={`/${username}`}>
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
<Avatar avatarUrl={avatarUrl} size={6} />
|
<Avatar avatarUrl={avatarUrl} size={6} />
|
||||||
<div className="truncate">{name}</div>
|
<div className="truncate">{name}</div>
|
||||||
</Row>
|
</Row>
|
||||||
</SiteLink>
|
</SiteLink>
|
||||||
|
{/* TODO: Show total pool? */}
|
||||||
<div className="">•</div>
|
|
||||||
|
|
||||||
<div className="whitespace-nowrap">
|
|
||||||
<DateTimeTooltip text="" time={contract.createdTime}>
|
|
||||||
{createdDate}
|
|
||||||
</DateTimeTooltip>
|
|
||||||
</div>
|
|
||||||
<div className="">•</div>
|
|
||||||
<div className="text-base">#{number}</div>
|
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{isBetting && (
|
||||||
|
<ContractFeed
|
||||||
|
contract={contract}
|
||||||
|
bets={[]}
|
||||||
|
comments={[]}
|
||||||
|
feedType="multi"
|
||||||
|
outcome={answer.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{isBetting ? (
|
{isBetting ? (
|
||||||
|
@ -92,11 +93,11 @@ export function AnswerItem(props: {
|
||||||
closePanel={() => setIsBetting(false)}
|
closePanel={() => setIsBetting(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Row className="self-end sm:self-start items-center gap-4 justify-end">
|
<Row className="items-center justify-end gap-4 self-end sm:self-start">
|
||||||
{!wasResolvedTo &&
|
{!wasResolvedTo &&
|
||||||
(showChoice === 'checkbox' ? (
|
(showChoice === 'checkbox' ? (
|
||||||
<input
|
<input
|
||||||
className="input input-bordered text-2xl justify-self-end w-24"
|
className="input input-bordered w-24 justify-self-end text-2xl"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={`${roundedProb}`}
|
placeholder={`${roundedProb}`}
|
||||||
maxLength={9}
|
maxLength={9}
|
||||||
|
@ -121,7 +122,7 @@ export function AnswerItem(props: {
|
||||||
))}
|
))}
|
||||||
{showChoice ? (
|
{showChoice ? (
|
||||||
<div className="form-control py-1">
|
<div className="form-control py-1">
|
||||||
<label className="cursor-pointer label gap-3">
|
<label className="label cursor-pointer gap-3">
|
||||||
<span className="">Choose this answer</span>
|
<span className="">Choose this answer</span>
|
||||||
{showChoice === 'radio' && (
|
{showChoice === 'radio' && (
|
||||||
<input
|
<input
|
||||||
|
@ -162,7 +163,7 @@ export function AnswerItem(props: {
|
||||||
<>
|
<>
|
||||||
{tradingAllowed(contract) && (
|
{tradingAllowed(contract) && (
|
||||||
<BuyButton
|
<BuyButton
|
||||||
className="justify-end self-end flex-initial btn-md !px-8"
|
className="btn-md flex-initial justify-end self-end !px-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsBetting(true)
|
setIsBetting(true)
|
||||||
}}
|
}}
|
||||||
|
@ -188,6 +189,6 @@ export function AnswerItem(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { useAdmin } from '../hooks/use-admin'
|
||||||
function FeedComment(props: {
|
function FeedComment(props: {
|
||||||
activityItem: any
|
activityItem: any
|
||||||
moreHref: string
|
moreHref: string
|
||||||
feedType: 'activity' | 'market'
|
feedType: FeedType
|
||||||
}) {
|
}) {
|
||||||
const { activityItem, moreHref, feedType } = props
|
const { activityItem, moreHref, feedType } = props
|
||||||
const { person, text, amount, outcome, createdTime } = activityItem
|
const { person, text, amount, outcome, createdTime } = activityItem
|
||||||
|
@ -65,7 +65,8 @@ function FeedComment(props: {
|
||||||
username={person.username}
|
username={person.username}
|
||||||
name={person.name}
|
name={person.name}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{bought} {money} of <OutcomeLabel outcome={outcome} />{' '}
|
{bought} {money}
|
||||||
|
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||||
<Timestamp time={createdTime} />
|
<Timestamp time={createdTime} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,8 +91,8 @@ function Timestamp(props: { time: number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeedBet(props: { activityItem: any }) {
|
function FeedBet(props: { activityItem: any; feedType: FeedType }) {
|
||||||
const { activityItem } = props
|
const { activityItem, feedType } = props
|
||||||
const { id, contractId, amount, outcome, createdTime } = activityItem
|
const { id, contractId, amount, outcome, createdTime } = activityItem
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isSelf = user?.id == activityItem.userId
|
const isSelf = user?.id == activityItem.userId
|
||||||
|
@ -122,8 +123,9 @@ function FeedBet(props: { activityItem: any }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 py-1.5">
|
<div className="min-w-0 flex-1 py-1.5">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money} of{' '}
|
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money}
|
||||||
<OutcomeLabel outcome={outcome} /> <Timestamp time={createdTime} />
|
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||||
|
<Timestamp time={createdTime} />
|
||||||
{canComment && (
|
{canComment && (
|
||||||
// Allow user to comment in an textarea if they are the creator
|
// Allow user to comment in an textarea if they are the creator
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
@ -382,6 +384,29 @@ function FeedDescription(props: { contract: Contract }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FeedAnswer(props: { contract: Contract; outcome: string }) {
|
||||||
|
const { contract, outcome } = props
|
||||||
|
const answer = contract?.answers?.[Number(outcome) - 1]
|
||||||
|
if (!answer) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Avatar username={answer.username} avatarUrl={answer.avatarUrl} />
|
||||||
|
<div className="min-w-0 flex-1 py-1.5">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<UserLink
|
||||||
|
className="text-gray-900"
|
||||||
|
name={answer.name}
|
||||||
|
username={answer.username}
|
||||||
|
/>{' '}
|
||||||
|
submitted answer <OutcomeLabel outcome={outcome} />{' '}
|
||||||
|
<Timestamp time={contract.createdTime} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function OutcomeIcon(props: { outcome?: string }) {
|
function OutcomeIcon(props: { outcome?: string }) {
|
||||||
const { outcome } = props
|
const { outcome } = props
|
||||||
switch (outcome) {
|
switch (outcome) {
|
||||||
|
@ -540,8 +565,12 @@ function groupBets(
|
||||||
return items as ActivityItem[]
|
return items as ActivityItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
function BetGroupSpan(props: {
|
||||||
const { bets, outcome } = props
|
bets: Bet[]
|
||||||
|
outcome: string
|
||||||
|
feedType: FeedType
|
||||||
|
}) {
|
||||||
|
const { bets, outcome, feedType } = props
|
||||||
|
|
||||||
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
||||||
|
|
||||||
|
@ -556,14 +585,14 @@ function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
||||||
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
||||||
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
||||||
</JoinSpans>
|
</JoinSpans>
|
||||||
of <OutcomeLabel outcome={outcome} />
|
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />{' '}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this expandable to show all grouped bets?
|
// TODO: Make this expandable to show all grouped bets?
|
||||||
function FeedBetGroup(props: { activityItem: any }) {
|
function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
|
||||||
const { activityItem } = props
|
const { activityItem, feedType } = props
|
||||||
const bets: Bet[] = activityItem.bets
|
const bets: Bet[] = activityItem.bets
|
||||||
|
|
||||||
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||||
|
@ -585,7 +614,11 @@ function FeedBetGroup(props: { activityItem: any }) {
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{outcomes.map((outcome, index) => (
|
{outcomes.map((outcome, index) => (
|
||||||
<Fragment key={outcome}>
|
<Fragment key={outcome}>
|
||||||
<BetGroupSpan outcome={outcome} bets={betGroups[outcome]} />
|
<BetGroupSpan
|
||||||
|
outcome={outcome}
|
||||||
|
bets={betGroups[outcome]}
|
||||||
|
feedType={feedType}
|
||||||
|
/>
|
||||||
{index !== outcomes.length - 1 && <br />}
|
{index !== outcomes.length - 1 && <br />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
@ -623,6 +656,18 @@ function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On 'multi' feeds, the outcome is redundant, so we hide it
|
||||||
|
function MaybeOutcomeLabel(props: { outcome: string; feedType: FeedType }) {
|
||||||
|
const { outcome, feedType } = props
|
||||||
|
return feedType === 'multi' ? null : (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
of <OutcomeLabel outcome={outcome} />
|
||||||
|
{/* TODO: Link to the correct e.g. #23 */}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Missing feed items:
|
// Missing feed items:
|
||||||
// - Bet sold?
|
// - Bet sold?
|
||||||
type ActivityItem = {
|
type ActivityItem = {
|
||||||
|
@ -637,15 +682,23 @@ type ActivityItem = {
|
||||||
| 'expand'
|
| 'expand'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FeedType =
|
||||||
|
// Main homepage/fold feed,
|
||||||
|
| 'activity'
|
||||||
|
// Comments feed on a market
|
||||||
|
| 'market'
|
||||||
|
// Grouped for a multi-category outcome
|
||||||
|
| 'multi'
|
||||||
|
|
||||||
export function ContractFeed(props: {
|
export function ContractFeed(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
bets: Bet[]
|
bets: Bet[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
|
feedType: FeedType
|
||||||
feedType: 'activity' | 'market'
|
outcome?: string // Which multi-category outcome to filter
|
||||||
betRowClassName?: string
|
betRowClassName?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, feedType, betRowClassName } = props
|
const { contract, feedType, outcome, betRowClassName } = props
|
||||||
const { id, outcomeType } = contract
|
const { id, outcomeType } = contract
|
||||||
const isBinary = outcomeType === 'BINARY'
|
const isBinary = outcomeType === 'BINARY'
|
||||||
|
|
||||||
|
@ -657,6 +710,10 @@ export function ContractFeed(props: {
|
||||||
? bets.filter((bet) => !bet.isAnte)
|
? bets.filter((bet) => !bet.isAnte)
|
||||||
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
||||||
|
|
||||||
|
if (feedType === 'multi') {
|
||||||
|
bets = bets.filter((bet) => bet.outcome === outcome)
|
||||||
|
}
|
||||||
|
|
||||||
const comments = useComments(id) ?? props.comments
|
const comments = useComments(id) ?? props.comments
|
||||||
|
|
||||||
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
const groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
||||||
|
@ -671,6 +728,10 @@ export function ContractFeed(props: {
|
||||||
if (contract.resolution) {
|
if (contract.resolution) {
|
||||||
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
allItems.push({ type: 'resolve', id: `${contract.resolutionTime}` })
|
||||||
}
|
}
|
||||||
|
if (feedType === 'multi') {
|
||||||
|
// Hack to add some more padding above the 'multi' feedType, by adding a null item
|
||||||
|
allItems.unshift({ type: '', id: -1 })
|
||||||
|
}
|
||||||
|
|
||||||
// If there are more than 5 items, only show the first, an expand item, and last 3
|
// If there are more than 5 items, only show the first, an expand item, and last 3
|
||||||
let items = allItems
|
let items = allItems
|
||||||
|
@ -684,45 +745,45 @@ export function ContractFeed(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root pr-2 md:pr-0">
|
<div className="flow-root pr-2 md:pr-0">
|
||||||
<ul role="list" className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-8')}>
|
||||||
{items.map((activityItem, activityItemIdx) => (
|
{items.map((activityItem, activityItemIdx) => (
|
||||||
<li key={activityItem.id}>
|
<div className="relative pb-8">
|
||||||
<div className="relative pb-8">
|
{activityItemIdx !== items.length - 1 ? (
|
||||||
{activityItemIdx !== items.length - 1 ? (
|
<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-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="relative flex items-start space-x-3">
|
||||||
|
{activityItem.type === 'start' ? (
|
||||||
|
feedType === 'activity' ? (
|
||||||
|
<FeedQuestion contract={contract} />
|
||||||
|
) : feedType === 'market' ? (
|
||||||
|
<FeedDescription contract={contract} />
|
||||||
|
) : feedType === 'multi' ? (
|
||||||
|
<FeedAnswer contract={contract} outcome={outcome || '0'} />
|
||||||
|
) : null
|
||||||
|
) : activityItem.type === 'comment' ? (
|
||||||
|
<FeedComment
|
||||||
|
activityItem={activityItem}
|
||||||
|
moreHref={contractPath(contract)}
|
||||||
|
feedType={feedType}
|
||||||
/>
|
/>
|
||||||
|
) : activityItem.type === 'bet' ? (
|
||||||
|
<FeedBet activityItem={activityItem} feedType={feedType} />
|
||||||
|
) : activityItem.type === 'betgroup' ? (
|
||||||
|
<FeedBetGroup activityItem={activityItem} feedType={feedType} />
|
||||||
|
) : activityItem.type === 'close' ? (
|
||||||
|
<FeedClose contract={contract} />
|
||||||
|
) : activityItem.type === 'resolve' ? (
|
||||||
|
<FeedResolve contract={contract} />
|
||||||
|
) : activityItem.type === 'expand' ? (
|
||||||
|
<FeedExpand setExpanded={setExpanded} />
|
||||||
) : null}
|
) : null}
|
||||||
<div className="relative flex items-start space-x-3">
|
|
||||||
{activityItem.type === 'start' ? (
|
|
||||||
feedType == 'activity' ? (
|
|
||||||
<FeedQuestion contract={contract} />
|
|
||||||
) : (
|
|
||||||
<FeedDescription contract={contract} />
|
|
||||||
)
|
|
||||||
) : activityItem.type === 'comment' ? (
|
|
||||||
<FeedComment
|
|
||||||
activityItem={activityItem}
|
|
||||||
moreHref={contractPath(contract)}
|
|
||||||
feedType={feedType}
|
|
||||||
/>
|
|
||||||
) : activityItem.type === 'bet' ? (
|
|
||||||
<FeedBet activityItem={activityItem} />
|
|
||||||
) : activityItem.type === 'betgroup' ? (
|
|
||||||
<FeedBetGroup activityItem={activityItem} />
|
|
||||||
) : activityItem.type === 'close' ? (
|
|
||||||
<FeedClose contract={contract} />
|
|
||||||
) : activityItem.type === 'resolve' ? (
|
|
||||||
<FeedResolve contract={contract} />
|
|
||||||
) : activityItem.type === 'expand' ? (
|
|
||||||
<FeedExpand setExpanded={setExpanded} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
{isBinary && tradingAllowed(contract) && (
|
{isBinary && tradingAllowed(contract) && (
|
||||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user