Single threaded comments (#175)

* Remove unused hideOutcome in comments

* Remove unused hideOutcome in comments

* Add replyToComment fields to Comment

* Add 1 threaded replies to comments & answers

* Allow smooth scrolling within pages via #

* remove yarn-error log

* correct spelling

* Remove smooth-scroll-to-hashtag component

* Cleanup & show user position/bets in replies
This commit is contained in:
Boa 2022-05-11 15:11:46 -06:00 committed by GitHub
parent aa433e309c
commit 02ed9bf7e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 380 additions and 190 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.idea/ .idea/
.vercel .vercel
node_modules node_modules
yarn-error.log

View File

@ -5,6 +5,7 @@ export type Comment = {
contractId: string contractId: string
betId?: string betId?: string
answerOutcome?: string answerOutcome?: string
replyToCommentId?: string
userId: string userId: string
text: string text: string

View File

@ -1,4 +1,4 @@
import _ from 'lodash' import _, { Dictionary } from 'lodash'
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
@ -18,6 +18,7 @@ export type ActivityItem =
| CloseItem | CloseItem
| ResolveItem | ResolveItem
| CommentInputItem | CommentInputItem
| CommentThreadItem
type BaseActivityItem = { type BaseActivityItem = {
id: string id: string
@ -53,9 +54,15 @@ export type CommentItem = BaseActivityItem & {
type: 'comment' type: 'comment'
comment: Comment comment: Comment
betsBySameUser: Bet[] betsBySameUser: Bet[]
hideOutcome: boolean truncate?: boolean
truncate: boolean smallAvatar?: boolean
smallAvatar: boolean }
export type CommentThreadItem = BaseActivityItem & {
type: 'commentThread'
parentComment: Comment
comments: Comment[]
betsByUserId: Dictionary<[Bet, ...Bet[]]>
} }
export type BetGroupItem = BaseActivityItem & { export type BetGroupItem = BaseActivityItem & {
@ -68,6 +75,8 @@ export type AnswerGroupItem = BaseActivityItem & {
type: 'answergroup' | 'answer' type: 'answergroup' | 'answer'
answer: Answer answer: Answer
items: ActivityItem[] items: ActivityItem[]
betsByCurrentUser?: Bet[]
comments?: Comment[]
} }
export type CloseItem = BaseActivityItem & { export type CloseItem = BaseActivityItem & {
@ -131,7 +140,6 @@ function groupBets(
comment, comment,
betsBySameUser: [bet], betsBySameUser: [bet],
contract, contract,
hideOutcome,
truncate: abbreviated, truncate: abbreviated,
smallAvatar, smallAvatar,
} }
@ -273,41 +281,23 @@ function getAnswerAndCommentInputGroups(
getOutcomeProbability(contract, 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 const answerGroups = outcomes
.map((outcome) => { .map((outcome) => {
const answer = contract.answers?.find( const answer = contract.answers?.find(
(answer) => answer.id === outcome (answer) => answer.id === outcome
) as Answer ) as Answer
const items = collateCommentsSectionForOutcome(outcome) const answerBets = bets.filter((bet) => bet.outcome === outcome)
const answerComments = comments.filter(
(comment) =>
comment.answerOutcome === outcome ||
answerBets.some((bet) => bet.id === comment.betId)
)
const items = getCommentThreads(
answerBets,
answerComments,
contract
).reverse()
return { return {
id: outcome, id: outcome,
@ -316,6 +306,8 @@ function getAnswerAndCommentInputGroups(
answer, answer,
items, items,
user, user,
betsByCurrentUser: answerBets.filter((bet) => bet.userId === user?.id),
comments: answerComments,
} }
}) })
.filter((group) => group.answer) as ActivityItem[] .filter((group) => group.answer) as ActivityItem[]
@ -344,7 +336,6 @@ function groupBetsAndComments(
comment, comment,
betsBySameUser: [], betsBySameUser: [],
truncate: abbreviated, truncate: abbreviated,
hideOutcome: true,
smallAvatar, smallAvatar,
})) }))
@ -370,22 +361,21 @@ function groupBetsAndComments(
return abbrItems return abbrItems
} }
function getCommentsWithPositions( function getCommentThreads(
bets: Bet[], bets: Bet[],
comments: Comment[], comments: Comment[],
contract: Contract contract: Contract
) { ) {
const betsByUserId = _.groupBy(bets, (bet) => bet.userId) const betsByUserId = _.groupBy(bets, (bet) => bet.userId)
const parentComments = comments.filter((comment) => !comment.replyToCommentId)
const items = comments.map((comment) => ({ const items = parentComments.map((comment) => ({
type: 'comment' as const, type: 'commentThread' as const,
id: comment.id, id: comment.id,
contract: contract, contract: contract,
comment, comments: comments,
betsBySameUser: bets.length === 0 ? [] : betsByUserId[comment.userId] ?? [], parentComment: comment,
truncate: false, betsByUserId: betsByUserId,
hideOutcome: false,
smallAvatar: false,
})) }))
return items return items
@ -566,7 +556,7 @@ export function getSpecificContractActivityItems(
const nonFreeResponseBets = const nonFreeResponseBets =
contract.outcomeType === 'FREE_RESPONSE' ? [] : bets contract.outcomeType === 'FREE_RESPONSE' ? [] : bets
items.push( items.push(
...getCommentsWithPositions( ...getCommentThreads(
nonFreeResponseBets, nonFreeResponseBets,
nonFreeResponseComments, nonFreeResponseComments,
contract contract

View File

@ -1,6 +1,7 @@
// From https://tailwindui.com/components/application-ui/lists/feeds // From https://tailwindui.com/components/application-ui/lists/feeds
import React, { Fragment, useRef, useState } from 'react' import React, { Fragment, useEffect, useRef, useState } from 'react'
import * as _ from 'lodash' import * as _ from 'lodash'
import { Dictionary } from 'lodash'
import { import {
BanIcon, BanIcon,
CheckIcon, CheckIcon,
@ -15,8 +16,8 @@ import Textarea from 'react-expanding-textarea'
import { OutcomeLabel } from '../outcome-label' import { OutcomeLabel } from '../outcome-label'
import { import {
contractMetrics,
Contract, Contract,
contractMetrics,
contractPath, contractPath,
tradingAllowed, tradingAllowed,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
@ -30,15 +31,13 @@ import { BinaryResolutionOrChance } from '../contract/contract-card'
import { SiteLink } from '../site-link' import { SiteLink } from '../site-link'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { UserLink } from '../user-page' import { UserLink } from '../user-page'
import { DateTimeTooltip } from '../datetime-tooltip'
import { Bet } from 'web/lib/firebase/bets' import { Bet } from 'web/lib/firebase/bets'
import { JoinSpans } from '../join-spans' import { JoinSpans } from '../join-spans'
import { fromNow } from 'web/lib/util/time'
import BetRow from '../bet-row' import BetRow from '../bet-row'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
import { ActivityItem, GENERAL_COMMENTS_OUTCOME_ID } from './activity-items' import { ActivityItem, GENERAL_COMMENTS_OUTCOME_ID } from './activity-items'
import { Binary, CPMM, DPM, FreeResponse, FullContract } from 'common/contract' import { Binary, CPMM, FreeResponse, FullContract } from 'common/contract'
import { BuyButton } from '../yes-no-selector' import { BuyButton } from '../yes-no-selector'
import { getDpmOutcomeProbability } from 'common/calculate-dpm' import { getDpmOutcomeProbability } from 'common/calculate-dpm'
import { AnswerBetPanel } from '../answers/answer-bet-panel' import { AnswerBetPanel } from '../answers/answer-bet-panel'
@ -51,6 +50,7 @@ import { DAY_MS } from 'common/util/time'
import NewContractBadge from '../new-contract-badge' import NewContractBadge from '../new-contract-badge'
import { RelativeTimestamp } from '../relative-timestamp' import { RelativeTimestamp } from '../relative-timestamp'
import { calculateCpmmSale } from 'common/calculate-cpmm' import { calculateCpmmSale } from 'common/calculate-cpmm'
import { useWindowSize } from 'web/hooks/use-window-size'
export function FeedItems(props: { export function FeedItems(props: {
contract: Contract contract: Contract
@ -118,24 +118,97 @@ function FeedItem(props: { item: ActivityItem }) {
return <FeedResolve {...item} /> return <FeedResolve {...item} />
case 'commentInput': case 'commentInput':
return <CommentInput {...item} /> return <CommentInput {...item} />
case 'commentThread':
return <FeedCommentThread {...item} />
} }
} }
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<HTMLTextAreaElement | null>(null)
function scrollAndOpenReplyInput(comment: Comment) {
setReplyToUsername(comment.userUsername)
setShowReply(true)
inputRef?.focus()
}
useEffect(() => {
if (showReply && inputRef) inputRef.focus()
}, [inputRef, showReply])
return (
<div className={'w-full flex-col flex-col pr-6'}>
{commentsList.map((comment, commentIdx) => (
<div
key={comment.id}
id={comment.id}
className={clsx(
'flex space-x-3',
commentIdx === 0 ? '' : 'mt-4 ml-8'
)}
>
<FeedComment
contract={contract}
comment={comment}
betsBySameUser={betsByUserId[comment.userId] ?? []}
onReplyClick={scrollAndOpenReplyInput}
smallAvatar={smallAvatar}
truncate={truncate}
/>
</div>
))}
{showReply && (
<div className={' ml-8 w-full pt-6'}>
<CommentInput
contract={contract}
// Should we allow replies to contain recent bet info?
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
comments={comments}
parentComment={parentComment}
replyToUsername={replyToUsername}
answerOutcome={comments[0].answerOutcome}
setRef={setInputRef}
/>
</div>
)}
</div>
)
}
export function FeedComment(props: { export function FeedComment(props: {
contract: Contract contract: Contract
comment: Comment comment: Comment
betsBySameUser: Bet[] betsBySameUser: Bet[]
hideOutcome: boolean truncate?: boolean
truncate: boolean smallAvatar?: boolean
smallAvatar: boolean onReplyClick?: (comment: Comment) => void
}) { }) {
const { const {
contract, contract,
comment, comment,
betsBySameUser, betsBySameUser,
hideOutcome,
truncate, truncate,
smallAvatar, smallAvatar,
onReplyClick,
} = props } = props
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
let outcome: string | undefined, let outcome: string | undefined,
@ -187,7 +260,7 @@ export function FeedComment(props: {
)} )}
<> <>
{bought} {money} {bought} {money}
{outcome && !hideOutcome && ( {contract.outcomeType !== 'FREE_RESPONSE' && outcome && (
<> <>
{' '} {' '}
of{' '} of{' '}
@ -206,6 +279,16 @@ export function FeedComment(props: {
moreHref={contractPath(contract)} moreHref={contractPath(contract)}
shouldTruncate={truncate} shouldTruncate={truncate}
/> />
{onReplyClick && (
<button
className={
'btn btn-ghost btn-xs text-transform: text-xs capitalize text-gray-500'
}
onClick={() => onReplyClick(comment)}
>
Reply
</button>
)}
</div> </div>
</> </>
) )
@ -215,133 +298,163 @@ export function CommentInput(props: {
contract: Contract contract: Contract
betsByCurrentUser: Bet[] betsByCurrentUser: Bet[]
comments: Comment[] comments: Comment[]
// Only for free response comment inputs // Tie a comment to an free response answer outcome
answerOutcome?: string answerOutcome?: string
// Tie a comment to another comment
parentComment?: Comment
replyToUsername?: string
setRef?: (ref: any) => void
}) { }) {
const { contract, betsByCurrentUser, comments, answerOutcome } = props const {
contract,
betsByCurrentUser,
comments,
answerOutcome,
parentComment,
replyToUsername,
setRef,
} = props
const user = useUser() const user = useUser()
const [comment, setComment] = useState('') const [comment, setComment] = useState('')
const [focused, setFocused] = useState(false) const [focused, setFocused] = useState(false)
const { width } = useWindowSize()
// Should this be oldest bet or most recent bet? const mostRecentCommentableBet = getMostRecentCommentableBet(
const mostRecentCommentableBet = betsByCurrentUser betsByCurrentUser,
.filter((bet) => { comments,
if ( user,
canCommentOnBet(bet, user) && answerOutcome
// 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 } 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) { async function submitComment(betId: string | undefined) {
if (!user) { if (!user) {
return await firebaseLogin() return await firebaseLogin()
} }
if (!comment) return if (!comment) return
await createComment(contract.id, comment, user, betId, answerOutcome)
// Update state asap to avoid double submission.
const commentValue = comment.toString()
setComment('') setComment('')
await createComment(
contract.id,
commentValue,
user,
betId,
answerOutcome,
parentComment?.id
)
} }
const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } = const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } =
getBettorsPosition(contract, Date.now(), betsByCurrentUser) getBettorsPosition(contract, Date.now(), betsByCurrentUser)
const shouldCollapseAfterClickOutside = false
function isMobile() {
return width ? width < 768 : false
}
return ( return (
<> <>
<Row className={'flex w-full gap-2'}> <Row className={'mb-2 flex w-full gap-2'}>
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} /> <div className={'mt-1'}>
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
</div>
<div className={'min-w-0 flex-1'}> <div className={'min-w-0 flex-1'}>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
{mostRecentCommentableBet && ( <div className={'mb-1'}>
<BetStatusText {mostRecentCommentableBet && (
contract={contract} <BetStatusText
bet={mostRecentCommentableBet} contract={contract}
isSelf={true} bet={mostRecentCommentableBet}
/> isSelf={true}
)}
{!mostRecentCommentableBet && user && userPosition > 0 && (
<>
{'You have ' + userPositionMoney + ' '}
<>
{' of '}
<OutcomeLabel
outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
contract={contract}
truncate="short"
/>
</>
</>
)}
{(answerOutcome === undefined || focused) && (
<div className="mt-2">
<Textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
className="textarea textarea-bordered w-full resize-none"
placeholder="Add a comment..."
autoFocus={focused}
rows={answerOutcome == undefined || focused ? 3 : 1}
onFocus={() => setFocused(true)}
onBlur={() => !comment && setFocused(false)}
maxLength={MAX_COMMENT_LENGTH}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
submitComment(id)
}
}}
/> />
</div> )}
)} {!mostRecentCommentableBet && user && userPosition > 0 && (
</div> <>
{!user && ( {'You have ' + userPositionMoney + ' '}
<button <>
className={ {' of '}
'btn btn-outline btn-sm text-transform: mt-1 capitalize' <OutcomeLabel
} outcome={yesFloorShares > noFloorShares ? 'YES' : 'NO'}
onClick={() => submitComment(id)} contract={contract}
> truncate="short"
Sign in to Comment />
</button> </>
)} </>
{user && answerOutcome === undefined && ( )}
<button </div>
className={
'btn btn-outline btn-sm text-transform: mt-1 capitalize' <Row className="gap-1.5">
} <Textarea
onClick={() => submitComment(id)} ref={(ref) => setRef?.(ref)}
> value={comment}
Comment onChange={(e) => setComment(e.target.value)}
</button> className="textarea textarea-bordered w-full resize-none"
)} placeholder={
{user && answerOutcome !== undefined && ( parentComment || answerOutcome
<button ? 'Write a reply... '
className={ : 'Write a comment...'
focused
? 'btn btn-outline btn-sm text-transform: mt-1 capitalize'
: 'btn btn-ghost btn-sm text-transform: mt-1 capitalize'
}
onClick={() => {
if (!focused) setFocused(true)
else {
submitComment(id)
setFocused(false)
} }
}} autoFocus={focused}
> rows={focused ? 3 : 1}
{!focused ? 'Add Comment' : 'Comment'} onFocus={() => setFocused(true)}
</button> onBlur={() =>
)} shouldCollapseAfterClickOutside && setFocused(false)
}
maxLength={MAX_COMMENT_LENGTH}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !isMobile()) {
e.preventDefault()
submitComment(id)
}
}}
/>
<div
className={clsx(
'flex justify-center',
focused ? 'items-end' : 'items-center'
)}
>
{!user && (
<button
className={
'btn btn-outline btn-sm text-transform: capitalize'
}
onClick={() => submitComment(id)}
>
Sign in to Comment
</button>
)}
{user && (
<button
className={clsx(
'btn text-transform: block capitalize md:hidden lg:hidden',
focused && comment
? 'btn-outline btn-sm '
: 'btn-ghost btn-sm text-gray-500'
)}
onClick={() => {
if (!focused) return
else {
submitComment(id)
setFocused(false)
}
}}
>
{parentComment || answerOutcome ? 'Reply' : 'Comment'}
</button>
)}
</div>
</Row>
</div>
</div> </div>
</Row> </Row>
</> </>
@ -435,7 +548,6 @@ export function FeedBet(props: {
bet={bet} bet={bet}
contract={contract} contract={contract}
isSelf={isSelf} isSelf={isSelf}
hideOutcome={hideOutcome}
bettor={bettor} bettor={bettor}
/> />
</div> </div>
@ -448,10 +560,9 @@ function BetStatusText(props: {
contract: Contract contract: Contract
bet: Bet bet: Bet
isSelf: boolean isSelf: boolean
hideOutcome?: boolean
bettor?: User bettor?: User
}) { }) {
const { bet, contract, hideOutcome, bettor, isSelf } = props const { bet, contract, bettor, isSelf } = props
const { amount, outcome, createdTime } = bet const { amount, outcome, createdTime } = bet
const bought = amount >= 0 ? 'bought' : 'sold' const bought = amount >= 0 ? 'bought' : 'sold'
@ -461,7 +572,7 @@ function BetStatusText(props: {
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
<span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span> {bought}{' '} <span>{isSelf ? 'You' : bettor ? bettor.name : 'A trader'}</span> {bought}{' '}
{money} {money}
{!hideOutcome && ( {contract.outcomeType !== 'FREE_RESPONSE' && (
<> <>
{' '} {' '}
of{' '} of{' '}
@ -581,6 +692,32 @@ export function FeedQuestion(props: {
) )
} }
function getMostRecentCommentableBet(
betsByCurrentUser: Bet[],
comments: Comment[],
user?: User | null,
answerOutcome?: string
) {
return betsByCurrentUser
.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()
}
function canCommentOnBet(bet: Bet, user?: User | null) { function canCommentOnBet(bet: Bet, user?: User | null) {
const { userId, createdTime, isRedemption } = bet const { userId, createdTime, isRedemption } = bet
const isSelf = user?.id === userId const isSelf = user?.id === userId
@ -768,13 +905,35 @@ function FeedAnswerGroup(props: {
answer: Answer answer: Answer
items: ActivityItem[] items: ActivityItem[]
type: string type: string
betsByCurrentUser?: Bet[]
comments?: Comment[]
}) { }) {
const { answer, items, contract, type } = props const { answer, items, contract, type, betsByCurrentUser, comments } = props
const { username, avatarUrl, name, text } = answer const { username, avatarUrl, name, text } = answer
const user = useUser()
const mostRecentCommentableBet = getMostRecentCommentableBet(
betsByCurrentUser ?? [],
comments ?? [],
user,
answer.number + ''
)
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id) const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
const probPercent = formatPercent(prob) const probPercent = formatPercent(prob)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [showReply, setShowReply] = useState(false)
const isFreeResponseContractPage = type === 'answergroup' && comments
if (mostRecentCommentableBet && !showReply) setShowReplyAndFocus(true)
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
// If they've already opened the input box, focus it once again
function setShowReplyAndFocus(show: boolean) {
setShowReply(show)
inputRef?.focus()
}
useEffect(() => {
if (showReply && inputRef) inputRef.focus()
}, [inputRef, showReply])
return ( return (
<Col <Col
@ -798,7 +957,7 @@ function FeedAnswerGroup(props: {
<div className="px-1"> <div className="px-1">
<Avatar username={username} avatarUrl={avatarUrl} /> <Avatar username={username} avatarUrl={avatarUrl} />
</div> </div>
<Col className="min-w-0 flex-1 gap-2"> <Col className="min-w-0 flex-1 lg:gap-1">
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
<UserLink username={username} name={name} /> answered <UserLink username={username} name={name} /> answered
</div> </div>
@ -808,27 +967,69 @@ function FeedAnswerGroup(props: {
<Linkify text={text} /> <Linkify text={text} />
</span> </span>
<Row className="align-items justify-end gap-4"> <Row className="items-center justify-center gap-4">
<span {isFreeResponseContractPage && (
className={clsx( <div className={'sm:hidden'}>
'text-2xl', <button
tradingAllowed(contract) ? 'text-green-500' : 'text-gray-500' className={
)} 'btn btn-ghost btn-xs text-transform: text-xs capitalize text-gray-500'
> }
{probPercent} onClick={() => setShowReplyAndFocus(true)}
</span> >
<BuyButton Reply
className={clsx( </button>
'btn-sm flex-initial !px-6 sm:flex', </div>
tradingAllowed(contract) ? '' : '!hidden' )}
)}
onClick={() => setOpen(true)} <div className={'align-items flex w-full justify-end gap-4 '}>
/> <span
className={clsx(
'text-2xl',
tradingAllowed(contract)
? 'text-green-500'
: 'text-gray-500'
)}
>
{probPercent}
</span>
<BuyButton
className={clsx(
'btn-sm flex-initial !px-6 sm:flex',
tradingAllowed(contract) ? '' : '!hidden'
)}
onClick={() => setOpen(true)}
/>
</div>
</Row> </Row>
</Col> </Col>
{isFreeResponseContractPage && (
<div className={'justify-initial hidden sm:block'}>
<button
className={
'btn btn-ghost btn-xs text-transform: text-xs capitalize text-gray-500'
}
onClick={() => setShowReplyAndFocus(true)}
>
Reply
</button>
</div>
)}
</Col> </Col>
</Row> </Row>
{showReply && (
<div className={'ml-8'}>
<CommentInput
contract={contract}
betsByCurrentUser={betsByCurrentUser ?? []}
comments={comments ?? []}
answerOutcome={answer.number + ''}
replyToUsername={answer.username}
setRef={setInputRef}
/>
</div>
)}
{items.map((item, index) => ( {items.map((item, index) => (
<div <div
key={item.id} key={item.id}

View File

@ -13,6 +13,7 @@ import { getValues, listenForValues } from './utils'
import { db } from './init' import { db } from './init'
import { User } from 'common/user' import { User } from 'common/user'
import { Comment } from 'common/comment' import { Comment } from 'common/comment'
import { removeUndefinedProps } from 'common/util/object'
export type { Comment } export type { Comment }
@ -23,12 +24,13 @@ export async function createComment(
text: string, text: string,
commenter: User, commenter: User,
betId?: string, betId?: string,
answerOutcome?: string answerOutcome?: string,
replyToCommentId?: string
) { ) {
const ref = betId const ref = betId
? doc(getCommentsCollection(contractId), betId) ? doc(getCommentsCollection(contractId), betId)
: doc(getCommentsCollection(contractId)) : doc(getCommentsCollection(contractId))
const comment: Comment = { const comment: Comment = removeUndefinedProps({
id: ref.id, id: ref.id,
contractId, contractId,
userId: commenter.id, userId: commenter.id,
@ -37,13 +39,10 @@ export async function createComment(
userName: commenter.name, userName: commenter.name,
userUsername: commenter.username, userUsername: commenter.username,
userAvatarUrl: commenter.avatarUrl, userAvatarUrl: commenter.avatarUrl,
} betId: betId,
if (betId) { answerOutcome: answerOutcome,
comment.betId = betId replyToCommentId: replyToCommentId,
} })
if (answerOutcome) {
comment.answerOutcome = answerOutcome
}
return await setDoc(ref, comment) return await setDoc(ref, comment)
} }

View File

@ -290,7 +290,6 @@ function ContractTopTrades(props: {
contract={contract} contract={contract}
comment={commentsById[topCommentId]} comment={commentsById[topCommentId]}
betsBySameUser={[betsById[topCommentId]]} betsBySameUser={[betsById[topCommentId]]}
hideOutcome={false}
truncate={false} truncate={false}
smallAvatar={false} smallAvatar={false}
/> />

View File

@ -76,7 +76,6 @@ function MyApp({ Component, pageProps }: AppProps) {
content="width=device-width, initial-scale=1, maximum-scale=1" content="width=device-width, initial-scale=1, maximum-scale=1"
/> />
</Head> </Head>
<Component {...pageProps} /> <Component {...pageProps} />
</> </>
) )