diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index a665a921..f048d8e9 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -25,12 +25,14 @@ import { useAdmin } from 'web/hooks/use-admin' import { needsAdminToResolve } from 'web/pages/[username]/[contractSlug]' import { CHOICE_ANSWER_COLORS } from '../charts/contract/choice' import { useChartAnswers } from '../charts/contract/choice' +import { ChatIcon } from '@heroicons/react/outline' export function AnswersPanel(props: { contract: FreeResponseContract | MultipleChoiceContract + onAnswerCommentClick: (answer: Answer) => void }) { const isAdmin = useAdmin() - const { contract } = props + const { contract, onAnswerCommentClick } = props const { creatorId, resolution, resolutions, totalBets, outcomeType } = contract const [showAllAnswers, setShowAllAnswers] = useState(false) @@ -138,6 +140,7 @@ export function AnswersPanel(props: { answer={item} contract={contract} colorIndex={colorSortedAnswer.indexOf(item.text)} + onAnswerCommentClick={onAnswerCommentClick} /> ))} {hasZeroBetAnswers && !showAllAnswers && ( @@ -183,8 +186,9 @@ function OpenAnswer(props: { contract: FreeResponseContract | MultipleChoiceContract answer: Answer colorIndex: number | undefined + onAnswerCommentClick: (answer: Answer) => void }) { - const { answer, contract, colorIndex } = props + const { answer, contract, colorIndex, onAnswerCommentClick } = props const { username, avatarUrl, text } = answer const prob = getDpmOutcomeProbability(contract.totalShares, answer.id) const probPercent = formatPercent(prob) @@ -240,6 +244,14 @@ function OpenAnswer(props: { BUY )} + { + + } diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index 460fa438..385c7828 100644 --- a/web/components/comment-input.tsx +++ b/web/components/comment-input.tsx @@ -1,12 +1,17 @@ -import { PaperAirplaneIcon } from '@heroicons/react/solid' +import { PaperAirplaneIcon, XCircleIcon } from '@heroicons/react/solid' import { Editor } from '@tiptap/react' import clsx from 'clsx' +import { Answer } from 'common/answer' +import { AnyContractType, Contract } from 'common/contract' import { User } from 'common/user' import { useEffect, useState } from 'react' import { useUser } from 'web/hooks/use-user' import { MAX_COMMENT_LENGTH } from 'web/lib/firebase/comments' +import Curve from 'web/public/custom-components/curve' import { Avatar } from './avatar' import { TextEditor, useTextEditor } from './editor' +import { CommentsAnswer } from './feed/feed-answer-comment-group' +import { ContractCommentInput } from './feed/feed-comments' import { Row } from './layout/row' import { LoadingIndicator } from './loading-indicator' @@ -72,6 +77,40 @@ export function CommentInput(props: { ) } +export function AnswerCommentInput(props: { + contract: Contract + answerResponse: Answer + onCancelAnswerResponse?: () => void +}) { + const { contract, answerResponse, onCancelAnswerResponse } = props + const replyTo = { + id: answerResponse.id, + username: answerResponse.username, + } + + return ( + <> + + +
+ +
+
+ + +
+
+ + ) +} export function CommentInputTextArea(props: { user: User | undefined | null @@ -123,7 +162,7 @@ export function CommentInputTextArea(props: { attrs: { label: replyTo.username, id: replyTo.id }, }) .insertContent(' ') - .focus() + .focus(undefined, { scrollIntoView: false }) .run() } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/web/components/comments/reply-toggle.tsx b/web/components/comments/reply-toggle.tsx new file mode 100644 index 00000000..934efe53 --- /dev/null +++ b/web/components/comments/reply-toggle.tsx @@ -0,0 +1,29 @@ +import clsx from 'clsx' +import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' +import { Row } from '../layout/row' + +export function ReplyToggle(props: { + seeReplies: boolean + numComments: number + onClick: () => void +}) { + const { seeReplies, numComments, onClick } = props + return ( + + ) +} diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 8531697b..b8b57510 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -1,9 +1,8 @@ import { memo, useState } from 'react' -import { getOutcomeProbability } from 'common/calculate' import { Pagination } from 'web/components/pagination' import { FeedBet } from '../feed/feed-bets' import { FeedLiquidity } from '../feed/feed-liquidity' -import { FeedAnswerCommentGroup } from '../feed/feed-answer-comment-group' +import { CommentsAnswer } from '../feed/feed-answer-comment-group' import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments' import { groupBy, sortBy, sum } from 'lodash' import { Bet } from 'common/bet' @@ -25,7 +24,6 @@ import { import { buildArray } from 'common/util/array' import { ContractComment } from 'common/comment' -import { Button } from 'web/components/button' import { MINUTE_MS } from 'common/util/time' import { useUser } from 'web/hooks/use-user' import { Tooltip } from 'web/components/tooltip' @@ -36,14 +34,27 @@ import { usePersistentState, } from 'web/hooks/use-persistent-state' import { safeLocalStorage } from 'web/lib/util/local' +import TriangleDownFillIcon from 'web/lib/icons/triangle-down-fill-icon' +import Curve from 'web/public/custom-components/curve' +import { Answer } from 'common/answer' +import { AnswerCommentInput } from '../comment-input' export function ContractTabs(props: { contract: Contract bets: Bet[] userBets: Bet[] comments: ContractComment[] + answerResponse?: Answer | undefined + onCancelAnswerResponse?: () => void }) { - const { contract, bets, userBets, comments } = props + const { + contract, + bets, + userBets, + comments, + answerResponse, + onCancelAnswerResponse, + } = props const yourTrades = (
@@ -56,7 +67,14 @@ export function ContractTabs(props: { const tabs = buildArray( { title: 'Comments', - content: , + content: ( + + ), }, bets.length > 0 && { title: capitalize(PAST_BETS), @@ -76,8 +94,10 @@ export function ContractTabs(props: { const CommentsTabContent = memo(function CommentsTabContent(props: { contract: Contract comments: ContractComment[] + answerResponse?: Answer + onCancelAnswerResponse?: () => void }) { - const { contract } = props + const { contract, answerResponse, onCancelAnswerResponse } = props const tips = useTipTxns({ contractId: contract.id }) const comments = useComments(contract.id) ?? props.comments const [sort, setSort] = usePersistentState<'Newest' | 'Best'>('Newest', { @@ -95,10 +115,7 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { // replied to answers/comments are NOT newest, otherwise newest first const shouldBeNewestFirst = (c: ContractComment) => - c.replyToCommentId == undefined && - (contract.outcomeType === 'FREE_RESPONSE' - ? c.betId === undefined && c.answerOutcome == undefined - : true) + c.replyToCommentId == undefined // TODO: links to comments are broken because tips load after render and // comments will reorganize themselves if there are tips/bounties awarded @@ -123,73 +140,85 @@ const CommentsTabContent = memo(function CommentsTabContent(props: { const topLevelComments = commentsByParent['_'] ?? [] const sortRow = comments.length > 0 && ( - - - + + +
Sort by:
+ +
) - if (contract.outcomeType === 'FREE_RESPONSE') { - const sortedAnswers = sortBy( - contract.answers, - (a) => -getOutcomeProbability(contract, a.id) - ) - const commentsByOutcome = groupBy( - sortedComments, - (c) => c.answerOutcome ?? c.betOutcome ?? '_' - ) - const generalTopLevelComments = topLevelComments.filter( - (c) => c.answerOutcome === undefined && c.betId === undefined - ) - return ( <> + {sortRow} - {sortedAnswers.map((answer) => ( -
-
- ))} - -
General Comments
-
- - {sortRow} - - {generalTopLevelComments.map((comment) => ( - - ))} - + {answerResponse && ( + + )} + {topLevelComments.map((parent) => { + if (parent.answerOutcome === undefined) { + return ( + c.createdTime + )} + tips={tips} + /> + ) + } + const answer = contract.answers.find( + (answer) => answer.id === parent.answerOutcome + ) + if (answer === undefined) { + console.error('Could not find answer that matches ID') + return <> + } + return ( + <> + + + + +
+ +
+
+ c.createdTime + )} + tips={tips} + /> +
+
+ + ) + })} ) } else { diff --git a/web/components/contract/tip-button.tsx b/web/components/contract/tip-button.tsx index b974938f..df245c68 100644 --- a/web/components/contract/tip-button.tsx +++ b/web/components/contract/tip-button.tsx @@ -44,7 +44,7 @@ export function TipButton(props: { 0 ? 'mr-2' : '', userTipped ? 'fill-teal-500 text-teal-500' : '' )} diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index d4401b8c..6b6b911a 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -33,7 +33,7 @@ export function CopyLinkDateTimeComponent(props: { {fromNow(createdTime)} diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index 11bc6139..e1b470a7 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -1,46 +1,21 @@ import { Answer } from 'common/answer' -import { FreeResponseContract } from 'common/contract' -import { ContractComment } from 'common/comment' -import React, { useEffect, useRef, useState } from 'react' -import { sum } from 'lodash' +import { Contract } from 'common/contract' +import React, { useEffect, useRef } from 'react' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Avatar } from 'web/components/avatar' -import { Linkify } from 'web/components/linkify' -import clsx from 'clsx' -import { - ContractCommentInput, - FeedComment, - ReplyTo, -} from 'web/components/feed/feed-comments' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' import { useRouter } from 'next/router' -import { useUser } from 'web/hooks/use-user' -import { useEvent } from 'web/hooks/use-event' -import { CommentTipMap } from 'web/hooks/use-tip-txns' import { UserLink } from 'web/components/user-link' -export function FeedAnswerCommentGroup(props: { - contract: FreeResponseContract - answer: Answer - answerComments: ContractComment[] - tips: CommentTipMap -}) { - const { answer, contract, answerComments, tips } = props +export function CommentsAnswer(props: { answer: Answer; contract: Contract }) { + const { answer, contract } = props const { username, avatarUrl, name, text } = answer - - const [replyTo, setReplyTo] = useState() - const user = useUser() - const router = useRouter() const answerElementId = `answer-${answer.id}` + const router = useRouter() const highlighted = router.asPath.endsWith(`#${answerElementId}`) const answerRef = useRef(null) - const onSubmitComment = useEvent(() => setReplyTo(undefined)) - const onReplyClick = useEvent((comment: ContractComment) => { - setReplyTo({ id: comment.id, username: comment.userUsername }) - }) - useEffect(() => { if (highlighted && answerRef.current != null) { answerRef.current.scrollIntoView(true) @@ -48,83 +23,20 @@ export function FeedAnswerCommentGroup(props: { }, [highlighted]) return ( - - - - - -
- answered - -
- - - - - -
- -
- -
- -
- -
- - {answerComments.map((comment) => ( - - ))} - - {replyTo && ( -
-