diff --git a/common/comment.ts b/common/comment.ts index 5daeb37e..15cfbcb5 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -4,6 +4,7 @@ export type Comment = { id: string contractId: string betId?: string + answerOutcome?: string userId: string text: string diff --git a/og-image/api/_lib/template.ts b/og-image/api/_lib/template.ts index 00d47394..a6b0336c 100644 --- a/og-image/api/_lib/template.ts +++ b/og-image/api/_lib/template.ts @@ -93,7 +93,8 @@ export function getHtml(parsedReq: ParsedRequest) { creatorAvatarUrl, } = parsedReq const MAX_QUESTION_CHARS = 100 - const truncatedQuestion = question.length > MAX_QUESTION_CHARS + const truncatedQuestion = + question.length > MAX_QUESTION_CHARS ? question.slice(0, MAX_QUESTION_CHARS) + '...' : question const hideAvatar = creatorAvatarUrl ? '' : 'hidden' diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 2ba6cde8..ae1203cf 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -7,6 +7,7 @@ import { ContractActivity } from '../feed/contract-activity' import { ContractBetsTable, MyBetsSummary } from '../bets-list' import { Spacer } from '../layout/spacer' import { Tabs } from '../layout/tabs' +import { Col } from '../layout/col' export function ContractTabs(props: { contract: Contract @@ -33,14 +34,34 @@ export function ContractTabs(props: { ) const commentActivity = ( - + <> + + {contract.outcomeType === 'FREE_RESPONSE' && ( + +
General Comments
+
+ + + )} + ) const yourTrades = ( diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index 40dd2338..71d42621 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -33,6 +33,7 @@ export type CommentInputItem = BaseActivityItem & { type: 'commentInput' betsByCurrentUser: Bet[] comments: Comment[] + answerOutcome?: string } export type DescriptionItem = BaseActivityItem & { @@ -82,6 +83,7 @@ export type ResolveItem = BaseActivityItem & { type: 'resolve' } +export const GENERAL_COMMENTS_OUTCOME_ID = 'General Comments' const DAY_IN_MS = 24 * 60 * 60 * 1000 const ABBREVIATED_NUM_COMMENTS_OR_BETS_TO_SHOW = 3 @@ -263,6 +265,68 @@ function getAnswerGroups( return answerGroups } +function getAnswerAndCommentInputGroups( + contract: FullContract, + bets: Bet[], + comments: Comment[], + user: User | undefined | null +) { + let outcomes = _.uniq(bets.map((bet) => bet.outcome)).filter( + (outcome) => getOutcomeProbability(contract, outcome) > 0.0001 + ) + outcomes = _.sortBy(outcomes, (outcome) => + getOutcomeProbability(contract, outcome) + ) + + 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 = [] + items.push({ + type: 'commentInput' as const, + id: 'commentInputFor' + outcome, + contract, + betsByCurrentUser: user + ? bets.filter((bet) => bet.userId === user.id) + : [], + comments: comments, + answerOutcome: outcome, + }) + items.push( + ...getCommentsWithPositions( + answerBets, + answerComments, + contract + ).reverse() + ) + return items + } + + const answerGroups = outcomes + .map((outcome) => { + const answer = contract.answers?.find( + (answer) => answer.id === outcome + ) as Answer + + const items = collateCommentsSectionForOutcome(outcome) + + return { + id: outcome, + type: 'answergroup' as const, + contract, + answer, + items, + user, + } + }) + .filter((group) => group.answer) as ActivityItem[] + return answerGroups +} + function groupBetsAndComments( bets: Bet[], comments: Comment[], @@ -382,7 +446,7 @@ export function getAllContractActivityItems( ) ) items.push({ - type: 'commentInput', + type: 'commentInput' as const, id: 'commentInput', contract, betsByCurrentUser: [], @@ -408,7 +472,7 @@ export function getAllContractActivityItems( if (outcomeType === 'BINARY') { items.push({ - type: 'commentInput', + type: 'commentInput' as const, id: 'commentInput', contract, betsByCurrentUser: [], @@ -479,7 +543,7 @@ export function getSpecificContractActivityItems( comments: Comment[], user: User | null | undefined, options: { - mode: 'comments' | 'bets' + mode: 'comments' | 'bets' | 'free-response-comment-answer-groups' } ) { const { mode } = options @@ -501,18 +565,39 @@ export function getSpecificContractActivityItems( break case 'comments': - items.push(...getCommentsWithPositions(bets, comments, contract)) + const nonFreeResponseComments = comments.filter( + (comment) => comment.answerOutcome === undefined + ) + const nonFreeResponseBets = + contract.outcomeType === 'FREE_RESPONSE' ? [] : bets + items.push( + ...getCommentsWithPositions( + nonFreeResponseBets, + nonFreeResponseComments, + contract + ) + ) items.push({ type: 'commentInput', id: 'commentInput', contract, betsByCurrentUser: user - ? bets.filter((bet) => bet.userId === user.id) + ? nonFreeResponseBets.filter((bet) => bet.userId === user.id) : [], - comments: comments, + comments: nonFreeResponseComments, }) break + case 'free-response-comment-answer-groups': + items.push( + ...getAnswerAndCommentInputGroups( + contract as FullContract, + bets, + comments, + user + ) + ) + break } return items.reverse() diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index f65c6716..a0e40916 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -16,7 +16,13 @@ export function ContractActivity(props: { bets: Bet[] comments: Comment[] user: User | null | undefined - mode: 'only-recent' | 'abbreviated' | 'all' | 'comments' | 'bets' + mode: + | 'only-recent' + | 'abbreviated' + | 'all' + | 'comments' + | 'bets' + | 'free-response-comment-answer-groups' contractPath?: string className?: string betRowClassName?: string @@ -38,7 +44,9 @@ export function ContractActivity(props: { ? getRecentContractActivityItems(contract, bets, comments, user, { contractPath, }) - : mode === 'comments' || mode === 'bets' + : mode === 'comments' || + mode === 'bets' || + mode === 'free-response-comment-answer-groups' ? getSpecificContractActivityItems(contract, bets, comments, user, { mode, }) diff --git a/web/components/feed/feed-items.tsx b/web/components/feed/feed-items.tsx index a3b650c0..3627a6b4 100644 --- a/web/components/feed/feed-items.tsx +++ b/web/components/feed/feed-items.tsx @@ -37,7 +37,7 @@ import { fromNow } from '../../lib/util/time' import BetRow from '../bet-row' import { Avatar } from '../avatar' import { Answer } from '../../../common/answer' -import { ActivityItem } from './activity-items' +import { ActivityItem, GENERAL_COMMENTS_OUTCOME_ID } from './activity-items' import { Binary, CPMM, @@ -222,29 +222,42 @@ export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] comments: Comment[] + // Only for free response comment inputs + answerOutcome?: string }) { - const { contract, betsByCurrentUser, comments } = props + const { contract, betsByCurrentUser, comments, answerOutcome } = props const user = useUser() const [comment, setComment] = useState('') + const [focused, setFocused] = useState(false) // Should this be oldest bet or most recent bet? const mostRecentCommentableBet = betsByCurrentUser - .filter( - (bet) => - canCommentOnBet(bet, bet.createdTime, user) && + .filter((bet) => { + if ( + canCommentOnBet(bet, user) && + // The bet doesn't already have a comment !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) .pop() const { id } = mostRecentCommentableBet || { id: undefined } - async function submitComment(id: string | undefined) { - if (!comment) return + async function submitComment(betId: string | undefined) { if (!user) { return await firebaseLogin() } - await createComment(contract.id, comment, user, id) + if (!comment) return + await createComment(contract.id, comment, user, betId, answerOutcome) setComment('') } @@ -253,11 +266,11 @@ export function CommentInput(props: { return ( <> - +
-
+
{mostRecentCommentableBet && ( )} -
-