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 { Avatar } from '../avatar'
|
||||
import { SiteLink } from '../site-link'
|
||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||
import dayjs from 'dayjs'
|
||||
import { BuyButton } from '../yes-no-selector'
|
||||
import { formatPercent } from '../../../common/util/format'
|
||||
import { getOutcomeProbability } from '../../../common/calculate'
|
||||
import { tradingAllowed } from '../../lib/firebase/contracts'
|
||||
import { AnswerBetPanel } from './answer-bet-panel'
|
||||
import { ContractFeed } from '../contract-feed'
|
||||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
|
@ -35,10 +34,9 @@ export function AnswerItem(props: {
|
|||
onDeselect,
|
||||
} = props
|
||||
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 createdDate = dayjs(createdTime).format('MMM D')
|
||||
const prob = getOutcomeProbability(totalShares, answer.id)
|
||||
const roundedProb = Math.round(prob * 100)
|
||||
const probPercent = formatPercent(prob)
|
||||
|
@ -48,41 +46,44 @@ export function AnswerItem(props: {
|
|||
const [isBetting, setIsBetting] = useState(false)
|
||||
|
||||
return (
|
||||
<Col
|
||||
<div
|
||||
className={clsx(
|
||||
'p-4 sm:flex-row rounded gap-4',
|
||||
'flex flex-col gap-4 rounded p-4 sm:flex-row',
|
||||
wasResolvedTo
|
||||
? resolution === 'MKT'
|
||||
? 'bg-blue-50 mb-2'
|
||||
: 'bg-green-50 mb-8'
|
||||
? 'mb-2 bg-blue-50'
|
||||
: 'mb-8 bg-green-50'
|
||||
: chosenProb === undefined
|
||||
? 'bg-gray-50'
|
||||
: showChoice === 'radio'
|
||||
? '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>
|
||||
|
||||
<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}`}>
|
||||
<Row className="items-center gap-2">
|
||||
<Avatar avatarUrl={avatarUrl} size={6} />
|
||||
<div className="truncate">{name}</div>
|
||||
</Row>
|
||||
</SiteLink>
|
||||
|
||||
<div className="">•</div>
|
||||
|
||||
<div className="whitespace-nowrap">
|
||||
<DateTimeTooltip text="" time={contract.createdTime}>
|
||||
{createdDate}
|
||||
</DateTimeTooltip>
|
||||
</div>
|
||||
<div className="">•</div>
|
||||
<div className="text-base">#{number}</div>
|
||||
{/* TODO: Show total pool? */}
|
||||
</Row>
|
||||
|
||||
{isBetting && (
|
||||
<ContractFeed
|
||||
contract={contract}
|
||||
bets={[]}
|
||||
comments={[]}
|
||||
feedType="multi"
|
||||
outcome={answer.id}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{isBetting ? (
|
||||
|
@ -92,11 +93,11 @@ export function AnswerItem(props: {
|
|||
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 &&
|
||||
(showChoice === 'checkbox' ? (
|
||||
<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"
|
||||
placeholder={`${roundedProb}`}
|
||||
maxLength={9}
|
||||
|
@ -121,7 +122,7 @@ export function AnswerItem(props: {
|
|||
))}
|
||||
{showChoice ? (
|
||||
<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>
|
||||
{showChoice === 'radio' && (
|
||||
<input
|
||||
|
@ -162,7 +163,7 @@ export function AnswerItem(props: {
|
|||
<>
|
||||
{tradingAllowed(contract) && (
|
||||
<BuyButton
|
||||
className="justify-end self-end flex-initial btn-md !px-8"
|
||||
className="btn-md flex-initial justify-end self-end !px-8"
|
||||
onClick={() => {
|
||||
setIsBetting(true)
|
||||
}}
|
||||
|
@ -188,6 +189,6 @@ export function AnswerItem(props: {
|
|||
)}
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ import { useAdmin } from '../hooks/use-admin'
|
|||
function FeedComment(props: {
|
||||
activityItem: any
|
||||
moreHref: string
|
||||
feedType: 'activity' | 'market'
|
||||
feedType: FeedType
|
||||
}) {
|
||||
const { activityItem, moreHref, feedType } = props
|
||||
const { person, text, amount, outcome, createdTime } = activityItem
|
||||
|
@ -65,7 +65,8 @@ function FeedComment(props: {
|
|||
username={person.username}
|
||||
name={person.name}
|
||||
/>{' '}
|
||||
{bought} {money} of <OutcomeLabel outcome={outcome} />{' '}
|
||||
{bought} {money}
|
||||
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||
<Timestamp time={createdTime} />
|
||||
</p>
|
||||
</div>
|
||||
|
@ -90,8 +91,8 @@ function Timestamp(props: { time: number }) {
|
|||
)
|
||||
}
|
||||
|
||||
function FeedBet(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
function FeedBet(props: { activityItem: any; feedType: FeedType }) {
|
||||
const { activityItem, feedType } = props
|
||||
const { id, contractId, amount, outcome, createdTime } = activityItem
|
||||
const user = useUser()
|
||||
const isSelf = user?.id == activityItem.userId
|
||||
|
@ -122,8 +123,9 @@ function FeedBet(props: { activityItem: any }) {
|
|||
</div>
|
||||
<div className="min-w-0 flex-1 py-1.5">
|
||||
<div className="text-sm text-gray-500">
|
||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money} of{' '}
|
||||
<OutcomeLabel outcome={outcome} /> <Timestamp time={createdTime} />
|
||||
<span>{isSelf ? 'You' : 'A trader'}</span> {bought} {money}
|
||||
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />
|
||||
<Timestamp time={createdTime} />
|
||||
{canComment && (
|
||||
// Allow user to comment in an textarea if they are the creator
|
||||
<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 }) {
|
||||
const { outcome } = props
|
||||
switch (outcome) {
|
||||
|
@ -540,8 +565,12 @@ function groupBets(
|
|||
return items as ActivityItem[]
|
||||
}
|
||||
|
||||
function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
||||
const { bets, outcome } = props
|
||||
function BetGroupSpan(props: {
|
||||
bets: Bet[]
|
||||
outcome: string
|
||||
feedType: FeedType
|
||||
}) {
|
||||
const { bets, outcome, feedType } = props
|
||||
|
||||
const numberTraders = _.uniqBy(bets, (b) => b.userId).length
|
||||
|
||||
|
@ -556,14 +585,14 @@ function BetGroupSpan(props: { bets: Bet[]; outcome: string }) {
|
|||
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
|
||||
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
|
||||
</JoinSpans>
|
||||
of <OutcomeLabel outcome={outcome} />
|
||||
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />{' '}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Make this expandable to show all grouped bets?
|
||||
function FeedBetGroup(props: { activityItem: any }) {
|
||||
const { activityItem } = props
|
||||
function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
|
||||
const { activityItem, feedType } = props
|
||||
const bets: Bet[] = activityItem.bets
|
||||
|
||||
const betGroups = _.groupBy(bets, (bet) => bet.outcome)
|
||||
|
@ -585,7 +614,11 @@ function FeedBetGroup(props: { activityItem: any }) {
|
|||
<div className="text-sm text-gray-500">
|
||||
{outcomes.map((outcome, index) => (
|
||||
<Fragment key={outcome}>
|
||||
<BetGroupSpan outcome={outcome} bets={betGroups[outcome]} />
|
||||
<BetGroupSpan
|
||||
outcome={outcome}
|
||||
bets={betGroups[outcome]}
|
||||
feedType={feedType}
|
||||
/>
|
||||
{index !== outcomes.length - 1 && <br />}
|
||||
</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:
|
||||
// - Bet sold?
|
||||
type ActivityItem = {
|
||||
|
@ -637,15 +682,23 @@ type ActivityItem = {
|
|||
| '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: {
|
||||
contract: Contract
|
||||
bets: Bet[]
|
||||
comments: Comment[]
|
||||
// Feed types: 'activity' = Activity feed, 'market' = Comments feed on a market
|
||||
feedType: 'activity' | 'market'
|
||||
feedType: FeedType
|
||||
outcome?: string // Which multi-category outcome to filter
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, feedType, betRowClassName } = props
|
||||
const { contract, feedType, outcome, betRowClassName } = props
|
||||
const { id, outcomeType } = contract
|
||||
const isBinary = outcomeType === 'BINARY'
|
||||
|
||||
|
@ -657,6 +710,10 @@ export function ContractFeed(props: {
|
|||
? bets.filter((bet) => !bet.isAnte)
|
||||
: 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 groupWindow = feedType == 'activity' ? 10 * DAY_IN_MS : DAY_IN_MS
|
||||
|
@ -671,6 +728,10 @@ export function ContractFeed(props: {
|
|||
if (contract.resolution) {
|
||||
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
|
||||
let items = allItems
|
||||
|
@ -684,45 +745,45 @@ export function ContractFeed(props: {
|
|||
|
||||
return (
|
||||
<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) => (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-8">
|
||||
{activityItemIdx !== items.length - 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-5 -ml-px h-[calc(100%-2rem)] w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
<div className="relative pb-8">
|
||||
{activityItemIdx !== items.length - 1 ? (
|
||||
<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 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}
|
||||
<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>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{isBinary && tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} className={clsx('mb-2', betRowClassName)} />
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue
Block a user