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:
Austin Chen 2022-02-24 11:26:01 -08:00 committed by GitHub
parent a2c1107e10
commit 952b7be94a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 137 additions and 75 deletions

View File

@ -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>
)
}

View File

@ -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)} />
)}