Add General Comments section to FR answers

This commit is contained in:
Ian Philips 2022-05-03 10:47:28 -04:00
parent 01bbba0db9
commit e1d4600e48
2 changed files with 115 additions and 41 deletions

View File

@ -1,6 +1,6 @@
import _ from 'lodash' import _ from 'lodash'
import { Answer } from '../../../common/answer' import { Answer, getNoneAnswer } from '../../../common/answer'
import { Bet } from '../../../common/bet' import { Bet } from '../../../common/bet'
import { getOutcomeProbability } from '../../../common/calculate' import { getOutcomeProbability } from '../../../common/calculate'
import { Comment } from '../../../common/comment' import { Comment } from '../../../common/comment'
@ -23,6 +23,7 @@ export type ActivityItem =
| CloseItem | CloseItem
| ResolveItem | ResolveItem
| CommentInputItem | CommentInputItem
| GeneralCommentsItem
type BaseActivityItem = { type BaseActivityItem = {
id: string id: string
@ -75,6 +76,11 @@ export type AnswerGroupItem = BaseActivityItem & {
items: ActivityItem[] items: ActivityItem[]
} }
export type GeneralCommentsItem = BaseActivityItem & {
type: 'generalcomments'
items: ActivityItem[]
}
export type CloseItem = BaseActivityItem & { export type CloseItem = BaseActivityItem & {
type: 'close' type: 'close'
} }
@ -83,6 +89,7 @@ export type ResolveItem = BaseActivityItem & {
type: 'resolve' type: 'resolve'
} }
export const GENERAL_COMMENTS_OUTCOME_ID = 'General Comments'
const DAY_IN_MS = 24 * 60 * 60 * 1000 const DAY_IN_MS = 24 * 60 * 60 * 1000
const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3 const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3
@ -276,19 +283,14 @@ function getAnswerAndCommentInputGroups(
outcomes = _.sortBy(outcomes, (outcome) => outcomes = _.sortBy(outcomes, (outcome) =>
getOutcomeProbability(contract, outcome) getOutcomeProbability(contract, 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 || comment.answerOutcome === outcome
)
)
const answer = contract.answers?.find(
(answer) => answer.id === outcome
) as Answer
// Create a list of items for each answer group made up of bets with comments and a comment input function collateCommentsSectionForOutcome(outcome: string) {
const answerBets = bets.filter((bet) => bet.outcome === outcome)
const answerComments = comments.filter(
(comment) =>
comment.answerOutcome === outcome ||
answerBets.some((bet) => bet.id === comment.betId)
)
let items = [] let items = []
items.push({ items.push({
type: 'commentInput' as const, type: 'commentInput' as const,
@ -307,6 +309,16 @@ function getAnswerAndCommentInputGroups(
contract contract
).reverse() ).reverse()
) )
return items
}
const answerGroups = outcomes
.map((outcome) => {
const answer = contract.answers?.find(
(answer) => answer.id === outcome
) as Answer
const items = collateCommentsSectionForOutcome(outcome)
return { return {
id: outcome, id: outcome,
@ -317,7 +329,16 @@ function getAnswerAndCommentInputGroups(
user, user,
} }
}) })
.filter((group) => group.answer) .filter((group) => group.answer) as ActivityItem[]
const outcome = GENERAL_COMMENTS_OUTCOME_ID
const items = collateCommentsSectionForOutcome(outcome)
answerGroups.unshift({
id: outcome,
type: 'generalcomments' as const,
contract,
items,
})
return answerGroups return answerGroups
} }

View File

@ -37,7 +37,7 @@ import { fromNow } from '../../lib/util/time'
import BetRow from '../bet-row' import BetRow from '../bet-row'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { Answer } from '../../../common/answer' import { Answer } from '../../../common/answer'
import { ActivityItem } from './activity-items' import { ActivityItem, GENERAL_COMMENTS_OUTCOME_ID } from './activity-items'
import { import {
Binary, Binary,
CPMM, CPMM,
@ -123,6 +123,8 @@ function FeedItem(props: { item: ActivityItem }) {
return <FeedResolve {...item} /> return <FeedResolve {...item} />
case 'commentInput': case 'commentInput':
return <CommentInput {...item} /> return <CommentInput {...item} />
case 'generalcomments':
return <FeedGeneralComments {...item} />
} }
} }
@ -222,30 +224,46 @@ export function CommentInput(props: {
contract: Contract contract: Contract
betsByCurrentUser: Bet[] betsByCurrentUser: Bet[]
comments: Comment[] comments: Comment[]
// Only for free response comment inputs
answerOutcome?: string answerOutcome?: string
}) { }) {
const { contract, betsByCurrentUser, comments, answerOutcome } = props const { contract, betsByCurrentUser, comments, answerOutcome } = props
const user = useUser() const user = useUser()
const [comment, setComment] = useState('') const [comment, setComment] = useState('')
const [focused, setFocused] = useState(false)
function setFocusedIfFreeResponse(focus: boolean) {
if (answerOutcome != undefined) setFocused(focus)
}
// Should this be oldest bet or most recent bet? // Should this be oldest bet or most recent bet?
const mostRecentCommentableBet = betsByCurrentUser const mostRecentCommentableBet = betsByCurrentUser
.filter( .filter((bet) => {
(bet) => if (
canCommentOnBet(bet, bet.createdTime, user) && canCommentOnBet(bet, user) &&
// The bet doesn't already have a comment
!comments.some((comment) => comment.betId == bet.id) !comments.some((comment) => comment.betId == bet.id)
) {
if (!answerOutcome) return true
// If we're in free response, don't allow commenting on ante bet
return (
bet.outcome !== GENERAL_COMMENTS_OUTCOME_ID &&
answerOutcome === bet.outcome
) )
}
return false
})
.sort((b1, b2) => b1.createdTime - b2.createdTime) .sort((b1, b2) => b1.createdTime - b2.createdTime)
.pop() .pop()
const { id } = mostRecentCommentableBet || { id: undefined } const { id } = mostRecentCommentableBet || { id: undefined }
async function submitComment(id: string | undefined) { async function submitComment(betId: string | undefined) {
if (!comment) return if (!comment) return
if (!user) { if (!user) {
return await firebaseLogin() return await firebaseLogin()
} }
await createComment(contract.id, comment, user, id) await createComment(contract.id, comment, user, betId, answerOutcome)
setComment('') setComment('')
} }
@ -286,7 +304,9 @@ export function CommentInput(props: {
onChange={(e) => setComment(e.target.value)} onChange={(e) => setComment(e.target.value)}
className="textarea textarea-bordered w-full resize-none" className="textarea textarea-bordered w-full resize-none"
placeholder="Add a comment..." placeholder="Add a comment..."
rows={3} rows={answerOutcome == undefined || focused ? 3 : 1}
onFocus={() => setFocusedIfFreeResponse(true)}
onBlur={() => !comment && setFocusedIfFreeResponse(false)}
maxLength={MAX_COMMENT_LENGTH} maxLength={MAX_COMMENT_LENGTH}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
@ -298,7 +318,10 @@ export function CommentInput(props: {
className={ className={
'btn btn-outline btn-sm text-transform: mt-1 capitalize' 'btn btn-outline btn-sm text-transform: mt-1 capitalize'
} }
onClick={() => submitComment(id)} onClick={() => {
submitComment(id)
setFocusedIfFreeResponse(false)
}}
> >
{user ? 'Comment' : 'Sign in to comment'} {user ? 'Comment' : 'Sign in to comment'}
</button> </button>
@ -561,12 +584,11 @@ export function FeedQuestion(props: {
) )
} }
function canCommentOnBet(bet: Bet, createdTime: number, user?: User | null) { function canCommentOnBet(bet: Bet, user?: User | null) {
const isSelf = user?.id === bet.userId const { userId, createdTime, isRedemption } = bet
const isSelf = user?.id === 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
return ( return !isRedemption && isSelf && Date.now() - createdTime < 60 * 60 * 1000
!bet.isRedemption && isSelf && Date.now() - createdTime < 60 * 60 * 1000
)
} }
function FeedDescription(props: { contract: Contract }) { function FeedDescription(props: { contract: Contract }) {
@ -835,6 +857,37 @@ function FeedAnswerGroup(props: {
) )
} }
function FeedGeneralComments(props: {
contract: FullContract<any, FreeResponse>
items: ActivityItem[]
type: string
}) {
const { items } = props
return (
<Col className={'mt-8 flex w-full '}>
<div className={'text-md mt-8 mb-2 text-left'}>General Comments</div>
<div className={'w-full border-b border-gray-200'} />
{items.map((item, index) => (
<div
key={item.id}
className={clsx('relative', index !== items.length - 1 && 'pb-4')}
>
{index !== items.length - 1 ? (
<span
className="absolute top-5 left-5 -ml-px h-[calc(100%-1rem)] w-0.5 bg-gray-200"
aria-hidden="true"
/>
) : null}
<div className="relative flex items-start space-x-3">
<FeedItem item={item} />
</div>
</div>
))}
</Col>
)
}
// TODO: Should highlight the entire Feed segment // TODO: Should highlight the entire Feed segment
function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) { function FeedExpand(props: { setExpanded: (expanded: boolean) => void }) {
const { setExpanded } = props const { setExpanded } = props