// From https://tailwindui.com/components/application-ui/lists/feeds import React, { Fragment, useEffect, useRef, useState } from 'react' import * as _ from 'lodash' import { Dictionary } from 'lodash' import { BanIcon, CheckIcon, DotsVerticalIcon, LockClosedIcon, UserIcon, UsersIcon, XIcon, } from '@heroicons/react/solid' import clsx from 'clsx' import Textarea from 'react-expanding-textarea' import { OutcomeLabel } from '../outcome-label' import { Contract, contractMetrics, contractPath, tradingAllowed, } from 'web/lib/firebase/contracts' import { useUser } from 'web/hooks/use-user' import { Linkify } from '../linkify' import { Row } from '../layout/row' import { createComment, MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' import { formatMoney } from 'common/util/format' import { Comment } from 'common/comment' import { BinaryResolutionOrChance } from '../contract/contract-card' import { SiteLink } from '../site-link' import { Col } from '../layout/col' import { UserLink } from '../user-page' import { Bet } from 'web/lib/firebase/bets' import { JoinSpans } from '../join-spans' import BetRow from '../bet-row' import { Avatar } from '../avatar' import { ActivityItem } from './activity-items' import { Binary, CPMM, FullContract } from 'common/contract' import { useSaveSeenContract } from 'web/hooks/use-seen-contracts' import { User } from 'common/user' import { trackClick } from 'web/lib/firebase/tracking' import { firebaseLogin } from 'web/lib/firebase/users' import { DAY_MS } from 'common/util/time' import NewContractBadge from '../new-contract-badge' import { RelativeTimestamp } from '../relative-timestamp' import { calculateCpmmSale } from 'common/calculate-cpmm' import { useRouter } from 'next/router' import { FeedAnswerCommentGroup } from 'web/components/feed/feed-answer-comment-group' import { getMostRecentCommentableBet } from 'web/components/feed/feed-comments' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' export function FeedItems(props: { contract: Contract items: ActivityItem[] className?: string betRowClassName?: string }) { const { contract, items, className, betRowClassName } = props const { outcomeType } = contract const [elem, setElem] = useState(null) useSaveSeenContract(elem, contract) return (
{items.map((item, activityItemIdx) => (
{activityItemIdx !== items.length - 1 || item.type === 'answergroup' ? (
))}
{outcomeType === 'BINARY' && tradingAllowed(contract) && ( )}
) } export function FeedItem(props: { item: ActivityItem }) { const { item } = props switch (item.type) { case 'question': return case 'description': return case 'comment': return case 'bet': return case 'betgroup': return case 'answergroup': return case 'close': return case 'resolve': return case 'commentInput': return case 'commentThread': return } } export function FeedCommentThread(props: { contract: Contract comments: Comment[] parentComment: Comment betsByUserId: Dictionary<[Bet, ...Bet[]]> truncate?: boolean smallAvatar?: boolean }) { const { contract, comments, betsByUserId, truncate, smallAvatar, parentComment, } = props const [showReply, setShowReply] = useState(false) const [replyToUsername, setReplyToUsername] = useState('') const user = useUser() const commentsList = comments.filter( (comment) => 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) => (
))} {showReply && (
)}
) } export function FeedComment(props: { contract: Contract comment: Comment betsBySameUser: Bet[] truncate?: boolean smallAvatar?: boolean onReplyClick?: (comment: Comment) => void }) { const { contract, comment, betsBySameUser, truncate, smallAvatar, onReplyClick, } = props const { text, userUsername, userName, userAvatarUrl, createdTime } = comment let outcome: string | undefined, bought: string | undefined, money: string | undefined const matchedBet = betsBySameUser.find((bet) => bet.id === comment.betId) if (matchedBet) { outcome = 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) } }, [router.asPath]) // Only calculated if they don't have a matching bet const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } = getBettorsPosition( contract, comment.createdTime, matchedBet ? [] : betsBySameUser ) return (

{' '} {!matchedBet && userPosition > 0 && ( <> {'had ' + userPositionMoney + ' '} <> {' of '} noFloorShares ? 'YES' : 'NO'} contract={contract} truncate="short" /> )} <> {bought} {money} {contract.outcomeType !== 'FREE_RESPONSE' && outcome && ( <> {' '} of{' '} )}

{onReplyClick && ( )}
) } export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] comments: 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, comments, answerOutcome, parentComment, replyToUsername, setRef, } = props const user = useUser() const [comment, setComment] = useState('') const [focused, setFocused] = useState(false) const mostRecentCommentableBet = getMostRecentCommentableBet( betsByCurrentUser, comments, user, answerOutcome ) const { id } = mostRecentCommentableBet || { id: undefined } useEffect(() => { if (!replyToUsername || !user || replyToUsername === user.username) return const replacement = `@${replyToUsername} ` setComment(replacement + comment.replace(replacement, '')) }, [user, replyToUsername]) async function submitComment(betId: string | undefined) { if (!user) { return await firebaseLogin() } if (!comment) return // Update state asap to avoid double submission. const commentValue = comment.toString() setComment('') await createComment( contract.id, commentValue, user, betId, answerOutcome, parentComment?.id ) } const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } = getBettorsPosition(contract, Date.now(), betsByCurrentUser) const shouldCollapseAfterClickOutside = false return ( <>
{mostRecentCommentableBet && ( )} {!mostRecentCommentableBet && user && userPosition > 0 && ( <> {'You have ' + userPositionMoney + ' '} <> {' of '} noFloorShares ? 'YES' : 'NO'} contract={contract} truncate="short" /> )}