import { Bet } from 'common/bet' import { Comment } from 'common/comment' import { User } from 'common/user' import { Contract } from 'common/contract' import React, { useEffect, useState } from 'react' import { minBy, maxBy, groupBy, partition, sumBy } from 'lodash' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' import { useRouter } from 'next/router' import { Row } from 'web/components/layout/row' import clsx from 'clsx' import { Avatar } from 'web/components/avatar' import { UserLink } from 'web/components/user-page' import { OutcomeLabel } from 'web/components/outcome-label' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { contractPath } from 'web/lib/firebase/contracts' import { firebaseLogin } from 'web/lib/firebase/users' import { createComment, MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' import Textarea from 'react-expanding-textarea' import { Linkify } from 'web/components/linkify' import { SiteLink } from 'web/components/site-link' import { BetStatusText } from 'web/components/feed/feed-bets' import { Col } from 'web/components/layout/col' import { getProbability } from 'common/calculate' import { LoadingIndicator } from 'web/components/loading-indicator' export function FeedCommentThread(props: { contract: Contract comments: Comment[] parentComment: Comment bets: Bet[] truncate?: boolean smallAvatar?: boolean }) { const { contract, comments, bets, truncate, smallAvatar, parentComment } = props const [showReply, setShowReply] = useState(false) const [replyToUsername, setReplyToUsername] = useState('') const betsByUserId = groupBy(bets, (bet) => bet.userId) const user = useUser() const commentsList = comments.filter( (comment) => parentComment.id && comment.replyToCommentId === parentComment.id ) commentsList.unshift(parentComment) const [inputRef, setInputRef] = useState(null) function scrollAndOpenReplyInput(comment: Comment) { setReplyToUsername(comment.userUsername) setShowReply(true) inputRef?.focus() } useEffect(() => { if (showReply && inputRef) inputRef.focus() }, [inputRef, showReply]) return (
{commentsList.map((comment, commentIdx) => (
{ return bet.createdTime < comment.createdTime ? comment.createdTime - bet.createdTime : comment.createdTime })?.probAfter : undefined } smallAvatar={smallAvatar} truncate={truncate} />
))} {showReply && (
)}
) } export function FeedComment(props: { contract: Contract comment: Comment betsBySameUser: Bet[] probAtCreatedTime?: number truncate?: boolean smallAvatar?: boolean onReplyClick?: (comment: Comment) => void }) { const { contract, comment, betsBySameUser, probAtCreatedTime, truncate, smallAvatar, onReplyClick, } = props const { text, userUsername, userName, userAvatarUrl, createdTime } = comment let betOutcome: string | undefined, bought: string | undefined, money: string | undefined const matchedBet = betsBySameUser.find((bet) => bet.id === comment.betId) if (matchedBet) { betOutcome = matchedBet.outcome bought = matchedBet.amount >= 0 ? 'bought' : 'sold' money = formatMoney(Math.abs(matchedBet.amount)) } const [highlighted, setHighlighted] = useState(false) const router = useRouter() useEffect(() => { if (router.asPath.endsWith(`#${comment.id}`)) { setHighlighted(true) } }, [comment.id, router.asPath]) // Only calculated if they don't have a matching bet const { userPosition, outcome } = getBettorsLargestPositionBeforeTime( contract, comment.createdTime, matchedBet ? [] : betsBySameUser ) return (

{' '} {!matchedBet && userPosition > 0 && contract.outcomeType !== 'NUMERIC' && ( <> {'is '} )} <> {bought} {money} {contract.outcomeType !== 'FREE_RESPONSE' && betOutcome && ( <> {' '} of{' '} )}

{onReplyClick && ( )}
) } export function getMostRecentCommentableBet( betsByCurrentUser: Bet[], commentsByCurrentUser: Comment[], user?: User | null, answerOutcome?: string ) { return betsByCurrentUser .filter((bet) => { if ( canCommentOnBet(bet, user) && !commentsByCurrentUser.some( (comment) => comment.createdTime > bet.createdTime ) ) { if (!answerOutcome) return true // If we're in free response, don't allow commenting on ante bet return answerOutcome === bet.outcome } return false }) .sort((b1, b2) => b1.createdTime - b2.createdTime) .pop() } function CommentStatus(props: { contract: Contract outcome: string prob?: number }) { const { contract, outcome, prob } = props return ( <> {' betting '} {prob && ' at ' + Math.round(prob * 100) + '%'} ) } export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] commentsByCurrentUser: Comment[] // Tie a comment to an free response answer outcome answerOutcome?: string // Tie a comment to another comment parentComment?: Comment replyToUsername?: string setRef?: (ref: HTMLTextAreaElement) => void }) { const { contract, betsByCurrentUser, commentsByCurrentUser, answerOutcome, parentComment, replyToUsername, setRef, } = props const user = useUser() const [comment, setComment] = useState('') const [focused, setFocused] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const mostRecentCommentableBet = getMostRecentCommentableBet( betsByCurrentUser, commentsByCurrentUser, user, answerOutcome ) const { id } = mostRecentCommentableBet || { id: undefined } useEffect(() => { if (!replyToUsername || !user || replyToUsername === user.username) return const replacement = `@${replyToUsername} ` setComment((comment) => replacement + comment.replace(replacement, '')) }, [user, replyToUsername]) async function submitComment(betId: string | undefined) { if (!user) { return await firebaseLogin() } if (!comment || isSubmitting) return setIsSubmitting(true) await createComment( contract.id, comment, user, betId, answerOutcome, parentComment?.id ) setComment('') setFocused(false) setIsSubmitting(false) } const { userPosition, outcome } = getBettorsLargestPositionBeforeTime( contract, Date.now(), betsByCurrentUser ) const shouldCollapseAfterClickOutside = false const isNumeric = contract.outcomeType === 'NUMERIC' return ( <>
{mostRecentCommentableBet && ( )} {!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && ( <> {"You're"} )}