Convert to typed ActivityItems

This commit is contained in:
James Grugett 2022-03-11 18:23:02 -06:00
parent 44a093aff4
commit 29e33bc0ac
3 changed files with 188 additions and 144 deletions

View File

@ -10,7 +10,7 @@ export type Comment = {
createdTime: number createdTime: number
// Denormalized, for rendering comments // Denormalized, for rendering comments
userName?: string userName: string
userUsername?: string userUsername: string
userAvatarUrl?: string userAvatarUrl?: string
} }

View File

@ -9,29 +9,72 @@ import { filterDefined } from '../../../common/util/array'
import { canAddComment, mapCommentsByBetId } from '../../lib/firebase/comments' import { canAddComment, mapCommentsByBetId } from '../../lib/firebase/comments'
import { fromNow } from '../../lib/util/time' import { fromNow } from '../../lib/util/time'
export type ActivityItem = { export type ActivityItem =
| DescriptionItem
| QuestionItem
| BetItem
| CommentItem
| CreateAnswerItem
| BetGroupItem
| AnswerGroupItem
| CloseItem
| ResolveItem
type BaseActivityItem = {
id: string id: string
type: contract: Contract
| 'bet'
| 'comment'
| 'start'
| 'betgroup'
| 'answergroup'
| 'close'
| 'resolve'
| 'expand'
| undefined
} }
export type FeedAnswerGroupItem = ActivityItem & { export type DescriptionItem = BaseActivityItem & {
type: 'description'
}
export type QuestionItem = BaseActivityItem & {
type: 'question'
showDescription: boolean
}
export type BetItem = BaseActivityItem & {
type: 'bet'
bet: Bet
hideOutcome: boolean
}
export type CommentItem = BaseActivityItem & {
type: 'comment'
comment: Comment
bet: Bet
showOutcomeLabel: boolean
truncate: boolean
}
export type CreateAnswerItem = BaseActivityItem & {
type: 'createanswer'
answer: Answer
}
export type BetGroupItem = BaseActivityItem & {
type: 'betgroup'
bets: Bet[]
hideOutcome: boolean
}
export type AnswerGroupItem = BaseActivityItem & {
type: 'answergroup' type: 'answergroup'
contract: Contract
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
answer: Answer answer: Answer
user: User | null | undefined user: User | null | undefined
} }
export type CloseItem = BaseActivityItem & {
type: 'close'
}
export type ResolveItem = BaseActivityItem & {
type: 'resolve'
}
const DAY_IN_MS = 24 * 60 * 60 * 1000 const DAY_IN_MS = 24 * 60 * 60 * 1000
// Group together bets that are: // Group together bets that are:
@ -47,22 +90,38 @@ function groupBets(
userId?: string userId?: string
) { ) {
const commentsMap = mapCommentsByBetId(comments) const commentsMap = mapCommentsByBetId(comments)
const items: any[] = [] const items: ActivityItem[] = []
let group: Bet[] = [] let group: Bet[] = []
// Turn the current group into an ActivityItem // Turn the current group into an ActivityItem
function pushGroup() { function pushGroup() {
if (group.length == 1) { if (group.length == 1) {
items.push(toActivityItem(group[0], false)) items.push(toActivityItem(group[0]))
} else if (group.length > 1) { } else if (group.length > 1) {
items.push({ type: 'betgroup', bets: [...group], id: group[0].id }) items.push({
type: 'betgroup',
bets: [...group],
id: group[0].id,
contract,
hideOutcome: false,
})
} }
group = [] group = []
} }
function toActivityItem(bet: Bet, isPublic: boolean) { function toActivityItem(bet: Bet) {
const comment = commentsMap[bet.id] const comment = commentsMap[bet.id]
return comment ? toFeedComment(bet, comment) : toFeedBet(bet, contract) return comment
? {
type: 'comment' as const,
id: bet.id,
comment,
bet,
contract,
showOutcomeLabel: true,
truncate: true,
}
: { type: 'bet' as const, id: bet.id, bet, contract, hideOutcome: false }
} }
for (const bet of bets) { for (const bet of bets) {
@ -71,7 +130,7 @@ function groupBets(
if (commentsMap[bet.id] || isCreator) { if (commentsMap[bet.id] || isCreator) {
pushGroup() pushGroup()
// Create a single item for this // Create a single item for this
items.push(toActivityItem(bet, true)) items.push(toActivityItem(bet))
} else { } else {
if ( if (
group.length > 0 && group.length > 0 &&
@ -86,7 +145,7 @@ function groupBets(
if (group.length > 0) { if (group.length > 0) {
pushGroup() pushGroup()
} }
return items as ActivityItem[] return items
} }
function getAnswerGroups( function getAnswerGroups(
@ -135,41 +194,6 @@ function getAnswerGroups(
return answerGroups return answerGroups
} }
function toFeedBet(bet: Bet, contract: Contract) {
return {
id: bet.id,
contractId: bet.contractId,
userId: bet.userId,
type: 'bet',
amount: bet.sale ? -bet.sale.amount : bet.amount,
outcome: bet.outcome,
createdTime: bet.createdTime,
date: fromNow(bet.createdTime),
contract,
}
}
function toFeedComment(bet: Bet, comment: Comment) {
return {
id: bet.id,
contractId: bet.contractId,
userId: bet.userId,
type: 'comment',
amount: bet.sale ? -bet.sale.amount : bet.amount,
outcome: bet.outcome,
createdTime: bet.createdTime,
date: fromNow(bet.createdTime),
// Invariant: bet.comment exists
text: comment.text,
person: {
username: comment.userUsername,
name: comment.userName,
avatarUrl: comment.userAvatarUrl,
},
}
}
export function getAllContractActivityItems( export function getAllContractActivityItems(
contract: Contract, contract: Contract,
bets: Bet[], bets: Bet[],
@ -184,8 +208,10 @@ export function getAllContractActivityItems(
? 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'))
let answer: Answer | undefined
if (outcome) { if (outcome) {
bets = bets.filter((bet) => bet.outcome === outcome) bets = bets.filter((bet) => bet.outcome === outcome)
answer = contract.answers?.find((answer) => answer.id === outcome)
} else if (outcomeType === 'FREE_RESPONSE') { } else if (outcomeType === 'FREE_RESPONSE') {
// Keep bets on comments or your bets where you can comment. // Keep bets on comments or your bets where you can comment.
const commentBetIds = new Set(comments.map((comment) => comment.betId)) const commentBetIds = new Set(comments.map((comment) => comment.betId))
@ -196,19 +222,18 @@ export function getAllContractActivityItems(
) )
} }
const items: ActivityItem[] = outcome ? [] : [{ type: 'start', id: '0' }] const items: ActivityItem[] =
outcome && answer
? [{ type: 'createanswer', id: answer.id, contract, answer }]
: [{ type: 'description', id: '0', contract }]
items.push(...groupBets(bets, comments, DAY_IN_MS, contract, user?.id)) items.push(...groupBets(bets, comments, DAY_IN_MS, contract, user?.id))
if (contract.closeTime && contract.closeTime <= Date.now()) { if (contract.closeTime && contract.closeTime <= Date.now()) {
items.push({ type: 'close', id: `${contract.closeTime}` }) items.push({ type: 'close', id: `${contract.closeTime}`, contract })
} }
if (contract.resolution) { if (contract.resolution) {
items.push({ type: 'resolve', id: `${contract.resolutionTime}` }) items.push({ type: 'resolve', id: `${contract.resolutionTime}`, contract })
}
if (outcome) {
// Hack to add some more padding above the 'multi' feedType, by adding a null item.
items.unshift({ type: undefined, id: '-1' })
} }
return items return items
@ -223,20 +248,13 @@ export function getRecentContractActivityItems(
bets = bets.sort((b1, b2) => b1.createdTime - b2.createdTime) bets = bets.sort((b1, b2) => b1.createdTime - b2.createdTime)
comments = comments.sort((c1, c2) => c1.createdTime - c2.createdTime) comments = comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
const items: ActivityItem[] = [{ type: 'start', id: '0' }] const items: ActivityItem[] = []
items.push( items.push(
...(contract.outcomeType === 'FREE_RESPONSE' ...(contract.outcomeType === 'FREE_RESPONSE'
? getAnswerGroups(contract, bets, comments, user) ? getAnswerGroups(contract, bets, comments, user)
: groupBets(bets, comments, DAY_IN_MS, contract, user?.id)) : groupBets(bets, comments, DAY_IN_MS, contract, user?.id))
) )
if (contract.closeTime && contract.closeTime <= Date.now()) {
items.push({ type: 'close', id: `${contract.closeTime}` })
}
if (contract.resolution) {
items.push({ type: 'resolve', id: `${contract.resolutionTime}` })
}
// Remove all but last bet group. // Remove all but last bet group.
const betGroups = items.filter((item) => item.type === 'betgroup') const betGroups = items.filter((item) => item.type === 'betgroup')
const lastBetGroup = betGroups[betGroups.length - 1] const lastBetGroup = betGroups[betGroups.length - 1]
@ -244,6 +262,16 @@ export function getRecentContractActivityItems(
(item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id (item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id
) )
// Only show the first item plus the last three items. const questionItem: QuestionItem = {
return filtered.length > 3 ? [filtered[0], ...filtered.slice(-3)] : filtered type: 'question',
id: '0',
contract,
showDescription: false,
}
return [
questionItem,
// Only take the last three items.
...filtered.slice(-3),
]
} }

View File

@ -31,6 +31,7 @@ import {
MAX_COMMENT_LENGTH, MAX_COMMENT_LENGTH,
} from '../../lib/firebase/comments' } from '../../lib/firebase/comments'
import { formatMoney } from '../../../common/util/format' import { formatMoney } from '../../../common/util/format'
import { Comment } from '../../../common/comment'
import { ResolutionOrChance } from '../contract-card' import { ResolutionOrChance } from '../contract-card'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { Col } from '../layout/col' import { Col } from '../layout/col'
@ -44,7 +45,8 @@ import { parseTags } from '../../../common/util/parse'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { useAdmin } from '../../hooks/use-admin' import { useAdmin } from '../../hooks/use-admin'
import { Answer } from '../../../common/answer' import { Answer } from '../../../common/answer'
import { ActivityItem, FeedAnswerGroupItem } from './activity-items' import { ActivityItem } from './activity-items'
import { User } from '../../../common/user'
export type FeedType = export type FeedType =
// Main homepage/fold feed, // Main homepage/fold feed,
@ -68,8 +70,8 @@ export function FeedItems(props: {
return ( return (
<div className="flow-root pr-2 md:pr-0"> <div className="flow-root pr-2 md:pr-0">
<div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}> <div className={clsx(tradingAllowed(contract) ? '' : '-mb-6')}>
{items.map((activityItem, activityItemIdx) => ( {items.map((item, activityItemIdx) => (
<div key={activityItem.id} className="relative pb-6"> <div key={item.id} className="relative pb-6">
{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"
@ -77,32 +79,24 @@ export function FeedItems(props: {
/> />
) : null} ) : null}
<div className="relative flex items-start space-x-3"> <div className="relative flex items-start space-x-3">
{activityItem.type === 'start' ? ( {item.type === 'question' ? (
feedType === 'activity' ? ( <FeedQuestion {...item} />
<FeedQuestion contract={contract} /> ) : item.type === 'description' ? (
) : feedType === 'market' ? ( <FeedDescription {...item} />
<FeedDescription contract={contract} /> ) : item.type === 'createanswer' ? (
) : feedType === 'multi' ? ( <FeedCreateAnswer {...item} />
<FeedAnswer contract={contract} outcome={outcome || '0'} /> ) : item.type === 'comment' ? (
) : null <FeedComment {...item} />
) : activityItem.type === 'comment' ? ( ) : item.type === 'bet' ? (
<FeedComment <FeedBet {...item} />
activityItem={activityItem} ) : item.type === 'betgroup' ? (
moreHref={contractPath(contract)} <FeedBetGroup {...item} />
feedType={feedType} ) : item.type === 'answergroup' ? (
/> <FeedAnswerGroup {...item} />
) : activityItem.type === 'bet' ? ( ) : item.type === 'close' ? (
<FeedBet activityItem={activityItem} feedType={feedType} /> <FeedClose {...item} />
) : activityItem.type === 'betgroup' ? ( ) : item.type === 'resolve' ? (
<FeedBetGroup activityItem={activityItem} feedType={feedType} /> <FeedResolve {...item} />
) : activityItem.type === 'answergroup' ? (
<FeedAnswerGroup
activityItem={activityItem as FeedAnswerGroupItem}
/>
) : activityItem.type === 'close' ? (
<FeedClose contract={contract} />
) : activityItem.type === 'resolve' ? (
<FeedResolve contract={contract} />
) : null} ) : null}
</div> </div>
</div> </div>
@ -116,12 +110,16 @@ export function FeedItems(props: {
} }
function FeedComment(props: { function FeedComment(props: {
activityItem: any contract: Contract
moreHref: string comment: Comment
feedType: FeedType bet: Bet
showOutcomeLabel: boolean
truncate: boolean
}) { }) {
const { activityItem, moreHref, feedType } = props const { contract, comment, bet, showOutcomeLabel, truncate } = props
const { person, text, amount, outcome, createdTime, contract } = activityItem const { createdTime } = contract
const { amount, outcome } = bet
const { text, userUsername, userName, userAvatarUrl } = comment
const bought = amount >= 0 ? 'bought' : 'sold' const bought = amount >= 0 ? 'bought' : 'sold'
const money = formatMoney(Math.abs(amount)) const money = formatMoney(Math.abs(amount))
@ -134,7 +132,7 @@ function FeedComment(props: {
return ( return (
<> <>
<Avatar username={person.username} avatarUrl={person.avatarUrl} /> <Avatar username={userUsername} avatarUrl={userAvatarUrl} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{/* {answer && ( {/* {answer && (
<div className="text-neutral mb-2" style={{ fontSize: 15 }}> <div className="text-neutral mb-2" style={{ fontSize: 15 }}>
@ -145,18 +143,23 @@ function FeedComment(props: {
<p className="mt-0.5 text-sm text-gray-500"> <p className="mt-0.5 text-sm text-gray-500">
<UserLink <UserLink
className="text-gray-500" className="text-gray-500"
username={person.username} username={userUsername}
name={person.name} name={userName}
/>{' '} />{' '}
{bought} {money} {bought} {money}
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} /> {showOutcomeLabel && (
<>
{' '}
of <OutcomeLabel outcome={outcome} />
</>
)}
<Timestamp time={createdTime} /> <Timestamp time={createdTime} />
</p> </p>
</div> </div>
<TruncatedComment <TruncatedComment
comment={text} comment={text}
moreHref={moreHref} moreHref={contractPath(contract)}
shouldTruncate={feedType == 'activity'} shouldTruncate={truncate}
/> />
</div> </div>
</> </>
@ -174,27 +177,31 @@ function Timestamp(props: { time: number }) {
) )
} }
function FeedBet(props: { activityItem: any; feedType: FeedType }) { function FeedBet(props: {
const { activityItem, feedType } = props contract: Contract
const { id, contractId, amount, outcome, createdTime, contract } = bet: Bet
activityItem hideOutcome: boolean
}) {
const { contract, bet, hideOutcome } = props
const { id, amount, outcome, createdTime, userId } = bet
const user = useUser() const user = useUser()
const isSelf = user?.id == activityItem.userId const isSelf = user?.id === userId
const isCreator = contract.creatorId == activityItem.userId const isCreator = contract.creatorId === userId
// You can comment if your bet was posted in the last hour // You can comment if your bet was posted in the last hour
const canComment = canAddComment(createdTime, isSelf) const canComment = canAddComment(createdTime, isSelf)
const [comment, setComment] = useState('') const [comment, setComment] = useState('')
async function submitComment() { async function submitComment() {
if (!user || !comment) return if (!user || !comment) return
await createComment(contractId, id, comment, user) await createComment(contract.id, id, comment, user)
} }
const bought = amount >= 0 ? 'bought' : 'sold' const bought = amount >= 0 ? 'bought' : 'sold'
const money = formatMoney(Math.abs(amount)) const money = formatMoney(Math.abs(amount))
const answer = const answer =
feedType !== 'multi' && !hideOutcome &&
(contract.answers?.find((answer: Answer) => answer?.id === outcome) as (contract.answers?.find((answer: Answer) => answer?.id === outcome) as
| Answer | Answer
| undefined) | undefined)
@ -225,8 +232,11 @@ function FeedBet(props: { activityItem: any; feedType: FeedType }) {
{isSelf ? 'You' : isCreator ? contract.creatorName : 'A trader'} {isSelf ? 'You' : isCreator ? contract.creatorName : 'A trader'}
</span>{' '} </span>{' '}
{bought} {money} {bought} {money}
{!answer && ( {!answer && !hideOutcome && (
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} /> <>
{' '}
of <OutcomeLabel outcome={outcome} />
</>
)} )}
<Timestamp time={createdTime} /> <Timestamp time={createdTime} />
{canComment && ( {canComment && (
@ -506,10 +516,8 @@ function FeedDescription(props: { contract: Contract }) {
) )
} }
function FeedAnswer(props: { contract: Contract; outcome: string }) { function FeedCreateAnswer(props: { contract: Contract; answer: Answer }) {
const { contract, outcome } = props const { contract, answer } = props
const answer = contract?.answers?.[Number(outcome) - 1]
if (!answer) return null
return ( return (
<> <>
@ -521,7 +529,7 @@ function FeedAnswer(props: { contract: Contract; outcome: string }) {
name={answer.name} name={answer.name}
username={answer.username} username={answer.username}
/>{' '} />{' '}
submitted answer <OutcomeLabel outcome={outcome} />{' '} submitted answer <OutcomeLabel outcome={answer.id} />{' '}
<Timestamp time={contract.createdTime} /> <Timestamp time={contract.createdTime} />
</div> </div>
</div> </div>
@ -597,12 +605,8 @@ function FeedClose(props: { contract: Contract }) {
) )
} }
function BetGroupSpan(props: { function BetGroupSpan(props: { bets: Bet[]; outcome?: string }) {
bets: Bet[] const { bets, outcome } = props
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
@ -617,15 +621,23 @@ function BetGroupSpan(props: {
{buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>} {buyTotal > 0 && <>bought {formatMoney(buyTotal)} </>}
{sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>} {sellTotal > 0 && <>sold {formatMoney(sellTotal)} </>}
</JoinSpans> </JoinSpans>
<MaybeOutcomeLabel outcome={outcome} feedType={feedType} />{' '} {outcome && (
<>
{' '}
of <OutcomeLabel outcome={outcome} />
</>
)}{' '}
</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; feedType: FeedType }) { function FeedBetGroup(props: {
const { activityItem, feedType } = props contract: Contract
const bets: Bet[] = activityItem.bets bets: Bet[]
hideOutcome: boolean
}) {
const { bets, hideOutcome } = props
const betGroups = _.groupBy(bets, (bet) => bet.outcome) const betGroups = _.groupBy(bets, (bet) => bet.outcome)
const outcomes = Object.keys(betGroups) const outcomes = Object.keys(betGroups)
@ -647,9 +659,8 @@ function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
{outcomes.map((outcome, index) => ( {outcomes.map((outcome, index) => (
<Fragment key={outcome}> <Fragment key={outcome}>
<BetGroupSpan <BetGroupSpan
outcome={outcome} outcome={hideOutcome ? undefined : outcome}
bets={betGroups[outcome]} bets={betGroups[outcome]}
feedType={feedType}
/> />
{index !== outcomes.length - 1 && <br />} {index !== outcomes.length - 1 && <br />}
</Fragment> </Fragment>
@ -661,9 +672,14 @@ function FeedBetGroup(props: { activityItem: any; feedType: FeedType }) {
) )
} }
function FeedAnswerGroup(props: { activityItem: FeedAnswerGroupItem }) { function FeedAnswerGroup(props: {
const { activityItem } = props contract: Contract
const { contract, answer, bets, comments, user } = activityItem answer: Answer
bets: Bet[]
comments: Comment[]
user: User | null | undefined
}) {
const { contract, answer, bets, comments, user } = props
const betGroups = _.groupBy(bets, (bet) => bet.outcome) const betGroups = _.groupBy(bets, (bet) => bet.outcome)
const outcomes = Object.keys(betGroups) const outcomes = Object.keys(betGroups)