833dd37469
* Add tip arrows UI (visual) * move tipper into its own component * simplify score calculation * Add tip txns - more specific txn types - fix transact cloud function to be able to create tip txns - insert tips into comments via a context * Refactor tipper to send tip txns * Stop tipping yourself. Disable anons. * Style tipper (smaller) * remove default exports * capitalize tooltips * rename stuff * add exhausting hook dependencies * replace context with prop threading * fix eslint unused vars * fix: thread tips correctly into fr comments
211 lines
4.6 KiB
TypeScript
211 lines
4.6 KiB
TypeScript
import { uniq, sortBy } from 'lodash'
|
|
|
|
import { Answer } from 'common/answer'
|
|
import { Bet } from 'common/bet'
|
|
import { getOutcomeProbability } from 'common/calculate'
|
|
import { Comment } from 'common/comment'
|
|
import { Contract, FreeResponseContract } from 'common/contract'
|
|
import { User } from 'common/user'
|
|
import { CommentTipMap } from 'web/hooks/use-tip-txns'
|
|
|
|
export type ActivityItem =
|
|
| DescriptionItem
|
|
| QuestionItem
|
|
| BetItem
|
|
| AnswerGroupItem
|
|
| CloseItem
|
|
| ResolveItem
|
|
| CommentInputItem
|
|
| CommentThreadItem
|
|
|
|
type BaseActivityItem = {
|
|
id: string
|
|
contract: Contract
|
|
}
|
|
|
|
export type CommentInputItem = BaseActivityItem & {
|
|
type: 'commentInput'
|
|
betsByCurrentUser: Bet[]
|
|
commentsByCurrentUser: Comment[]
|
|
}
|
|
|
|
export type DescriptionItem = BaseActivityItem & {
|
|
type: 'description'
|
|
}
|
|
|
|
export type QuestionItem = BaseActivityItem & {
|
|
type: 'question'
|
|
showDescription: boolean
|
|
contractPath?: string
|
|
}
|
|
|
|
export type BetItem = BaseActivityItem & {
|
|
type: 'bet'
|
|
bet: Bet
|
|
hideOutcome: boolean
|
|
smallAvatar: boolean
|
|
hideComment?: boolean
|
|
}
|
|
|
|
export type CommentThreadItem = BaseActivityItem & {
|
|
type: 'commentThread'
|
|
parentComment: Comment
|
|
comments: Comment[]
|
|
tips: CommentTipMap
|
|
bets: Bet[]
|
|
}
|
|
|
|
export type AnswerGroupItem = BaseActivityItem & {
|
|
type: 'answergroup'
|
|
user: User | undefined | null
|
|
answer: Answer
|
|
comments: Comment[]
|
|
tips: CommentTipMap
|
|
bets: Bet[]
|
|
}
|
|
|
|
export type CloseItem = BaseActivityItem & {
|
|
type: 'close'
|
|
}
|
|
|
|
export type ResolveItem = BaseActivityItem & {
|
|
type: 'resolve'
|
|
}
|
|
|
|
function getAnswerAndCommentInputGroups(
|
|
contract: FreeResponseContract,
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
tips: CommentTipMap,
|
|
user: User | undefined | null
|
|
) {
|
|
let outcomes = uniq(bets.map((bet) => bet.outcome))
|
|
outcomes = sortBy(outcomes, (outcome) =>
|
|
getOutcomeProbability(contract, outcome)
|
|
)
|
|
|
|
const answerGroups = outcomes
|
|
.map((outcome) => {
|
|
const answer = contract.answers?.find(
|
|
(answer) => answer.id === outcome
|
|
) as Answer
|
|
|
|
return {
|
|
id: outcome,
|
|
type: 'answergroup' as const,
|
|
contract,
|
|
user,
|
|
answer,
|
|
comments,
|
|
tips,
|
|
bets,
|
|
}
|
|
})
|
|
.filter((group) => group.answer) as ActivityItem[]
|
|
return answerGroups
|
|
}
|
|
|
|
function getCommentThreads(
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
tips: CommentTipMap,
|
|
contract: Contract
|
|
) {
|
|
const parentComments = comments.filter((comment) => !comment.replyToCommentId)
|
|
|
|
const items = parentComments.map((comment) => ({
|
|
type: 'commentThread' as const,
|
|
id: comment.id,
|
|
contract: contract,
|
|
comments: comments,
|
|
parentComment: comment,
|
|
bets: bets,
|
|
tips,
|
|
}))
|
|
|
|
return items
|
|
}
|
|
|
|
function commentIsGeneralComment(comment: Comment, contract: Contract) {
|
|
return (
|
|
comment.answerOutcome === undefined &&
|
|
(contract.outcomeType === 'FREE_RESPONSE'
|
|
? comment.betId === undefined
|
|
: true)
|
|
)
|
|
}
|
|
|
|
export function getSpecificContractActivityItems(
|
|
contract: Contract,
|
|
bets: Bet[],
|
|
comments: Comment[],
|
|
tips: CommentTipMap,
|
|
user: User | null | undefined,
|
|
options: {
|
|
mode: 'comments' | 'bets' | 'free-response-comment-answer-groups'
|
|
}
|
|
) {
|
|
const { mode } = options
|
|
const items = [] as ActivityItem[]
|
|
|
|
switch (mode) {
|
|
case 'bets':
|
|
// Remove first bet (which is the ante):
|
|
if (contract.outcomeType === 'FREE_RESPONSE') bets = bets.slice(1)
|
|
items.push(
|
|
...bets.map((bet) => ({
|
|
type: 'bet' as const,
|
|
id: bet.id,
|
|
bet,
|
|
contract,
|
|
hideOutcome: false,
|
|
smallAvatar: false,
|
|
hideComment: true,
|
|
}))
|
|
)
|
|
break
|
|
|
|
case 'comments': {
|
|
const nonFreeResponseComments = comments.filter((comment) =>
|
|
commentIsGeneralComment(comment, contract)
|
|
)
|
|
const nonFreeResponseBets =
|
|
contract.outcomeType === 'FREE_RESPONSE' ? [] : bets
|
|
items.push(
|
|
...getCommentThreads(
|
|
nonFreeResponseBets,
|
|
nonFreeResponseComments,
|
|
tips,
|
|
contract
|
|
)
|
|
)
|
|
|
|
items.push({
|
|
type: 'commentInput',
|
|
id: 'commentInput',
|
|
contract,
|
|
betsByCurrentUser: nonFreeResponseBets.filter(
|
|
(bet) => bet.userId === user?.id
|
|
),
|
|
commentsByCurrentUser: nonFreeResponseComments.filter(
|
|
(comment) => comment.userId === user?.id
|
|
),
|
|
})
|
|
break
|
|
}
|
|
case 'free-response-comment-answer-groups':
|
|
items.push(
|
|
...getAnswerAndCommentInputGroups(
|
|
contract as FreeResponseContract,
|
|
bets,
|
|
comments,
|
|
tips,
|
|
user
|
|
)
|
|
)
|
|
break
|
|
}
|
|
|
|
return items.reverse()
|
|
}
|