manifold/web/components/feed/activity-items.ts
Sinclair Chen 9a11f55762
Rich content (#620)
* 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
2022-07-13 11:58:22 -07:00

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()
}