Show users position or recent bet with comments

This commit is contained in:
Ian Philips 2022-04-29 10:39:59 -06:00
parent 3e600f45b5
commit d0700c1221
3 changed files with 261 additions and 149 deletions

View File

@ -31,6 +31,8 @@ type BaseActivityItem = {
export type CommentInputItem = BaseActivityItem & { export type CommentInputItem = BaseActivityItem & {
type: 'commentInput' type: 'commentInput'
bets: Bet[]
comments: Comment[]
} }
export type DescriptionItem = BaseActivityItem & { export type DescriptionItem = BaseActivityItem & {
@ -48,12 +50,13 @@ export type BetItem = BaseActivityItem & {
bet: Bet bet: Bet
hideOutcome: boolean hideOutcome: boolean
smallAvatar: boolean smallAvatar: boolean
hideComment?: boolean
} }
export type CommentItem = BaseActivityItem & { export type CommentItem = BaseActivityItem & {
type: 'comment' type: 'comment'
comment: Comment comment: Comment
bet: Bet | undefined betsBySameUser: Bet[]
hideOutcome: boolean hideOutcome: boolean
truncate: boolean truncate: boolean
smallAvatar: boolean smallAvatar: boolean
@ -129,7 +132,7 @@ function groupBets(
type: 'comment' as const, type: 'comment' as const,
id: bet.id, id: bet.id,
comment, comment,
bet, betsBySameUser: [bet],
contract, contract,
hideOutcome, hideOutcome,
truncate: abbreviated, truncate: abbreviated,
@ -280,7 +283,7 @@ function groupBetsAndComments(
id: comment.id, id: comment.id,
contract: contract, contract: contract,
comment, comment,
bet: undefined, betsBySameUser: [],
truncate: abbreviated, truncate: abbreviated,
hideOutcome: true, hideOutcome: true,
smallAvatar, smallAvatar,
@ -308,6 +311,37 @@ function groupBetsAndComments(
return abbrItems return abbrItems
} }
function getCommentsWithPositions(
bets: Bet[],
comments: Comment[],
contract: Contract
) {
function mapBetsByUserId(bets: Bet[]) {
return bets.reduce((acc, bet) => {
const userId = bet.userId
if (!acc[userId]) {
acc[userId] = []
}
acc[userId].push(bet)
return acc
}, {} as { [userId: string]: Bet[] })
}
const betsByUserId = mapBetsByUserId(bets)
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( export function getAllContractActivityItems(
contract: Contract, contract: Contract,
bets: Bet[], bets: Bet[],
@ -361,6 +395,8 @@ export function getAllContractActivityItems(
type: 'commentInput', type: 'commentInput',
id: 'commentInput', id: 'commentInput',
contract, contract,
bets: [],
comments: [],
}) })
} else { } else {
items.push( items.push(
@ -385,6 +421,8 @@ export function getAllContractActivityItems(
type: 'commentInput', type: 'commentInput',
id: 'commentInput', id: 'commentInput',
contract, contract,
bets: [],
comments: [],
}) })
} }
@ -467,32 +505,20 @@ export function getSpecificContractActivityItems(
contract, contract,
hideOutcome: false, hideOutcome: false,
smallAvatar: false, smallAvatar: false,
hideComment: true,
})) }))
) )
break break
case 'comments': case 'comments':
const onlyBetsWithComments = bets.filter((bet) => items.push(...getCommentsWithPositions(bets, comments, contract))
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({ items.push({
type: 'commentInput', type: 'commentInput',
id: 'commentInput', id: 'commentInput',
contract, contract,
bets: bets,
comments: comments,
}) })
break break
} }

View File

@ -56,8 +56,7 @@ import { trackClick } from '../../lib/firebase/tracking'
import { firebaseLogin } from '../../lib/firebase/users' import { firebaseLogin } from '../../lib/firebase/users'
import { DAY_MS } from '../../../common/util/time' import { DAY_MS } from '../../../common/util/time'
import NewContractBadge from '../new-contract-badge' import NewContractBadge from '../new-contract-badge'
import { useUserContractBets } from '../../hooks/use-user-bets' import { calculateCpmmSale } from '../../../common/calculate-cpmm'
import { useSaveShares } from '../use-save-shares'
export function FeedItems(props: { export function FeedItems(props: {
contract: Contract contract: Contract
@ -131,29 +130,38 @@ function FeedItem(props: { item: ActivityItem }) {
export function FeedComment(props: { export function FeedComment(props: {
contract: Contract contract: Contract
comment: Comment comment: Comment
bet: Bet | undefined betsBySameUser: Bet[]
hideOutcome: boolean hideOutcome: boolean
truncate: boolean truncate: boolean
smallAvatar: boolean smallAvatar: boolean
}) { }) {
const { contract, comment, bet, hideOutcome, truncate, smallAvatar } = props const {
let money: string | undefined contract,
let outcome: string | undefined comment,
let bought: string | undefined betsBySameUser,
if (bet) { hideOutcome,
outcome = bet.outcome truncate,
bought = bet.amount >= 0 ? 'bought' : 'sold' smallAvatar,
money = formatMoney(Math.abs(bet.amount)) } = props
} const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
const { text, userUsername, userName, userAvatarUrl, createdTime, userId } = let outcome: string | undefined,
comment bought: string | undefined,
money: string | undefined
const userBets = useUserContractBets(userId, contract.id) const matchedBet = betsBySameUser.find((bet) => bet.id === comment.betId)
const { yesFloorShares, noFloorShares } = useSaveShares( if (matchedBet) {
contract as FullContract<CPMM | DPM, Binary>, outcome = matchedBet.outcome
userBets bought = matchedBet.amount >= 0 ? 'bought' : 'sold'
) money = formatMoney(Math.abs(matchedBet.amount))
const userPosition = yesFloorShares || noFloorShares }
// Only calculated if they don't have a matching bet
const [userPosition, userPositionMoney, yesFloorShares, noFloorShares] =
getBettorsPosition(
contract,
comment.createdTime,
matchedBet ? [] : betsBySameUser
)
return ( return (
<> <>
@ -171,36 +179,33 @@ export function FeedComment(props: {
username={userUsername} username={userUsername}
name={userName} name={userName}
/>{' '} />{' '}
{contract.outcomeType === 'BINARY' ? ( {!matchedBet && userPosition > 0 && (
userPosition > 0 && (
<>
{'owns ' + userPosition + ' shares '}
<>
{' of '}
<OutcomeLabel
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
contract={contract}
truncate="short"
/>
</>
</>
)
) : (
<> <>
{bought} {money} {'with ' + userPositionMoney + ' '}
{!hideOutcome && ( <>
<> {' of '}
{' '} <OutcomeLabel
of{' '} outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
<OutcomeLabel contract={contract}
outcome={outcome ? outcome : ''} truncate="short"
contract={contract} />
truncate="short" </>
/>
</>
)}
</> </>
)} )}
<>
{bought} {money}
{outcome && !hideOutcome && (
<>
{' '}
of{' '}
<OutcomeLabel
outcome={outcome ? outcome : ''}
contract={contract}
truncate="short"
/>
</>
)}
</>
<RelativeTimestamp time={createdTime} /> <RelativeTimestamp time={createdTime} />
</p> </p>
</div> </div>
@ -214,20 +219,12 @@ export function FeedComment(props: {
) )
} }
function RelativeTimestamp(props: { time: number }) { export function CommentInput(props: {
const { time } = props contract: Contract
return ( bets: Bet[]
<DateTimeTooltip time={time}> comments: Comment[]
<span className="ml-1 whitespace-nowrap text-gray-400"> }) {
{fromNow(time)} const { contract, bets, comments } = props
</span>
</DateTimeTooltip>
)
}
export function CommentInput(props: { contract: Contract }) {
// see if we can comment input on any bet:
const { contract } = props
const user = useUser() const user = useUser()
const [comment, setComment] = useState('') const [comment, setComment] = useState('')
@ -240,14 +237,50 @@ export function CommentInput(props: { contract: Contract }) {
setComment('') setComment('')
} }
// Should this be oldest bet or most recent bet?
const mostRecentCommentableBet = bets
.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(), bets)
return ( return (
<> <>
<Row className={'flex w-full gap-2 pt-5'}> <Row className={'flex w-full gap-2 pt-3'}>
<div> <div>
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} /> <Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
</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">
{user && userPosition > 0 && (
<>
{'You with ' + userPositionMoney + ' '}
<>
{' of '}
<OutcomeLabel
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
contract={contract}
truncate="short"
/>
</>
</>
)}
<div className="mt-2"> <div className="mt-2">
<Textarea <Textarea
value={comment} value={comment}
@ -278,20 +311,68 @@ 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
// TODO: show which of the answers was their majority stake at time of comment for FR?
if (contract.outcomeType != 'BINARY') {
return [0, 0, 0, 0]
}
if (bets.length === 0) {
return [0, 0, 0, 0]
}
// 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: { export function FeedBet(props: {
contract: Contract contract: Contract
bet: Bet bet: Bet
hideOutcome: boolean hideOutcome: boolean
smallAvatar: boolean smallAvatar: boolean
hideComment?: boolean
bettor?: User // If set: reveal bettor identity 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 { id, amount, outcome, createdTime, userId } = bet
const user = useUser() const user = useUser()
const isSelf = user?.id === userId const isSelf = user?.id === userId
const canComment = const canComment = canCommentOnBet(userId, createdTime, user) && !hideComment
canCommentOnBet(userId, createdTime, user) &&
contract.outcomeType !== 'BINARY'
const [comment, setComment] = useState('') const [comment, setComment] = useState('')
async function submitComment() { async function submitComment() {
@ -304,71 +385,76 @@ export function FeedBet(props: {
return ( return (
<> <>
<div> <Row className={'flex w-full gap-2 pt-3'}>
{isSelf ? ( <div>
<Avatar {isSelf ? (
className={clsx(smallAvatar && 'ml-1')} <Avatar
size={smallAvatar ? 'sm' : undefined} className={clsx(smallAvatar && 'ml-1')}
avatarUrl={user.avatarUrl} size={smallAvatar ? 'sm' : undefined}
username={user.username} avatarUrl={user.avatarUrl}
/> username={user.username}
) : bettor ? ( />
<Avatar ) : bettor ? (
className={clsx(smallAvatar && 'ml-1')} <Avatar
size={smallAvatar ? 'sm' : undefined} className={clsx(smallAvatar && 'ml-1')}
avatarUrl={bettor.avatarUrl} size={smallAvatar ? 'sm' : undefined}
username={bettor.username} avatarUrl={bettor.avatarUrl}
/> username={bettor.username}
) : ( />
<div className="relative px-1"> ) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"> <div className="relative px-1">
<UserIcon className="h-5 w-5 text-gray-500" aria-hidden="true" /> <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
</div> <UserIcon
</div> className="h-5 w-5 text-gray-500"
)} aria-hidden="true"
</div> />
<div className={'min-w-0 flex-1 py-1.5'}> </div>
<div className="text-sm text-gray-500">
<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '}
{bought} {money}
{!hideOutcome && (
<>
{' '}
of{' '}
<OutcomeLabel
outcome={outcome}
contract={contract}
truncate="short"
/>
</>
)}
<RelativeTimestamp time={createdTime} />
{(canComment || comment) && (
<div className="mt-2">
<Textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
className="textarea textarea-bordered w-full resize-none"
placeholder="Add a comment..."
rows={3}
maxLength={MAX_COMMENT_LENGTH}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
submitComment()
}
}}
/>
<button
className="btn btn-outline btn-sm text-transform: mt-1 capitalize"
onClick={submitComment}
disabled={!canComment}
>
Comment
</button>
</div> </div>
)} )}
</div> </div>
</div> <div className={'min-w-0 flex-1 py-1.5'}>
<div className="text-sm text-gray-500">
<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span>{' '}
{bought} {money}
{!hideOutcome && (
<>
{' '}
of{' '}
<OutcomeLabel
outcome={outcome}
contract={contract}
truncate="short"
/>
</>
)}
<RelativeTimestamp time={createdTime} />
{(canComment || comment) && (
<div className="mt-2">
<Textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
className="textarea textarea-bordered w-full resize-none"
placeholder="Add a comment..."
rows={3}
maxLength={MAX_COMMENT_LENGTH}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
submitComment()
}
}}
/>
<button
className="btn btn-outline btn-sm text-transform: mt-1 capitalize"
onClick={submitComment}
disabled={!canComment}
>
Comment
</button>
</div>
)}
</div>
</div>
</Row>
</> </>
) )
} }

View File

@ -287,7 +287,7 @@ function ContractTopTrades(props: {
<FeedComment <FeedComment
contract={contract} contract={contract}
comment={commentsById[topCommentId]} comment={commentsById[topCommentId]}
bet={betsById[topCommentId]} betsBySameUser={[betsById[topCommentId]]}
hideOutcome={false} hideOutcome={false}
truncate={false} truncate={false}
smallAvatar={false} smallAvatar={false}