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, Dictionary } 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' import { PaperAirplaneIcon } from '@heroicons/react/outline' import { track } from 'web/lib/service/analytics' 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 (
) } export function CommentRepliesList(props: { contract: Contract commentsList: Comment[] betsByUserId: Dictionary scrollAndOpenReplyInput: (comment: Comment) => void bets: Bet[] treatFirstIndexEqually?: boolean smallAvatar?: boolean truncate?: boolean }) { const { contract, commentsList, betsByUserId, truncate, smallAvatar, bets, scrollAndOpenReplyInput, treatFirstIndexEqually, } = props return ( <> {commentsList.map((comment, commentIdx) => (
{/*draw a gray line from the comment to the left:*/} {(treatFirstIndexEqually || commentIdx != 0) && (
))} ) } 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, 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 ) { let sortedBetsByCurrentUser = betsByCurrentUser.sort( (a, b) => b.createdTime - a.createdTime ) if (answerOutcome) { sortedBetsByCurrentUser = sortedBetsByCurrentUser.slice(0, 1) } return sortedBetsByCurrentUser .filter((bet) => { if ( canCommentOnBet(bet, user) && !commentsByCurrentUser.some( (comment) => comment.createdTime > bet.createdTime ) ) { if (!answerOutcome) return true return answerOutcome === bet.outcome } return false }) .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[] replyToUsername?: string setRef?: (ref: HTMLTextAreaElement) => void // Reply to a free response answer parentAnswerOutcome?: string // Reply to another comment parentCommentId?: string onSubmitComment?: () => void }) { const { contract, betsByCurrentUser, commentsByCurrentUser, parentAnswerOutcome, parentCommentId, replyToUsername, onSubmitComment, setRef, } = props const user = useUser() const [comment, setComment] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const mostRecentCommentableBet = getMostRecentCommentableBet( betsByCurrentUser, commentsByCurrentUser, user, parentAnswerOutcome ) 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) { track('sign in to comment') return await firebaseLogin() } if (!comment || isSubmitting) return setIsSubmitting(true) await createComment( contract.id, comment, user, betId, parentAnswerOutcome, parentCommentId ) onSubmitComment?.() setComment('') setIsSubmitting(false) } const { userPosition, outcome } = getBettorsLargestPositionBeforeTime( contract, Date.now(), betsByCurrentUser ) const isNumeric = contract.outcomeType === 'NUMERIC' return ( <>
{mostRecentCommentableBet && ( )} {!mostRecentCommentableBet && user && userPosition > 0 && !isNumeric && ( <> {"You're"} )}