9a11f55762
* Add TipTap editor and renderer components * Change market description editor to rich text * Type description as JSON, fix string-based logic - Delete make-predictions.tsx - Delete feed logic that showed descriptions * wip Fix API validation * fix type error * fix extension import (backend) In firebase, typescript compiles imports into common js imports like `const StarterKit = require("@tiptap/starter-kit")` Even though StarterKit is exported from the cjs file, it gets imported as undefined. But it magically works if we import * If you're reading this in the future, consider replacing StarterKit with the entire list of extensions. * Stop load on fail create market, improve warning * Refactor editor as hook / fix infinite submit bug Move state of editor back up to parent We have to do this later anyways to allow parent to edit * Add images - display, paste + uploading * add uploading state of image * Fix placeholder, misc styling min height, quote * Fix appending to description * code review fixes: rename, refactor, chop carets * Add hint & upload button on new lines - bump to Tailwind 3.1 for arbitrary variants * clean up, run prettier * rename FileButton to FileUploadButton * add image extension as functions dependency
238 lines
5.4 KiB
TypeScript
238 lines
5.4 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'
|
|
import { LiquidityProvision } from 'common/liquidity-provision'
|
|
|
|
export type ActivityItem =
|
|
| DescriptionItem
|
|
| QuestionItem
|
|
| BetItem
|
|
| AnswerGroupItem
|
|
| CloseItem
|
|
| ResolveItem
|
|
| CommentInputItem
|
|
| CommentThreadItem
|
|
| LiquidityItem
|
|
|
|
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'
|
|
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'
|
|
}
|
|
|
|
export type LiquidityItem = BaseActivityItem & {
|
|
type: 'liquidity'
|
|
liquidity: LiquidityProvision
|
|
hideOutcome: boolean
|
|
smallAvatar: boolean
|
|
hideComment?: boolean
|
|
}
|
|
|
|
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[],
|
|
liquidityProvisions: LiquidityProvision[],
|
|
tips: CommentTipMap,
|
|
user: User | null | undefined,
|
|
options: {
|
|
mode: 'comments' | 'bets' | 'free-response-comment-answer-groups'
|
|
}
|
|
) {
|
|
const { mode } = options
|
|
let 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.isSold,
|
|
bet,
|
|
contract,
|
|
hideOutcome: false,
|
|
smallAvatar: false,
|
|
hideComment: true,
|
|
}))
|
|
)
|
|
items.push(
|
|
...liquidityProvisions.map((liquidity) => ({
|
|
type: 'liquidity' as const,
|
|
id: liquidity.id,
|
|
contract,
|
|
liquidity,
|
|
hideOutcome: false,
|
|
smallAvatar: false,
|
|
}))
|
|
)
|
|
items = sortBy(items, (item) =>
|
|
item.type === 'bet'
|
|
? item.bet.createdTime
|
|
: item.type === 'liquidity'
|
|
? item.liquidity.createdTime
|
|
: undefined
|
|
)
|
|
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()
|
|
}
|