250 lines
6.7 KiB
TypeScript
250 lines
6.7 KiB
TypeScript
import _ from 'lodash'
|
|
|
|
import { Answer } from '../../../common/answer'
|
|
import { Bet } from '../../../common/bet'
|
|
import { Comment } from '../../../common/comment'
|
|
import { Contract } from '../../../common/contract'
|
|
import { User } from '../../../common/user'
|
|
import { filterDefined } from '../../../common/util/array'
|
|
import { canAddComment, mapCommentsByBetId } from '../../lib/firebase/comments'
|
|
import { fromNow } from '../../lib/util/time'
|
|
|
|
export type ActivityItem = {
|
|
id: string
|
|
type:
|
|
| 'bet'
|
|
| 'comment'
|
|
| 'start'
|
|
| 'betgroup'
|
|
| 'answergroup'
|
|
| 'close'
|
|
| 'resolve'
|
|
| 'expand'
|
|
| undefined
|
|
}
|
|
|
|
export type FeedAnswerGroupItem = ActivityItem & {
|
|
type: 'answergroup'
|
|
contract: Contract
|
|
bets: Bet[]
|
|
comments: Comment[]
|
|
answer: Answer
|
|
user: User | null | undefined
|
|
}
|
|
|
|
const DAY_IN_MS = 24 * 60 * 60 * 1000
|
|
|
|
// Group together bets that are:
|
|
// - Within `windowMs` of the first in the group
|
|
// - Do not have a comment
|
|
// - Were not created by this user or the contract creator
|
|
// Return a list of ActivityItems
|
|
function groupBets(
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
windowMs: number,
|
|
contract: Contract,
|
|
userId?: string
|
|
) {
|
|
const commentsMap = mapCommentsByBetId(comments)
|
|
const items: any[] = []
|
|
let group: Bet[] = []
|
|
|
|
// Turn the current group into an ActivityItem
|
|
function pushGroup() {
|
|
if (group.length == 1) {
|
|
items.push(toActivityItem(group[0], false))
|
|
} else if (group.length > 1) {
|
|
items.push({ type: 'betgroup', bets: [...group], id: group[0].id })
|
|
}
|
|
group = []
|
|
}
|
|
|
|
function toActivityItem(bet: Bet, isPublic: boolean) {
|
|
const comment = commentsMap[bet.id]
|
|
return comment ? toFeedComment(bet, comment) : toFeedBet(bet, contract)
|
|
}
|
|
|
|
for (const bet of bets) {
|
|
const isCreator = userId === bet.userId || contract.creatorId === bet.userId
|
|
|
|
if (commentsMap[bet.id] || isCreator) {
|
|
pushGroup()
|
|
// Create a single item for this
|
|
items.push(toActivityItem(bet, true))
|
|
} else {
|
|
if (
|
|
group.length > 0 &&
|
|
bet.createdTime - group[0].createdTime > windowMs
|
|
) {
|
|
// More than `windowMs` has passed; start a new group
|
|
pushGroup()
|
|
}
|
|
group.push(bet)
|
|
}
|
|
}
|
|
if (group.length > 0) {
|
|
pushGroup()
|
|
}
|
|
return items as ActivityItem[]
|
|
}
|
|
|
|
function getAnswerGroups(
|
|
contract: Contract,
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
user: User | undefined | null
|
|
) {
|
|
// Keep last two comments.
|
|
comments = comments.slice(-2)
|
|
const lastBet = bets[bets.length - 1]
|
|
|
|
// Include up to 2 outcomes from comments and last bet.
|
|
const outcomes = filterDefined(
|
|
_.uniq([
|
|
...comments.map(
|
|
(comment) => bets.find((bet) => bet.id === comment.betId)?.outcome
|
|
),
|
|
lastBet?.outcome,
|
|
])
|
|
).slice(0, 2)
|
|
|
|
// Keep bets on selected outcomes.
|
|
bets = bets.filter((bet) => outcomes.includes(bet.outcome))
|
|
|
|
const answerGroups = outcomes.map((outcome) => {
|
|
const answerBets = bets.filter((bet) => bet.outcome === outcome)
|
|
const answerComments = comments.filter((comment) =>
|
|
answerBets.some((bet) => bet.id === comment.betId)
|
|
)
|
|
const answer = contract.answers?.find(
|
|
(answer) => answer.id === outcome
|
|
) as Answer
|
|
|
|
return {
|
|
id: outcome,
|
|
type: 'answergroup' as const,
|
|
contract,
|
|
answer,
|
|
bets: answerBets,
|
|
comments: answerComments,
|
|
user,
|
|
}
|
|
})
|
|
|
|
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(
|
|
contract: Contract,
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
user: User | null | undefined,
|
|
outcome?: string
|
|
) {
|
|
const { outcomeType } = contract
|
|
const isBinary = outcomeType === 'BINARY'
|
|
|
|
bets = isBinary
|
|
? bets.filter((bet) => !bet.isAnte)
|
|
: bets.filter((bet) => !(bet.isAnte && (bet.outcome as string) === '0'))
|
|
|
|
if (outcome) {
|
|
bets = bets.filter((bet) => bet.outcome === outcome)
|
|
} else if (outcomeType === 'FREE_RESPONSE') {
|
|
// Keep bets on comments or your bets where you can comment.
|
|
const commentBetIds = new Set(comments.map((comment) => comment.betId))
|
|
bets = bets.filter(
|
|
(bet) =>
|
|
commentBetIds.has(bet.id) ||
|
|
canAddComment(bet.createdTime, user?.id === bet.userId)
|
|
)
|
|
}
|
|
|
|
const items: ActivityItem[] = outcome ? [] : [{ type: 'start', id: '0' }]
|
|
|
|
items.push(...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}` })
|
|
}
|
|
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
|
|
}
|
|
|
|
export function getRecentContractActivityItems(
|
|
contract: Contract,
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
user: User | null | undefined
|
|
) {
|
|
bets = bets.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
|
comments = comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
|
|
|
|
const items: ActivityItem[] = [{ type: 'start', id: '0' }]
|
|
items.push(
|
|
...(contract.outcomeType === 'FREE_RESPONSE'
|
|
? getAnswerGroups(contract, bets, comments, user)
|
|
: 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.
|
|
const betGroups = items.filter((item) => item.type === 'betgroup')
|
|
const lastBetGroup = betGroups[betGroups.length - 1]
|
|
const filtered = items.filter(
|
|
(item) => item.type !== 'betgroup' || item.id === lastBetGroup?.id
|
|
)
|
|
|
|
// Only show the first item plus the last three items.
|
|
return filtered.length > 3 ? [filtered[0], ...filtered.slice(-3)] : filtered
|
|
}
|