Comment tips (attempt 2) (#539)

* Add tip arrows UI (visual)

* move tipper into its own component

* simplify score calculation

* Add tip txns

- more specific txn types
- fix transact cloud function to be able to create tip txns
- insert tips into comments via a context

* Refactor tipper to send tip txns

* Stop tipping yourself. Disable anons.

* Style tipper (smaller)

* remove default exports

* capitalize tooltips

* rename stuff

* add exhausting hook dependencies

* replace context with prop threading

* fix eslint unused vars

* fix: thread tips correctly into fr comments
This commit is contained in:
Sinclair Chen 2022-06-17 20:28:16 -07:00 committed by GitHub
parent bb934d8390
commit 833dd37469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 316 additions and 37 deletions

View File

@ -13,9 +13,27 @@ export type Txn = {
amount: number amount: number
token: 'M$' // | 'USD' | MarketOutcome token: 'M$' // | 'USD' | MarketOutcome
category: 'CHARITY' // | 'BET' | 'TIP' category: 'CHARITY' | 'TIP' // | 'BET'
// Any extra data
data?: { [key: string]: any }
// Human-readable description // Human-readable description
description?: string description?: string
} }
export type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK' export type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type DonationTxn = Omit<Txn, 'data'> & {
fromType: 'USER'
toType: 'CHARITY'
category: 'CHARITY'
}
export type TipTxn = Txn & {
fromType: 'USER'
toType: 'USER'
category: 'TIP'
data: {
contractId: string
commentId: string
}
}

View File

@ -11,7 +11,17 @@ export const transact = functions
const userId = context?.auth?.uid const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' } if (!userId) return { status: 'error', message: 'Not authorized' }
const { amount, fromType, fromId, toId, toType, description } = data const {
amount,
fromType,
fromId,
toId,
toType,
category,
token,
data: innerData,
description,
} = data
if (fromType !== 'USER') if (fromType !== 'USER')
return { return {
@ -25,7 +35,7 @@ export const transact = functions
message: 'Must be authenticated with userId equal to specified fromId.', message: 'Must be authenticated with userId equal to specified fromId.',
} }
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) if (isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' } return { status: 'error', message: 'Invalid amount' }
// Run as transaction to prevent race conditions. // Run as transaction to prevent race conditions.
@ -69,9 +79,10 @@ export const transact = functions
toType, toType,
amount, amount,
// TODO: Unhardcode once we have non-donation txns category,
token: 'M$', data: innerData,
category: 'CHARITY', token,
description, description,
}) })

View File

@ -1,11 +1,11 @@
import { Txn } from 'common/txn' import { DonationTxn } from 'common/txn'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { useUserById } from 'web/hooks/use-users' import { useUserById } from 'web/hooks/use-users'
import { UserLink } from '../user-page' import { UserLink } from '../user-page'
import { manaToUSD } from '../../../common/util/format' import { manaToUSD } from '../../../common/util/format'
import { RelativeTimestamp } from '../relative-timestamp' import { RelativeTimestamp } from '../relative-timestamp'
export function Donation(props: { txn: Txn }) { export function Donation(props: { txn: DonationTxn }) {
const { txn } = props const { txn } = props
const user = useUserById(txn.fromId) const user = useUserById(txn.fromId)

View File

@ -13,7 +13,6 @@ import {
NumericResolutionOrExpectation, NumericResolutionOrExpectation,
} from './contract-card' } from './contract-card'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { Comment } from 'common/comment'
import BetRow from '../bet-row' import BetRow from '../bet-row'
import { AnswersGraph } from '../answers/answers-graph' import { AnswersGraph } from '../answers/answers-graph'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
@ -25,7 +24,6 @@ import { NumericGraph } from './numeric-graph'
export const ContractOverview = (props: { export const ContractOverview = (props: {
contract: Contract contract: Contract
bets: Bet[] bets: Bet[]
comments: Comment[]
className?: string className?: string
}) => { }) => {
const { contract, bets, className } = props const { contract, bets, className } = props

View File

@ -7,14 +7,16 @@ import { ContractBetsTable, BetsSummary } from '../bets-list'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
import { Tabs } from '../layout/tabs' import { Tabs } from '../layout/tabs'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
export function ContractTabs(props: { export function ContractTabs(props: {
contract: Contract contract: Contract
user: User | null | undefined user: User | null | undefined
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
tips: CommentTipMap
}) { }) {
const { contract, user, bets, comments } = props const { contract, user, bets, comments, tips } = props
const { outcomeType } = contract const { outcomeType } = contract
const userBets = user && bets.filter((bet) => bet.userId === user.id) const userBets = user && bets.filter((bet) => bet.userId === user.id)
@ -24,6 +26,7 @@ export function ContractTabs(props: {
contract={contract} contract={contract}
bets={bets} bets={bets}
comments={comments} comments={comments}
tips={tips}
user={user} user={user}
mode="bets" mode="bets"
betRowClassName="!mt-0 xl:hidden" betRowClassName="!mt-0 xl:hidden"
@ -36,6 +39,7 @@ export function ContractTabs(props: {
contract={contract} contract={contract}
bets={bets} bets={bets}
comments={comments} comments={comments}
tips={tips}
user={user} user={user}
mode={ mode={
contract.outcomeType === 'FREE_RESPONSE' contract.outcomeType === 'FREE_RESPONSE'
@ -52,6 +56,7 @@ export function ContractTabs(props: {
contract={contract} contract={contract}
bets={bets} bets={bets}
comments={comments} comments={comments}
tips={tips}
user={user} user={user}
mode={'comments'} mode={'comments'}
betRowClassName="!mt-0 xl:hidden" betRowClassName="!mt-0 xl:hidden"

View File

@ -6,6 +6,7 @@ import { getOutcomeProbability } from 'common/calculate'
import { Comment } from 'common/comment' import { Comment } from 'common/comment'
import { Contract, FreeResponseContract } from 'common/contract' import { Contract, FreeResponseContract } from 'common/contract'
import { User } from 'common/user' import { User } from 'common/user'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
export type ActivityItem = export type ActivityItem =
| DescriptionItem | DescriptionItem
@ -50,6 +51,7 @@ export type CommentThreadItem = BaseActivityItem & {
type: 'commentThread' type: 'commentThread'
parentComment: Comment parentComment: Comment
comments: Comment[] comments: Comment[]
tips: CommentTipMap
bets: Bet[] bets: Bet[]
} }
@ -58,6 +60,7 @@ export type AnswerGroupItem = BaseActivityItem & {
user: User | undefined | null user: User | undefined | null
answer: Answer answer: Answer
comments: Comment[] comments: Comment[]
tips: CommentTipMap
bets: Bet[] bets: Bet[]
} }
@ -73,6 +76,7 @@ function getAnswerAndCommentInputGroups(
contract: FreeResponseContract, contract: FreeResponseContract,
bets: Bet[], bets: Bet[],
comments: Comment[], comments: Comment[],
tips: CommentTipMap,
user: User | undefined | null user: User | undefined | null
) { ) {
let outcomes = uniq(bets.map((bet) => bet.outcome)) let outcomes = uniq(bets.map((bet) => bet.outcome))
@ -93,6 +97,7 @@ function getAnswerAndCommentInputGroups(
user, user,
answer, answer,
comments, comments,
tips,
bets, bets,
} }
}) })
@ -103,6 +108,7 @@ function getAnswerAndCommentInputGroups(
function getCommentThreads( function getCommentThreads(
bets: Bet[], bets: Bet[],
comments: Comment[], comments: Comment[],
tips: CommentTipMap,
contract: Contract contract: Contract
) { ) {
const parentComments = comments.filter((comment) => !comment.replyToCommentId) const parentComments = comments.filter((comment) => !comment.replyToCommentId)
@ -114,6 +120,7 @@ function getCommentThreads(
comments: comments, comments: comments,
parentComment: comment, parentComment: comment,
bets: bets, bets: bets,
tips,
})) }))
return items return items
@ -132,6 +139,7 @@ export function getSpecificContractActivityItems(
contract: Contract, contract: Contract,
bets: Bet[], bets: Bet[],
comments: Comment[], comments: Comment[],
tips: CommentTipMap,
user: User | null | undefined, user: User | null | undefined,
options: { options: {
mode: 'comments' | 'bets' | 'free-response-comment-answer-groups' mode: 'comments' | 'bets' | 'free-response-comment-answer-groups'
@ -167,6 +175,7 @@ export function getSpecificContractActivityItems(
...getCommentThreads( ...getCommentThreads(
nonFreeResponseBets, nonFreeResponseBets,
nonFreeResponseComments, nonFreeResponseComments,
tips,
contract contract
) )
) )
@ -190,6 +199,7 @@ export function getSpecificContractActivityItems(
contract as FreeResponseContract, contract as FreeResponseContract,
bets, bets,
comments, comments,
tips,
user user
) )
) )

View File

@ -7,18 +7,20 @@ import { getSpecificContractActivityItems } from './activity-items'
import { FeedItems } from './feed-items' import { FeedItems } from './feed-items'
import { User } from 'common/user' import { User } from 'common/user'
import { useContractWithPreload } from 'web/hooks/use-contract' import { useContractWithPreload } from 'web/hooks/use-contract'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
export function ContractActivity(props: { export function ContractActivity(props: {
contract: Contract contract: Contract
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
tips: CommentTipMap
user: User | null | undefined user: User | null | undefined
mode: 'comments' | 'bets' | 'free-response-comment-answer-groups' mode: 'comments' | 'bets' | 'free-response-comment-answer-groups'
contractPath?: string contractPath?: string
className?: string className?: string
betRowClassName?: string betRowClassName?: string
}) { }) {
const { user, mode, className, betRowClassName } = props const { user, mode, tips, className, betRowClassName } = props
const contract = useContractWithPreload(props.contract) ?? props.contract const contract = useContractWithPreload(props.contract) ?? props.contract
@ -31,6 +33,7 @@ export function ContractActivity(props: {
contract, contract,
bets, bets,
comments, comments,
tips,
user, user,
{ mode } { mode }
) )

View File

@ -24,15 +24,17 @@ import { groupBy } from 'lodash'
import { User } from 'common/user' import { User } from 'common/user'
import { useEvent } from 'web/hooks/use-event' import { useEvent } from 'web/hooks/use-event'
import { getDpmOutcomeProbability } from 'common/calculate-dpm' import { getDpmOutcomeProbability } from 'common/calculate-dpm'
import { CommentTipMap } from 'web/hooks/use-tip-txns'
export function FeedAnswerCommentGroup(props: { export function FeedAnswerCommentGroup(props: {
contract: any contract: any
user: User | undefined | null user: User | undefined | null
answer: Answer answer: Answer
comments: Comment[] comments: Comment[]
tips: CommentTipMap
bets: Bet[] bets: Bet[]
}) { }) {
const { answer, contract, comments, bets, user } = props const { answer, contract, comments, tips, bets, user } = props
const { username, avatarUrl, name, text } = answer const { username, avatarUrl, name, text } = answer
const [replyToUsername, setReplyToUsername] = useState('') const [replyToUsername, setReplyToUsername] = useState('')
@ -214,6 +216,7 @@ export function FeedAnswerCommentGroup(props: {
smallAvatar={true} smallAvatar={true}
truncate={false} truncate={false}
bets={bets} bets={bets}
tips={tips}
scrollAndOpenReplyInput={scrollAndOpenReplyInput} scrollAndOpenReplyInput={scrollAndOpenReplyInput}
treatFirstIndexEqually={true} treatFirstIndexEqually={true}
/> />

View File

@ -25,17 +25,27 @@ import { getProbability } from 'common/calculate'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { PaperAirplaneIcon } from '@heroicons/react/outline' import { PaperAirplaneIcon } from '@heroicons/react/outline'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { Tipper } from '../tipper'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
export function FeedCommentThread(props: { export function FeedCommentThread(props: {
contract: Contract contract: Contract
comments: Comment[] comments: Comment[]
tips: CommentTipMap
parentComment: Comment parentComment: Comment
bets: Bet[] bets: Bet[]
truncate?: boolean truncate?: boolean
smallAvatar?: boolean smallAvatar?: boolean
}) { }) {
const { contract, comments, bets, truncate, smallAvatar, parentComment } = const {
props contract,
comments,
bets,
tips,
truncate,
smallAvatar,
parentComment,
} = props
const [showReply, setShowReply] = useState(false) const [showReply, setShowReply] = useState(false)
const [replyToUsername, setReplyToUsername] = useState('') const [replyToUsername, setReplyToUsername] = useState('')
const betsByUserId = groupBy(bets, (bet) => bet.userId) const betsByUserId = groupBy(bets, (bet) => bet.userId)
@ -64,6 +74,7 @@ export function FeedCommentThread(props: {
contract={contract} contract={contract}
commentsList={commentsList} commentsList={commentsList}
betsByUserId={betsByUserId} betsByUserId={betsByUserId}
tips={tips}
smallAvatar={smallAvatar} smallAvatar={smallAvatar}
truncate={truncate} truncate={truncate}
bets={bets} bets={bets}
@ -97,6 +108,7 @@ export function CommentRepliesList(props: {
contract: Contract contract: Contract
commentsList: Comment[] commentsList: Comment[]
betsByUserId: Dictionary<Bet[]> betsByUserId: Dictionary<Bet[]>
tips: CommentTipMap
scrollAndOpenReplyInput: (comment: Comment) => void scrollAndOpenReplyInput: (comment: Comment) => void
bets: Bet[] bets: Bet[]
treatFirstIndexEqually?: boolean treatFirstIndexEqually?: boolean
@ -107,6 +119,7 @@ export function CommentRepliesList(props: {
contract, contract,
commentsList, commentsList,
betsByUserId, betsByUserId,
tips,
truncate, truncate,
smallAvatar, smallAvatar,
bets, bets,
@ -134,6 +147,7 @@ export function CommentRepliesList(props: {
<FeedComment <FeedComment
contract={contract} contract={contract}
comment={comment} comment={comment}
tips={tips[comment.id]}
betsBySameUser={betsByUserId[comment.userId] ?? []} betsBySameUser={betsByUserId[comment.userId] ?? []}
onReplyClick={scrollAndOpenReplyInput} onReplyClick={scrollAndOpenReplyInput}
probAtCreatedTime={ probAtCreatedTime={
@ -157,6 +171,7 @@ export function CommentRepliesList(props: {
export function FeedComment(props: { export function FeedComment(props: {
contract: Contract contract: Contract
comment: Comment comment: Comment
tips: CommentTips
betsBySameUser: Bet[] betsBySameUser: Bet[]
probAtCreatedTime?: number probAtCreatedTime?: number
truncate?: boolean truncate?: boolean
@ -166,6 +181,7 @@ export function FeedComment(props: {
const { const {
contract, contract,
comment, comment,
tips,
betsBySameUser, betsBySameUser,
probAtCreatedTime, probAtCreatedTime,
truncate, truncate,
@ -257,14 +273,17 @@ export function FeedComment(props: {
moreHref={contractPath(contract)} moreHref={contractPath(contract)}
shouldTruncate={truncate} shouldTruncate={truncate}
/> />
<Row className="mt-2 items-center gap-6 text-xs text-gray-500">
<Tipper comment={comment} tips={tips ?? {}} />
{onReplyClick && ( {onReplyClick && (
<button <button
className={'text-xs font-bold text-gray-500 hover:underline'} className="font-bold hover:underline"
onClick={() => onReplyClick(comment)} onClick={() => onReplyClick(comment)}
> >
Reply Reply
</button> </button>
)} )}
</Row>
</div> </div>
</Row> </Row>
) )

157
web/components/tipper.tsx Normal file
View File

@ -0,0 +1,157 @@
import {
ChevronDoubleRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from '@heroicons/react/solid'
import clsx from 'clsx'
import { Comment } from 'common/comment'
import { User } from 'common/user'
import { formatMoney } from 'common/util/format'
import { debounce, sumBy } from 'lodash'
import { useEffect, useMemo, useRef, useState } from 'react'
import { CommentTips } from 'web/hooks/use-tip-txns'
import { useUser } from 'web/hooks/use-user'
import { transact } from 'web/lib/firebase/fn-call'
import { Row } from './layout/row'
import { Tooltip } from './tooltip'
// xth triangle number * 5 = 5 + 10 + 15 + ... + (x * 5)
const quad = (x: number) => (5 / 2) * x * (x + 1)
// inverse (see https://math.stackexchange.com/questions/2041988/how-to-get-inverse-of-formula-for-sum-of-integers-from-1-to-nsee )
const invQuad = (y: number) => Math.sqrt((2 / 5) * y + 1 / 4) - 1 / 2
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
const { comment, tips } = prop
const me = useUser()
const myId = me?.id ?? ''
const savedTip = tips[myId] as number | undefined
// optimistically increase the tip count, but debounce the update
const [localTip, setLocalTip] = useState(savedTip ?? 0)
const initialized = useRef(false)
useEffect(() => {
if (savedTip && !initialized.current) {
setLocalTip(savedTip)
initialized.current = true
}
}, [savedTip])
const score = useMemo(() => {
const tipVals = Object.values({ ...tips, [myId]: localTip })
return sumBy(tipVals, invQuad)
}, [localTip, tips, myId])
// declare debounced function only on first render
const [saveTip] = useState(() =>
debounce(async (user: User, change: number) => {
if (change === 0) {
return
}
await transact({
amount: change,
fromId: user.id,
fromType: 'USER',
toId: comment.userId,
toType: 'USER',
token: 'M$',
category: 'TIP',
data: {
contractId: comment.contractId,
commentId: comment.id,
},
description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`,
})
}, 1500)
)
// instant save on unrender
useEffect(() => () => void saveTip.flush(), [saveTip])
const changeTip = (tip: number) => {
setLocalTip(tip)
me && saveTip(me, tip - (savedTip ?? 0))
}
return (
<Row className="items-center gap-0.5">
<DownTip
value={localTip}
onChange={changeTip}
disabled={!me || localTip <= 0}
/>
<span className="font-bold">{Math.floor(score)} </span>
<UpTip
value={localTip}
onChange={changeTip}
disabled={!me || me.id === comment.userId}
/>
{localTip === 0 ? (
''
) : (
<span
className={clsx(
'font-semibold',
localTip > 0 ? 'text-primary' : 'text-red-400'
)}
>
({formatMoney(localTip)} tip)
</span>
)}
</Row>
)
}
function DownTip(prop: {
value: number
onChange: (tip: number) => void
disabled?: boolean
}) {
const { onChange, value, disabled } = prop
const marginal = 5 * invQuad(value)
return (
<Tooltip
className="tooltip-bottom"
text={!disabled && `Refund ${formatMoney(marginal)}`}
>
<button
className="flex h-max items-center hover:text-red-600 disabled:text-gray-300"
disabled={disabled}
onClick={() => onChange(value - marginal)}
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
</Tooltip>
)
}
function UpTip(prop: {
value: number
onChange: (tip: number) => void
disabled?: boolean
}) {
const { onChange, value, disabled } = prop
const marginal = 5 * invQuad(value) + 5
return (
<Tooltip
className="tooltip-bottom"
text={!disabled && `Tip ${formatMoney(marginal)}`}
>
<button
className="hover:text-primary flex h-max items-center disabled:text-gray-300"
disabled={disabled}
onClick={() => onChange(value + marginal)}
>
{value >= quad(2) ? (
<ChevronDoubleRightIcon className="text-primary mx-1 h-6 w-6" />
) : value > 0 ? (
<ChevronRightIcon className="text-primary h-6 w-6" />
) : (
<ChevronRightIcon className="h-6 w-6" />
)}
</button>
</Tooltip>
)
}

View File

@ -0,0 +1,16 @@
import clsx from 'clsx'
export function Tooltip(
props: {
text: string | false | undefined | null
} & JSX.IntrinsicElements['div']
) {
const { text, children, className } = props
return text ? (
<div className={clsx(className, 'tooltip z-10')} data-tip={text}>
{children}
</div>
) : (
<>{children}</>
)
}

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Txn } from 'common/txn' import { DonationTxn } from 'common/txn'
import { listenForCharityTxns } from 'web/lib/firebase/txns' import { listenForCharityTxns } from 'web/lib/firebase/txns'
export const useCharityTxns = (charityId: string) => { export const useCharityTxns = (charityId: string) => {
const [txns, setTxns] = useState<Txn[]>([]) const [txns, setTxns] = useState<DonationTxn[]>([])
useEffect(() => { useEffect(() => {
return listenForCharityTxns(charityId, setTxns) return listenForCharityTxns(charityId, setTxns)

23
web/hooks/use-tip-txns.ts Normal file
View File

@ -0,0 +1,23 @@
import { TipTxn } from 'common/txn'
import { groupBy, mapValues, sumBy } from 'lodash'
import { useEffect, useMemo, useState } from 'react'
import { listenForTipTxns } from 'web/lib/firebase/txns'
export type CommentTips = { [userId: string]: number }
export type CommentTipMap = { [commentId: string]: CommentTips }
export function useTipTxns(contractId: string): CommentTipMap {
const [txns, setTxns] = useState<TipTxn[]>([])
useEffect(() => {
return listenForTipTxns(contractId, setTxns)
}, [contractId, setTxns])
return useMemo(() => {
const byComment = groupBy(txns, 'data.commentId')
return mapValues(byComment, (txns) => {
const bySender = groupBy(txns, 'fromId')
return mapValues(bySender, (t) => sumBy(t, 'amount'))
})
}, [txns])
}

View File

@ -1,6 +1,5 @@
import { collection, query, where, orderBy } from 'firebase/firestore' import { DonationTxn, TipTxn } from 'common/txn'
import { Txn } from 'common/txn' import { collection, orderBy, query, where } from 'firebase/firestore'
import { db } from './init' import { db } from './init'
import { getValues, listenForValues } from './utils' import { getValues, listenForValues } from './utils'
@ -16,13 +15,27 @@ const getCharityQuery = (charityId: string) =>
export function listenForCharityTxns( export function listenForCharityTxns(
charityId: string, charityId: string,
setTxns: (txns: Txn[]) => void setTxns: (txns: DonationTxn[]) => void
) { ) {
return listenForValues<Txn>(getCharityQuery(charityId), setTxns) return listenForValues<DonationTxn>(getCharityQuery(charityId), setTxns)
} }
const charitiesQuery = query(txnCollection, where('toType', '==', 'CHARITY')) const charitiesQuery = query(txnCollection, where('toType', '==', 'CHARITY'))
export function getAllCharityTxns() { export function getAllCharityTxns() {
return getValues<Txn>(charitiesQuery) return getValues<DonationTxn>(charitiesQuery)
}
const getTipsQuery = (contractId: string) =>
query(
txnCollection,
where('category', '==', 'TIP'),
where('data.contractId', '==', contractId)
)
export function listenForTipTxns(
contractId: string,
setTxns: (txns: TipTxn[]) => void
) {
return listenForValues<TipTxn>(getTipsQuery(contractId), setTxns)
} }

View File

@ -41,6 +41,7 @@ import ContractEmbedPage from '../embed/[username]/[contractSlug]'
import { useBets } from 'web/hooks/use-bets' import { useBets } from 'web/hooks/use-bets'
import { AlertBox } from 'web/components/alert-box' import { AlertBox } from 'web/components/alert-box'
import { useTracking } from 'web/hooks/use-tracking' import { useTracking } from 'web/hooks/use-tracking'
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
export const getStaticProps = fromPropz(getStaticPropz) export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: { export async function getStaticPropz(props: {
@ -119,6 +120,8 @@ export function ContractPageContent(
// Sort for now to see if bug is fixed. // Sort for now to see if bug is fixed.
comments.sort((c1, c2) => c1.createdTime - c2.createdTime) comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
const tips = useTipTxns(contract.id)
const user = useUser() const user = useUser()
const { width, height } = useWindowSize() const { width, height } = useWindowSize()
@ -192,11 +195,7 @@ export function ContractPageContent(
</button> </button>
)} )}
<ContractOverview <ContractOverview contract={contract} bets={bets} />
contract={contract}
bets={bets}
comments={comments ?? []}
/>
{isNumeric && ( {isNumeric && (
<AlertBox <AlertBox
title="Warning" title="Warning"
@ -224,6 +223,7 @@ export function ContractPageContent(
contract={contract} contract={contract}
bets={bets} bets={bets}
comments={comments} comments={comments}
tips={tips}
/> />
</div> </div>
<Spacer h={12} /> <Spacer h={12} />
@ -234,6 +234,7 @@ export function ContractPageContent(
contract={contract} contract={contract}
user={user} user={user}
bets={bets} bets={bets}
tips={tips}
comments={comments} comments={comments}
/> />
</Col> </Col>
@ -290,8 +291,9 @@ function ContractTopTrades(props: {
contract: Contract contract: Contract
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
tips: CommentTipMap
}) { }) {
const { contract, bets, comments } = props const { contract, bets, comments, tips } = props
const commentsById = keyBy(comments, 'id') const commentsById = keyBy(comments, 'id')
const betsById = keyBy(bets, 'id') const betsById = keyBy(bets, 'id')
@ -328,6 +330,7 @@ function ContractTopTrades(props: {
<FeedComment <FeedComment
contract={contract} contract={contract}
comment={commentsById[topCommentId]} comment={commentsById[topCommentId]}
tips={tips[topCommentId]}
betsBySameUser={[betsById[topCommentId]]} betsBySameUser={[betsById[topCommentId]]}
truncate={false} truncate={false}
smallAvatar={false} smallAvatar={false}