Add General Comments section to FR answers
This commit is contained in:
parent
01bbba0db9
commit
e1d4600e48
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user