Show comments position (#110)
* Add betting activity back to feed * Show position in bin. markets, no comments on bets * Degroup bets on Bets tab * Show users position or recent bet with comments * Add tooltip on answer to FR comments * Style improvements * Only use bets by current user for comment input
This commit is contained in:
parent
5cb6ee3bca
commit
78997c1e45
|
@ -31,6 +31,8 @@ type BaseActivityItem = {
|
|||
|
||||
export type CommentInputItem = BaseActivityItem & {
|
||||
type: 'commentInput'
|
||||
betsByCurrentUser: Bet[]
|
||||
comments: Comment[]
|
||||
}
|
||||
|
||||
export type DescriptionItem = BaseActivityItem & {
|
||||
|
@ -48,12 +50,13 @@ export type BetItem = BaseActivityItem & {
|
|||
bet: Bet
|
||||
hideOutcome: boolean
|
||||
smallAvatar: boolean
|
||||
hideComment?: boolean
|
||||
}
|
||||
|
||||
export type CommentItem = BaseActivityItem & {
|
||||
type: 'comment'
|
||||
comment: Comment
|
||||
bet: Bet | undefined
|
||||
betsBySameUser: Bet[]
|
||||
hideOutcome: boolean
|
||||
truncate: boolean
|
||||
smallAvatar: boolean
|
||||
|
@ -129,7 +132,7 @@ function groupBets(
|
|||
type: 'comment' as const,
|
||||
id: bet.id,
|
||||
comment,
|
||||
bet,
|
||||
betsBySameUser: [bet],
|
||||
contract,
|
||||
hideOutcome,
|
||||
truncate: abbreviated,
|
||||
|
@ -280,7 +283,7 @@ function groupBetsAndComments(
|
|||
id: comment.id,
|
||||
contract: contract,
|
||||
comment,
|
||||
bet: undefined,
|
||||
betsBySameUser: [],
|
||||
truncate: abbreviated,
|
||||
hideOutcome: true,
|
||||
smallAvatar,
|
||||
|
@ -308,6 +311,27 @@ function groupBetsAndComments(
|
|||
return abbrItems
|
||||
}
|
||||
|
||||
function getCommentsWithPositions(
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
contract: Contract
|
||||
) {
|
||||
const betsByUserId = _.groupBy(bets, (bet) => bet.userId)
|
||||
|
||||
const items = comments.map((comment) => ({
|
||||
type: 'comment' as const,
|
||||
id: comment.id,
|
||||
contract: contract,
|
||||
comment,
|
||||
betsBySameUser: bets.length === 0 ? [] : betsByUserId[comment.userId] ?? [],
|
||||
truncate: true,
|
||||
hideOutcome: false,
|
||||
smallAvatar: false,
|
||||
}))
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export function getAllContractActivityItems(
|
||||
contract: Contract,
|
||||
bets: Bet[],
|
||||
|
@ -361,6 +385,8 @@ export function getAllContractActivityItems(
|
|||
type: 'commentInput',
|
||||
id: 'commentInput',
|
||||
contract,
|
||||
betsByCurrentUser: [],
|
||||
comments: [],
|
||||
})
|
||||
} else {
|
||||
items.push(
|
||||
|
@ -385,6 +411,8 @@ export function getAllContractActivityItems(
|
|||
type: 'commentInput',
|
||||
id: 'commentInput',
|
||||
contract,
|
||||
betsByCurrentUser: [],
|
||||
comments: [],
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -432,24 +460,13 @@ export function getRecentContractActivityItems(
|
|||
)
|
||||
)
|
||||
} else {
|
||||
const onlyUsersBetsOrBetsWithComments = bets.filter((bet) =>
|
||||
comments.some(
|
||||
(comment) => comment.betId === bet.id || bet.userId === user?.id
|
||||
)
|
||||
)
|
||||
items.push(
|
||||
...groupBetsAndComments(
|
||||
onlyUsersBetsOrBetsWithComments,
|
||||
comments,
|
||||
contract,
|
||||
user?.id,
|
||||
{
|
||||
...groupBetsAndComments(bets, comments, contract, user?.id, {
|
||||
hideOutcome: false,
|
||||
abbreviated: true,
|
||||
smallAvatar: false,
|
||||
reversed: true,
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -471,37 +488,29 @@ export function getSpecificContractActivityItems(
|
|||
switch (mode) {
|
||||
case 'bets':
|
||||
items.push(
|
||||
...groupBets(bets, comments, contract, user?.id, {
|
||||
...bets.map((bet) => ({
|
||||
type: 'bet' as const,
|
||||
id: bet.id,
|
||||
bet,
|
||||
contract,
|
||||
hideOutcome: false,
|
||||
abbreviated: false,
|
||||
smallAvatar: false,
|
||||
reversed: false,
|
||||
})
|
||||
hideComment: true,
|
||||
}))
|
||||
)
|
||||
break
|
||||
|
||||
case 'comments':
|
||||
const onlyBetsWithComments = bets.filter((bet) =>
|
||||
comments.some((comment) => comment.betId === bet.id)
|
||||
)
|
||||
items.push(
|
||||
...groupBetsAndComments(
|
||||
onlyBetsWithComments,
|
||||
comments,
|
||||
contract,
|
||||
user?.id,
|
||||
{
|
||||
hideOutcome: false,
|
||||
abbreviated: false,
|
||||
smallAvatar: false,
|
||||
reversed: false,
|
||||
}
|
||||
)
|
||||
)
|
||||
items.push(...getCommentsWithPositions(bets, comments, contract))
|
||||
|
||||
items.push({
|
||||
type: 'commentInput',
|
||||
id: 'commentInput',
|
||||
contract,
|
||||
betsByCurrentUser: user
|
||||
? bets.filter((bet) => bet.userId === user.id)
|
||||
: [],
|
||||
comments: comments,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
|
|
@ -39,7 +39,13 @@ import BetRow from '../bet-row'
|
|||
import { Avatar } from '../avatar'
|
||||
import { Answer } from '../../../common/answer'
|
||||
import { ActivityItem } from './activity-items'
|
||||
import { FreeResponse, FullContract } from '../../../common/contract'
|
||||
import {
|
||||
Binary,
|
||||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
FullContract,
|
||||
} from '../../../common/contract'
|
||||
import { BuyButton } from '../yes-no-selector'
|
||||
import { getDpmOutcomeProbability } from '../../../common/calculate-dpm'
|
||||
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
||||
|
@ -50,6 +56,7 @@ import { trackClick } from '../../lib/firebase/tracking'
|
|||
import { firebaseLogin } from '../../lib/firebase/users'
|
||||
import { DAY_MS } from '../../../common/util/time'
|
||||
import NewContractBadge from '../new-contract-badge'
|
||||
import { calculateCpmmSale } from '../../../common/calculate-cpmm'
|
||||
|
||||
export function FeedItems(props: {
|
||||
contract: Contract
|
||||
|
@ -123,21 +130,38 @@ function FeedItem(props: { item: ActivityItem }) {
|
|||
export function FeedComment(props: {
|
||||
contract: Contract
|
||||
comment: Comment
|
||||
bet: Bet | undefined
|
||||
betsBySameUser: Bet[]
|
||||
hideOutcome: boolean
|
||||
truncate: boolean
|
||||
smallAvatar: boolean
|
||||
}) {
|
||||
const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props
|
||||
let money: string | undefined
|
||||
let outcome: string | undefined
|
||||
let bought: string | undefined
|
||||
if (bet) {
|
||||
outcome = bet.outcome
|
||||
bought = bet.amount >= 0 ? 'bought' : 'sold'
|
||||
money = formatMoney(Math.abs(bet.amount))
|
||||
}
|
||||
const {
|
||||
contract,
|
||||
comment,
|
||||
betsBySameUser,
|
||||
hideOutcome,
|
||||
truncate,
|
||||
smallAvatar,
|
||||
} = props
|
||||
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
|
||||
let outcome: string | undefined,
|
||||
bought: string | undefined,
|
||||
money: string | undefined
|
||||
|
||||
const matchedBet = betsBySameUser.find((bet) => bet.id === comment.betId)
|
||||
if (matchedBet) {
|
||||
outcome = matchedBet.outcome
|
||||
bought = matchedBet.amount >= 0 ? 'bought' : 'sold'
|
||||
money = formatMoney(Math.abs(matchedBet.amount))
|
||||
}
|
||||
|
||||
// Only calculated if they don't have a matching bet
|
||||
const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } =
|
||||
getBettorsPosition(
|
||||
contract,
|
||||
comment.createdTime,
|
||||
matchedBet ? [] : betsBySameUser
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -155,8 +179,22 @@ export function FeedComment(props: {
|
|||
username={userUsername}
|
||||
name={userName}
|
||||
/>{' '}
|
||||
{!matchedBet && userPosition > 0 && (
|
||||
<>
|
||||
{'with ' + userPositionMoney + ' '}
|
||||
<>
|
||||
{' of '}
|
||||
<OutcomeLabel
|
||||
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{bought} {money}
|
||||
{!hideOutcome && (
|
||||
{outcome && !hideOutcome && (
|
||||
<>
|
||||
{' '}
|
||||
of{' '}
|
||||
|
@ -167,6 +205,7 @@ export function FeedComment(props: {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<RelativeTimestamp time={createdTime} />
|
||||
</p>
|
||||
</div>
|
||||
|
@ -180,20 +219,12 @@ export function FeedComment(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function RelativeTimestamp(props: { time: number }) {
|
||||
const { time } = props
|
||||
return (
|
||||
<DateTimeTooltip time={time}>
|
||||
<span className="ml-1 whitespace-nowrap text-gray-400">
|
||||
{fromNow(time)}
|
||||
</span>
|
||||
</DateTimeTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function CommentInput(props: { contract: Contract }) {
|
||||
// see if we can comment input on any bet:
|
||||
const { contract } = props
|
||||
export function CommentInput(props: {
|
||||
contract: Contract
|
||||
betsByCurrentUser: Bet[]
|
||||
comments: Comment[]
|
||||
}) {
|
||||
const { contract, betsByCurrentUser, comments } = props
|
||||
const user = useUser()
|
||||
const [comment, setComment] = useState('')
|
||||
|
||||
|
@ -206,14 +237,50 @@ export function CommentInput(props: { contract: Contract }) {
|
|||
setComment('')
|
||||
}
|
||||
|
||||
// Should this be oldest bet or most recent bet?
|
||||
const mostRecentCommentableBet = betsByCurrentUser
|
||||
.filter(
|
||||
(bet) =>
|
||||
canCommentOnBet(bet.userId, bet.createdTime, user) &&
|
||||
!comments.some((comment) => comment.betId == bet.id)
|
||||
)
|
||||
.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||
.pop()
|
||||
|
||||
if (mostRecentCommentableBet) {
|
||||
return (
|
||||
<FeedBet
|
||||
contract={contract}
|
||||
bet={mostRecentCommentableBet}
|
||||
hideOutcome={false}
|
||||
smallAvatar={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } =
|
||||
getBettorsPosition(contract, Date.now(), betsByCurrentUser)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className={'flex w-full gap-2 pt-5'}>
|
||||
<Row className={'flex w-full gap-2 pt-3'}>
|
||||
<div>
|
||||
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
|
||||
</div>
|
||||
<div className={'min-w-0 flex-1 py-1.5'}>
|
||||
<div className="text-sm text-gray-500">
|
||||
{user && userPosition > 0 && (
|
||||
<>
|
||||
{'You with ' + userPositionMoney + ' '}
|
||||
<>
|
||||
{' of '}
|
||||
<OutcomeLabel
|
||||
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
|
||||
contract={contract}
|
||||
truncate="short"
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<Textarea
|
||||
value={comment}
|
||||
|
@ -244,18 +311,76 @@ export function CommentInput(props: { contract: Contract }) {
|
|||
)
|
||||
}
|
||||
|
||||
function RelativeTimestamp(props: { time: number }) {
|
||||
const { time } = props
|
||||
return (
|
||||
<DateTimeTooltip time={time}>
|
||||
<span className="ml-1 whitespace-nowrap text-gray-400">
|
||||
{fromNow(time)}
|
||||
</span>
|
||||
</DateTimeTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function getBettorsPosition(
|
||||
contract: Contract,
|
||||
createdTime: number,
|
||||
bets: Bet[]
|
||||
) {
|
||||
let yesFloorShares = 0,
|
||||
yesShares = 0,
|
||||
noShares = 0,
|
||||
noFloorShares = 0
|
||||
|
||||
const emptyReturn = {
|
||||
userPosition: 0,
|
||||
userPositionMoney: 0,
|
||||
yesFloorShares,
|
||||
noFloorShares,
|
||||
}
|
||||
|
||||
// TODO: show which of the answers was their majority stake at time of comment for FR?
|
||||
if (contract.outcomeType != 'BINARY') {
|
||||
return emptyReturn
|
||||
}
|
||||
if (bets.length === 0) {
|
||||
return emptyReturn
|
||||
}
|
||||
|
||||
// Calculate the majority shares they had when they made the comment
|
||||
const betsBefore = bets.filter((prevBet) => prevBet.createdTime < createdTime)
|
||||
const [yesBets, noBets] = _.partition(
|
||||
betsBefore ?? [],
|
||||
(bet) => bet.outcome === 'YES'
|
||||
)
|
||||
yesShares = _.sumBy(yesBets, (bet) => bet.shares)
|
||||
noShares = _.sumBy(noBets, (bet) => bet.shares)
|
||||
yesFloorShares = Math.floor(yesShares)
|
||||
noFloorShares = Math.floor(noShares)
|
||||
|
||||
const userPosition = yesFloorShares || noFloorShares
|
||||
const { saleValue } = calculateCpmmSale(
|
||||
contract as FullContract<CPMM, Binary>,
|
||||
yesShares || noShares,
|
||||
yesFloorShares > noFloorShares ? 'YES' : 'NO'
|
||||
)
|
||||
const userPositionMoney = formatMoney(Math.abs(saleValue))
|
||||
return { userPosition, userPositionMoney, yesFloorShares, noFloorShares }
|
||||
}
|
||||
|
||||
export function FeedBet(props: {
|
||||
contract: Contract
|
||||
bet: Bet
|
||||
hideOutcome: boolean
|
||||
smallAvatar: boolean
|
||||
hideComment?: boolean
|
||||
bettor?: User // If set: reveal bettor identity
|
||||
}) {
|
||||
const { contract, bet, hideOutcome, smallAvatar, bettor } = props
|
||||
const { contract, bet, hideOutcome, smallAvatar, bettor, hideComment } = props
|
||||
const { id, amount, outcome, createdTime, userId } = bet
|
||||
const user = useUser()
|
||||
const isSelf = user?.id === userId
|
||||
const canComment = canCommentOnBet(userId, createdTime, user)
|
||||
const canComment = canCommentOnBet(userId, createdTime, user) && !hideComment
|
||||
|
||||
const [comment, setComment] = useState('')
|
||||
async function submitComment() {
|
||||
|
@ -268,6 +393,7 @@ export function FeedBet(props: {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Row className={'flex w-full gap-2 pt-3'}>
|
||||
<div>
|
||||
{isSelf ? (
|
||||
<Avatar
|
||||
|
@ -286,7 +412,10 @@ export function FeedBet(props: {
|
|||
) : (
|
||||
<div className="relative px-1">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
|
||||
<UserIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
||||
<UserIcon
|
||||
className="h-5 w-5 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -333,6 +462,7 @@ export function FeedBet(props: {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import clsx from 'clsx'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import {
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
FullContract,
|
||||
} from '../../common/contract'
|
||||
import { formatPercent } from '../../common/util/format'
|
||||
import { ClientRender } from './client-render'
|
||||
|
||||
export function OutcomeLabel(props: {
|
||||
contract: Contract
|
||||
|
@ -72,11 +72,13 @@ export function FreeResponseOutcomeLabel(props: {
|
|||
const chosen = answers?.find((answer) => answer.id === resolution)
|
||||
if (!chosen) return <AnswerNumberLabel number={resolution} />
|
||||
return (
|
||||
<FreeResponseAnswerToolTip text={chosen.text}>
|
||||
<AnswerLabel
|
||||
answer={chosen}
|
||||
truncate={truncate}
|
||||
className={answerClassName}
|
||||
/>
|
||||
</FreeResponseAnswerToolTip>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -126,3 +128,23 @@ export function AnswerLabel(props: {
|
|||
|
||||
return <span className={className}>{truncated}</span>
|
||||
}
|
||||
|
||||
function FreeResponseAnswerToolTip(props: {
|
||||
text: string
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const { text } = props
|
||||
return (
|
||||
<>
|
||||
<ClientRender>
|
||||
<span
|
||||
className="tooltip hidden cursor-default sm:inline-block"
|
||||
data-tip={text}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
</ClientRender>
|
||||
<span className="whitespace-nowrap sm:hidden">{props.children}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -287,7 +287,7 @@ function ContractTopTrades(props: {
|
|||
<FeedComment
|
||||
contract={contract}
|
||||
comment={commentsById[topCommentId]}
|
||||
bet={betsById[topCommentId]}
|
||||
betsBySameUser={[betsById[topCommentId]]}
|
||||
hideOutcome={false}
|
||||
truncate={false}
|
||||
smallAvatar={false}
|
||||
|
|
Loading…
Reference in New Issue
Block a user