diff --git a/docs/docs/faq.md b/docs/docs/faq.md
index 01c4dc36..5c369e39 100644
--- a/docs/docs/faq.md
+++ b/docs/docs/faq.md
@@ -4,11 +4,7 @@
### Do I have to pay real money in order to participate?
-Nope! Each account starts with a free M$1000. If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
-
-### What is the name for the currency Manifold uses, represented by M$?
-
-Manifold Dollars, or mana for short.
+Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
### Can M$ be sold for real money?
diff --git a/web/components/add-funds-button.tsx b/web/components/add-funds-button.tsx
index 90b24b2c..b610bfee 100644
--- a/web/components/add-funds-button.tsx
+++ b/web/components/add-funds-button.tsx
@@ -30,10 +30,10 @@ export function AddFundsButton(props: { className?: string }) {
-
Get Manifold Dollars
+
Get Mana
- Use Manifold Dollars to trade in your favorite markets. (Not
+ Buy mana (M$) to trade in your favorite markets. (Not
redeemable for cash.)
diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx
index 41b7f0f9..51cf5799 100644
--- a/web/components/answers/answers-panel.tsx
+++ b/web/components/answers/answers-panel.tsx
@@ -1,4 +1,4 @@
-import { sortBy, partition, sum, uniq } from 'lodash'
+import { sortBy, partition, sum } from 'lodash'
import { useEffect, useState } from 'react'
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
@@ -11,7 +11,6 @@ import { AnswerItem } from './answer-item'
import { CreateAnswerPanel } from './create-answer-panel'
import { AnswerResolvePanel } from './answer-resolve-panel'
import { Spacer } from '../layout/spacer'
-import { User } from 'common/user'
import { getOutcomeProbability } from 'common/calculate'
import { Answer } from 'common/answer'
import clsx from 'clsx'
@@ -39,22 +38,14 @@ export function AnswersPanel(props: {
const answers = (useAnswers(contract.id) ?? contract.answers).filter(
(a) => a.number != 0 || contract.outcomeType === 'MULTIPLE_CHOICE'
)
- const hasZeroBetAnswers = answers.some((answer) => totalBets[answer.id] < 1)
-
- const [winningAnswers, losingAnswers] = partition(
- answers.filter((a) => (showAllAnswers ? true : totalBets[a.id] > 0)),
- (answer) =>
- answer.id === resolution || (resolutions && resolutions[answer.id])
+ const [winningAnswers, notWinningAnswers] = partition(
+ answers,
+ (a) => a.id === resolution || (resolutions && resolutions[a.id])
+ )
+ const [visibleAnswers, invisibleAnswers] = partition(
+ sortBy(notWinningAnswers, (a) => -getOutcomeProbability(contract, a.id)),
+ (a) => showAllAnswers || totalBets[a.id] > 0
)
- const sortedAnswers = [
- ...sortBy(winningAnswers, (answer) =>
- resolutions ? -1 * resolutions[answer.id] : 0
- ),
- ...sortBy(
- resolution ? [] : losingAnswers,
- (answer) => -1 * getDpmOutcomeProbability(contract.totalShares, answer.id)
- ),
- ]
const user = useUser()
@@ -67,12 +58,6 @@ export function AnswersPanel(props: {
const chosenTotal = sum(Object.values(chosenAnswers))
- const answerItems = getAnswerItems(
- contract,
- losingAnswers.length > 0 ? losingAnswers : sortedAnswers,
- user
- )
-
const onChoose = (answerId: string, prob: number) => {
if (resolveOption === 'CHOOSE') {
setChosenAnswers({ [answerId]: prob })
@@ -109,13 +94,13 @@ export function AnswersPanel(props: {
return (
{(resolveOption || resolution) &&
- sortedAnswers.map((answer) => (
+ sortBy(winningAnswers, (a) => -(resolutions?.[a.id] ?? 0)).map((a) => (
-
- {answerItems.map((item) => (
-
- ))}
-
- {hasZeroBetAnswers && !showAllAnswers && (
- setShowAllAnswers(true)}
- size={'md'}
- >
- Show More
-
- )}
-
-
-
+
+ {visibleAnswers.map((a) => (
+
+ ))}
+ {invisibleAnswers.length > 0 && !showAllAnswers && (
+
setShowAllAnswers(true)}
+ size="md"
+ >
+ Show More
+
+ )}
+
)}
- {answers.length <= 1 && (
+ {answers.length === 0 && (
No answers yet...
)}
@@ -175,35 +158,9 @@ export function AnswersPanel(props: {
)
}
-function getAnswerItems(
- contract: FreeResponseContract | MultipleChoiceContract,
- answers: Answer[],
- user: User | undefined | null
-) {
- let outcomes = uniq(answers.map((answer) => answer.number.toString()))
- outcomes = sortBy(outcomes, (outcome) =>
- getOutcomeProbability(contract, outcome)
- ).reverse()
-
- return outcomes
- .map((outcome) => {
- const answer = answers.find((answer) => answer.id === outcome) as Answer
- //unnecessary
- return {
- id: outcome,
- type: 'answer' as const,
- contract,
- answer,
- user,
- }
- })
- .filter((group) => group.answer)
-}
-
function OpenAnswer(props: {
contract: FreeResponseContract | MultipleChoiceContract
answer: Answer
- type: string
}) {
const { answer, contract } = props
const { username, avatarUrl, name, text } = answer
@@ -212,7 +169,7 @@ function OpenAnswer(props: {
const [open, setOpen] = useState(false)
return (
-
+
-
+
answered
-
-
-
-
-
-
-
- {probPercent}
-
- setOpen(true)}
- />
-
+
+
+
+ {probPercent}
+
+ setOpen(true)}
+ />
diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx
index d7c7b717..19ced0b2 100644
--- a/web/components/auth-context.tsx
+++ b/web/components/auth-context.tsx
@@ -17,7 +17,7 @@ import { setCookie } from 'web/lib/util/cookie'
// Either we haven't looked up the logged in user yet (undefined), or we know
// the user is not logged in (null), or we know the user is logged in.
-type AuthUser = undefined | null | UserAndPrivateUser
+export type AuthUser = undefined | null | UserAndPrivateUser
const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10
const CACHED_USER_KEY = 'CACHED_USER_KEY_V2'
diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx
index 44c37128..abb67d46 100644
--- a/web/components/avatar.tsx
+++ b/web/components/avatar.tsx
@@ -40,7 +40,7 @@ export function Avatar(props: {
style={{ maxWidth: `${s * 0.25}rem` }}
src={avatarUrl}
onClick={onClick}
- alt={username}
+ alt={`${username ?? 'Unknown user'} avatar`}
onError={() => {
// If the image doesn't load, clear the avatarUrl to show the default
// Mostly for localhost, when getting a 403 from googleusercontent
diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx
index bf3730f3..3ba6f2ce 100644
--- a/web/components/comment-input.tsx
+++ b/web/components/comment-input.tsx
@@ -11,7 +11,7 @@ import { Row } from './layout/row'
import { LoadingIndicator } from './loading-indicator'
export function CommentInput(props: {
- replyToUser?: { id: string; username: string }
+ replyTo?: { id: string; username: string }
// Reply to a free response answer
parentAnswerOutcome?: string
// Reply to another comment
@@ -19,7 +19,7 @@ export function CommentInput(props: {
onSubmitComment?: (editor: Editor) => void
className?: string
}) {
- const { parentAnswerOutcome, parentCommentId, replyToUser, onSubmitComment } =
+ const { parentAnswerOutcome, parentCommentId, replyTo, onSubmitComment } =
props
const user = useUser()
@@ -55,7 +55,7 @@ export function CommentInput(props: {
[0]['upload']
submitComment: () => void
isSubmitting: boolean
}) {
- const { user, editor, upload, submitComment, isSubmitting, replyToUser } =
- props
+ const { user, editor, upload, submitComment, isSubmitting, replyTo } = props
useEffect(() => {
editor?.setEditable(!isSubmitting)
}, [isSubmitting, editor])
@@ -108,12 +107,12 @@ export function CommentInputTextArea(props: {
},
})
// insert at mention and focus
- if (replyToUser) {
+ if (replyTo) {
editor
.chain()
.setContent({
type: 'mention',
- attrs: { label: replyToUser.username, id: replyToUser.id },
+ attrs: { label: replyTo.username, id: replyTo.id },
})
.insertContent(' ')
.focus()
@@ -127,7 +126,7 @@ export function CommentInputTextArea(props: {
{user && !isSubmitting && (
diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx
index 3d25dcdd..919cce86 100644
--- a/web/components/contract-search.tsx
+++ b/web/components/contract-search.tsx
@@ -9,7 +9,14 @@ import {
} from './contract/contracts-grid'
import { ShowTime } from './contract/contract-details'
import { Row } from './layout/row'
-import { useEffect, useLayoutEffect, useRef, useMemo, ReactNode } from 'react'
+import {
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useMemo,
+ ReactNode,
+ useState,
+} from 'react'
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import { useFollows } from 'web/hooks/use-follows'
import {
@@ -32,22 +39,25 @@ import {
searchClient,
searchIndexName,
} from 'web/lib/service/algolia'
+import { useIsMobile } from 'web/hooks/use-is-mobile'
+import { AdjustmentsIcon } from '@heroicons/react/solid'
+import { Button } from './button'
+import { Modal } from './layout/modal'
+import { Title } from './title'
export const SORTS = [
{ label: 'Newest', value: 'newest' },
{ label: 'Trending', value: 'score' },
- { label: `Most traded`, value: 'most-traded' },
{ label: '24h volume', value: '24-hour-vol' },
- { label: '24h change', value: 'prob-change-day' },
{ label: 'Last updated', value: 'last-updated' },
- { label: 'Subsidy', value: 'liquidity' },
- { label: 'Close date', value: 'close-date' },
+ { label: 'Closing soon', value: 'close-date' },
{ label: 'Resolve date', value: 'resolve-date' },
{ label: 'Highest %', value: 'prob-descending' },
{ label: 'Lowest %', value: 'prob-ascending' },
] as const
export type Sort = typeof SORTS[number]['value']
+export const PROB_SORTS = ['prob-descending', 'prob-ascending']
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
@@ -83,6 +93,7 @@ export function ContractSearch(props: {
persistPrefix?: string
useQueryUrlParam?: boolean
isWholePage?: boolean
+ includeProbSorts?: boolean
noControls?: boolean
maxResults?: number
renderContracts?: (
@@ -104,6 +115,7 @@ export function ContractSearch(props: {
headerClassName,
persistPrefix,
useQueryUrlParam,
+ includeProbSorts,
isWholePage,
noControls,
maxResults,
@@ -209,6 +221,7 @@ export function ContractSearch(props: {
persistPrefix={persistPrefix}
hideOrderSelector={hideOrderSelector}
useQueryUrlParam={useQueryUrlParam}
+ includeProbSorts={includeProbSorts}
user={user}
onSearchParametersChanged={onSearchParametersChanged}
noControls={noControls}
@@ -238,6 +251,7 @@ function ContractSearchControls(props: {
additionalFilter?: AdditionalFilter
persistPrefix?: string
hideOrderSelector?: boolean
+ includeProbSorts?: boolean
onSearchParametersChanged: (params: SearchParameters) => void
useQueryUrlParam?: boolean
user?: User | null
@@ -257,6 +271,7 @@ function ContractSearchControls(props: {
user,
noControls,
autoFocus,
+ includeProbSorts,
} = props
const router = useRouter()
@@ -270,6 +285,8 @@ function ContractSearchControls(props: {
}
)
+ const isMobile = useIsMobile()
+
const sortKey = `${persistPrefix}-search-sort`
const savedSort = safeLocalStorage()?.getItem(sortKey)
@@ -415,30 +432,33 @@ function ContractSearchControls(props: {
className="input input-bordered w-full"
autoFocus={autoFocus}
/>
- {!query && (
- selectFilter(e.target.value as filter)}
- >
- Open
- Closed
- Resolved
- All
-
+ {!isMobile && (
+
)}
- {!hideOrderSelector && !query && (
- selectSort(e.target.value as Sort)}
- >
- {SORTS.map((option) => (
-
- {option.label}
-
- ))}
-
+ {isMobile && (
+ <>
+
+ }
+ />
+ >
)}
@@ -481,3 +501,78 @@ function ContractSearchControls(props: {
)
}
+
+export function SearchFilters(props: {
+ filter: string
+ selectFilter: (newFilter: filter) => void
+ hideOrderSelector: boolean | undefined
+ selectSort: (newSort: Sort) => void
+ sort: string
+ className?: string
+ includeProbSorts?: boolean
+}) {
+ const {
+ filter,
+ selectFilter,
+ hideOrderSelector,
+ selectSort,
+ sort,
+ className,
+ includeProbSorts,
+ } = props
+
+ const sorts = includeProbSorts
+ ? SORTS
+ : SORTS.filter((sort) => !PROB_SORTS.includes(sort.value))
+
+ return (
+
+ selectFilter(e.target.value as filter)}
+ >
+ Open
+ Closed
+ Resolved
+ All
+
+ {!hideOrderSelector && (
+ selectSort(e.target.value as Sort)}
+ >
+ {sorts.map((option) => (
+
+ {option.label}
+
+ ))}
+
+ )}
+
+ )
+}
+
+export function MobileSearchBar(props: { children: ReactNode }) {
+ const { children } = props
+ const [openFilters, setOpenFilters] = useState(false)
+ return (
+ <>
+ setOpenFilters(true)}>
+
+
+
+
+
+ {children}
+
+
+ >
+ )
+}
diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx
index 4d25ffa4..f984e3b6 100644
--- a/web/components/contract/contract-leaderboard.tsx
+++ b/web/components/contract/contract-leaderboard.tsx
@@ -1,11 +1,10 @@
import { Bet } from 'common/bet'
-import { ContractComment } from 'common/comment'
import { resolvedPayout } from 'common/calculate'
import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format'
import { groupBy, mapValues, sumBy, sortBy, keyBy } from 'lodash'
-import { useState, useMemo, useEffect } from 'react'
-import { listUsers, User } from 'web/lib/firebase/users'
+import { memo } from 'react'
+import { useComments } from 'web/hooks/use-comments'
import { FeedBet } from '../feed/feed-bets'
import { FeedComment } from '../feed/feed-comments'
import { Spacer } from '../layout/spacer'
@@ -13,61 +12,48 @@ import { Leaderboard } from '../leaderboard'
import { Title } from '../title'
import { BETTORS } from 'common/user'
-export function ContractLeaderboard(props: {
+export const ContractLeaderboard = memo(function ContractLeaderboard(props: {
contract: Contract
bets: Bet[]
}) {
const { contract, bets } = props
- const [users, setUsers] = useState()
- const { userProfits, top5Ids } = useMemo(() => {
- // Create a map of userIds to total profits (including sales)
- const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
- const betsByUser = groupBy(openBets, 'userId')
-
- const userProfits = mapValues(betsByUser, (bets) =>
- sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount)
- )
- // Find the 5 users with the most profits
- const top5Ids = Object.entries(userProfits)
- .sort(([_i1, p1], [_i2, p2]) => p2 - p1)
- .filter(([, p]) => p > 0)
- .slice(0, 5)
- .map(([id]) => id)
- return { userProfits, top5Ids }
- }, [contract, bets])
-
- useEffect(() => {
- if (top5Ids.length > 0) {
- listUsers(top5Ids).then((users) => {
- const sortedUsers = sortBy(users, (user) => -userProfits[user.id])
- setUsers(sortedUsers)
- })
+ // Create a map of userIds to total profits (including sales)
+ const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
+ const betsByUser = groupBy(openBets, 'userId')
+ const userProfits = mapValues(betsByUser, (bets) => {
+ return {
+ name: bets[0].userName,
+ username: bets[0].userUsername,
+ avatarUrl: bets[0].userAvatarUrl,
+ total: sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount),
}
- }, [userProfits, top5Ids])
+ })
+ // Find the 5 users with the most profits
+ const top5 = Object.values(userProfits)
+ .sort((p1, p2) => p2.total - p1.total)
+ .filter((p) => p.total > 0)
+ .slice(0, 5)
- return users && users.length > 0 ? (
+ return top5 && top5.length > 0 ? (
formatMoney(userProfits[user.id] || 0),
+ renderCell: (entry) => formatMoney(entry.total),
},
]}
className="mt-12 max-w-sm"
/>
) : null
-}
+})
-export function ContractTopTrades(props: {
- contract: Contract
- bets: Bet[]
- comments: ContractComment[]
-}) {
- const { contract, bets, comments } = props
- const commentsById = keyBy(comments, 'id')
+export function ContractTopTrades(props: { contract: Contract; bets: Bet[] }) {
+ const { contract, bets } = props
+ // todo: this stuff should be calced in DB at resolve time
+ const comments = useComments(contract.id)
const betsById = keyBy(bets, 'id')
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
@@ -88,29 +74,23 @@ export function ContractTopTrades(props: {
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
const topBettor = betsById[topBetId]?.userName
- // And also the commentId of the comment with the highest profit
- const topCommentId = sortBy(
- comments,
- (c) => c.betId && -profitById[c.betId]
- )[0]?.id
+ // And also the comment with the highest profit
+ const topComment = sortBy(comments, (c) => c.betId && -profitById[c.betId])[0]
return (
- {topCommentId && profitById[topCommentId] > 0 && (
+ {topComment && profitById[topComment.id] > 0 && (
<>
-
+
>
)}
{/* If they're the same, only show the comment; otherwise show both */}
- {topBettor && topBetId !== topCommentId && profitById[topBetId] > 0 && (
+ {topBettor && topBetId !== topComment?.betId && profitById[topBetId] > 0 && (
<>
diff --git a/web/components/contract/contract-prob-graph.tsx b/web/components/contract/contract-prob-graph.tsx
index aad44b82..60ef85b5 100644
--- a/web/components/contract/contract-prob-graph.tsx
+++ b/web/components/contract/contract-prob-graph.tsx
@@ -47,14 +47,14 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
times.push(latestTime.valueOf())
probs.push(probs[probs.length - 1])
- const quartiles = [0, 25, 50, 75, 100]
+ const { width } = useWindowSize()
+
+ const quartiles = !width || width < 800 ? [0, 50, 100] : [0, 25, 50, 75, 100]
const yTickValues = isBinary
? quartiles
: quartiles.map((x) => x / 100).map(f)
- const { width } = useWindowSize()
-
const numXTickValues = !width || width < 800 ? 2 : 5
const startDate = dayjs(times[0])
const endDate = startDate.add(1, 'hour').isAfter(latestTime)
@@ -104,7 +104,7 @@ export const ContractProbGraph = memo(function ContractProbGraph(props: {
return (
= 800 ? 350 : 250) }}
+ style={{ height: height ?? (!width || width >= 800 ? 250 : 150) }}
>
= 800}
+ enableGridX={false}
enableArea
areaBaselineValue={isBinary || isLogScale ? 0 : contract.min}
margin={{ top: 20, right: 20, bottom: 25, left: 40 }}
diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx
index 245a8d7d..17471796 100644
--- a/web/components/contract/contract-tabs.tsx
+++ b/web/components/contract/contract-tabs.tsx
@@ -5,19 +5,19 @@ import { FeedBet } from '../feed/feed-bets'
import { FeedLiquidity } from '../feed/feed-liquidity'
import { FeedAnswerCommentGroup } from '../feed/feed-answer-comment-group'
import { FeedCommentThread, ContractCommentInput } from '../feed/feed-comments'
-import { CommentTipMap } from 'web/hooks/use-tip-txns'
import { groupBy, sortBy } from 'lodash'
import { Bet } from 'common/bet'
-import { Contract, FreeResponseContract } from 'common/contract'
-import { ContractComment } from 'common/comment'
-import { PAST_BETS, User } from 'common/user'
+import { Contract } from 'common/contract'
+import { PAST_BETS } from 'common/user'
import { ContractBetsTable, BetsSummary } from '../bets-list'
import { Spacer } from '../layout/spacer'
import { Tabs } from '../layout/tabs'
import { Col } from '../layout/col'
+import { LoadingIndicator } from 'web/components/loading-indicator'
import { useComments } from 'web/hooks/use-comments'
import { useLiquidity } from 'web/hooks/use-liquidity'
import { useTipTxns } from 'web/hooks/use-tip-txns'
+import { useUser } from 'web/hooks/use-user'
import { capitalize } from 'lodash'
import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
@@ -25,21 +25,13 @@ import {
} from 'common/antes'
import { useIsMobile } from 'web/hooks/use-is-mobile'
-export function ContractTabs(props: {
- contract: Contract
- user: User | null | undefined
- bets: Bet[]
- comments: ContractComment[]
-}) {
- const { contract, user, bets, comments } = props
+export function ContractTabs(props: { contract: Contract; bets: Bet[] }) {
+ const { contract, bets } = props
const isMobile = useIsMobile()
-
+ const user = useUser()
const userBets =
user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id)
- const visibleBets = bets.filter(
- (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
- )
const yourTrades = (
@@ -57,19 +49,16 @@ export function ContractTabs(props: {
return (
- ),
+ content:
,
},
{
title: capitalize(PAST_BETS),
- content: (
-
- ),
+ content:
,
},
...(!user || !userBets?.length
? []
@@ -86,46 +75,87 @@ export function ContractTabs(props: {
const CommentsTabContent = memo(function CommentsTabContent(props: {
contract: Contract
- comments: ContractComment[]
}) {
- const { contract, comments } = props
+ const { contract } = props
const tips = useTipTxns({ contractId: contract.id })
- const updatedComments = useComments(contract.id) ?? comments
+ const comments = useComments(contract.id)
+ if (comments == null) {
+ return
+ }
if (contract.outcomeType === 'FREE_RESPONSE') {
+ const generalComments = comments.filter(
+ (c) => c.answerOutcome === undefined && c.betId === undefined
+ )
+ const sortedAnswers = sortBy(
+ contract.answers,
+ (a) => -getOutcomeProbability(contract, a.id)
+ )
+ const commentsByOutcome = groupBy(
+ comments,
+ (c) => c.answerOutcome ?? c.betOutcome ?? '_'
+ )
return (
<>
-
+ {sortedAnswers.map((answer) => (
+
+
+ c.createdTime
+ )}
+ tips={tips}
+ />
+
+ ))}
General Comments
-
- comment.answerOutcome === undefined &&
- comment.betId === undefined
- )}
- tips={tips}
- />
+
+ {generalComments.map((comment) => (
+
+ ))}
>
)
} else {
+ const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_')
+ const topLevelComments = commentsByParent['_'] ?? []
return (
-
+ <>
+
+ {sortBy(topLevelComments, (c) => -c.createdTime).map((parent) => (
+ c.createdTime
+ )}
+ tips={tips}
+ />
+ ))}
+ >
)
}
})
-function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
+const BetsTabContent = memo(function BetsTabContent(props: {
+ contract: Contract
+ bets: Bet[]
+}) {
const { contract, bets } = props
const [page, setPage] = useState(0)
const ITEMS_PER_PAGE = 50
@@ -133,6 +163,9 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
const end = start + ITEMS_PER_PAGE
const lps = useLiquidity(contract.id) ?? []
+ const visibleBets = bets.filter(
+ (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0
+ )
const visibleLps = lps.filter(
(l) =>
!l.isAnte &&
@@ -142,7 +175,7 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
)
const items = [
- ...bets.map((bet) => ({
+ ...visibleBets.map((bet) => ({
type: 'bet' as const,
id: bet.id + '-' + bet.isSold,
bet,
@@ -184,74 +217,4 @@ function ContractBetsActivity(props: { contract: Contract; bets: Bet[] }) {
/>
>
)
-}
-
-function ContractCommentsActivity(props: {
- contract: Contract
- comments: ContractComment[]
- tips: CommentTipMap
-}) {
- const { contract, comments, tips } = props
- const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_')
- const topLevelComments = sortBy(
- commentsByParentId['_'] ?? [],
- (c) => -c.createdTime
- )
-
- return (
- <>
-
- {topLevelComments.map((parent) => (
- c.createdTime
- )}
- tips={tips}
- />
- ))}
- >
- )
-}
-
-function FreeResponseContractCommentsActivity(props: {
- contract: FreeResponseContract
- comments: ContractComment[]
- tips: CommentTipMap
-}) {
- const { contract, comments, tips } = props
-
- const sortedAnswers = sortBy(
- contract.answers,
- (answer) => -getOutcomeProbability(contract, answer.number.toString())
- )
- const commentsByOutcome = groupBy(
- comments,
- (c) => c.answerOutcome ?? c.betOutcome ?? '_'
- )
-
- return (
- <>
- {sortedAnswers.map((answer) => (
-
-
- c.createdTime
- )}
- tips={tips}
- />
-
- ))}
- >
- )
-}
+})
diff --git a/web/components/contract/extra-contract-actions-row.tsx b/web/components/contract/extra-contract-actions-row.tsx
index af5db9c3..8f4b5579 100644
--- a/web/components/contract/extra-contract-actions-row.tsx
+++ b/web/components/contract/extra-contract-actions-row.tsx
@@ -1,6 +1,4 @@
-import clsx from 'clsx'
import { ShareIcon } from '@heroicons/react/outline'
-
import { Row } from '../layout/row'
import { Contract } from 'web/lib/firebase/contracts'
import React, { useState } from 'react'
@@ -10,7 +8,7 @@ import { ShareModal } from './share-modal'
import { FollowMarketButton } from 'web/components/follow-market-button'
import { LikeMarketButton } from 'web/components/contract/like-market-button'
import { ContractInfoDialog } from 'web/components/contract/contract-info-dialog'
-import { Col } from 'web/components/layout/col'
+import { Tooltip } from '../tooltip'
export function ExtraContractActionsRow(props: { contract: Contract }) {
const { contract } = props
@@ -23,27 +21,23 @@ export function ExtraContractActionsRow(props: { contract: Contract }) {
{user?.id !== contract.creatorId && (
)}
- {
- setShareOpen(true)
- }}
- >
-
-
-
-
-
-
-
-
+
+ setShareOpen(true)}
+ >
+
+
+
+
+
)
}
diff --git a/web/components/contract/like-market-button.tsx b/web/components/contract/like-market-button.tsx
index 01dce32f..7e0c765a 100644
--- a/web/components/contract/like-market-button.tsx
+++ b/web/components/contract/like-market-button.tsx
@@ -13,6 +13,7 @@ import { Col } from 'web/components/layout/col'
import { firebaseLogin } from 'web/lib/firebase/users'
import { useMarketTipTxns } from 'web/hooks/use-tip-txns'
import { sum } from 'lodash'
+import { Tooltip } from '../tooltip'
export function LikeMarketButton(props: {
contract: Contract
@@ -37,37 +38,44 @@ export function LikeMarketButton(props: {
}
return (
-
-
- 0 ? 'mr-2' : '',
- user &&
- (userLikedContractIds?.includes(contract.id) ||
- (!likes && contract.likedByUserIds?.includes(user.id)))
- ? 'fill-red-500 text-red-500'
- : ''
- )}
- />
- {totalTipped > 0 && (
-
+
+ 99
- ? 'text-[0.4rem] sm:text-[0.5rem]'
- : 'sm:text-2xs text-[0.5rem]'
+ 'h-5 w-5 sm:h-6 sm:w-6',
+ totalTipped > 0 ? 'mr-2' : '',
+ user &&
+ (userLikedContractIds?.includes(contract.id) ||
+ (!likes && contract.likedByUserIds?.includes(user.id)))
+ ? 'fill-red-500 text-red-500'
+ : ''
)}
- >
- {totalTipped}
-
- )}
-
-
+ />
+ {totalTipped > 0 && (
+ 99
+ ? 'text-[0.4rem] sm:text-[0.5rem]'
+ : 'sm:text-2xs text-[0.5rem]'
+ )}
+ >
+ {totalTipped}
+
+ )}
+
+
+
)
}
diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx
index e1eb26eb..72c7aba3 100644
--- a/web/components/contract/share-modal.tsx
+++ b/web/components/contract/share-modal.tsx
@@ -21,6 +21,7 @@ import { CreateChallengeModal } from 'web/components/challenges/create-challenge
import { useState } from 'react'
import { CHALLENGES_ENABLED } from 'common/challenge'
import ChallengeIcon from 'web/lib/icons/challenge-icon'
+import { QRCode } from '../qr-code'
export function ShareModal(props: {
contract: Contract
@@ -54,6 +55,12 @@ export function ShareModal(props: {
{' '}
if a new user signs up using the link!
+
>()
- const [showReply, setShowReply] = useState(false)
- const [highlighted, setHighlighted] = useState(false)
+ const [replyTo, setReplyTo] = useState()
const router = useRouter()
-
const answerElementId = `answer-${answer.id}`
-
- const scrollAndOpenReplyInput = useEvent(
- (comment?: ContractComment, answer?: Answer) => {
- setReplyToUser(
- comment
- ? { id: comment.userId, username: comment.userUsername }
- : answer
- ? { id: answer.userId, username: answer.username }
- : undefined
- )
- setShowReply(true)
- }
- )
+ const highlighted = router.asPath.endsWith(`#${answerElementId}`)
+ const answerRef = useRef(null)
useEffect(() => {
- if (router.asPath.endsWith(`#${answerElementId}`)) {
- setHighlighted(true)
+ if (highlighted && answerRef.current != null) {
+ answerRef.current.scrollIntoView(true)
}
- }, [answerElementId, router.asPath])
+ }, [highlighted])
return (
@@ -61,6 +45,7 @@ export function FeedAnswerCommentGroup(props: {
'gap-3 space-x-3 pt-4 transition-all duration-1000',
highlighted ? `-m-2 my-3 rounded bg-indigo-500/[0.2] p-2` : ''
)}
+ ref={answerRef}
id={answerElementId}
>
@@ -83,7 +68,9 @@ export function FeedAnswerCommentGroup(props: {
scrollAndOpenReplyInput(undefined, answer)}
+ onClick={() =>
+ setReplyTo({ id: answer.id, username: answer.username })
+ }
>
Reply
@@ -92,7 +79,9 @@ export function FeedAnswerCommentGroup(props: {
scrollAndOpenReplyInput(undefined, answer)}
+ onClick={() =>
+ setReplyTo({ id: answer.id, username: answer.username })
+ }
>
Reply
@@ -107,11 +96,13 @@ export function FeedAnswerCommentGroup(props: {
contract={contract}
comment={comment}
tips={tips[comment.id] ?? {}}
- onReplyClick={scrollAndOpenReplyInput}
+ onReplyClick={() =>
+ setReplyTo({ id: comment.id, username: comment.userUsername })
+ }
/>
))}
- {showReply && (
+ {replyTo && (
setShowReply(false)}
+ replyTo={replyTo}
+ onSubmitComment={() => setReplyTo(undefined)}
/>
)}
diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx
index acb48ec1..1b62690b 100644
--- a/web/components/feed/feed-comments.tsx
+++ b/web/components/feed/feed-comments.tsx
@@ -1,6 +1,6 @@
import { ContractComment } from 'common/comment'
import { Contract } from 'common/contract'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import { useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format'
import { useRouter } from 'next/router'
@@ -20,6 +20,8 @@ import { Editor } from '@tiptap/react'
import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input'
+export type ReplyTo = { id: string; username: string }
+
export function FeedCommentThread(props: {
contract: Contract
threadComments: ContractComment[]
@@ -27,13 +29,7 @@ export function FeedCommentThread(props: {
parentComment: ContractComment
}) {
const { contract, threadComments, tips, parentComment } = props
- const [showReply, setShowReply] = useState(false)
- const [replyTo, setReplyTo] = useState<{ id: string; username: string }>()
-
- function scrollAndOpenReplyInput(comment: ContractComment) {
- setReplyTo({ id: comment.userId, username: comment.userUsername })
- setShowReply(true)
- }
+ const [replyTo, setReplyTo] = useState
()
return (
@@ -48,10 +44,12 @@ export function FeedCommentThread(props: {
contract={contract}
comment={comment}
tips={tips[comment.id] ?? {}}
- onReplyClick={scrollAndOpenReplyInput}
+ onReplyClick={() =>
+ setReplyTo({ id: comment.id, username: comment.userUsername })
+ }
/>
))}
- {showReply && (
+ {replyTo && (
{
- setShowReply(false)
- }}
+ replyTo={replyTo}
+ onSubmitComment={() => setReplyTo(undefined)}
/>
)}
@@ -76,7 +72,7 @@ export function FeedComment(props: {
comment: ContractComment
tips?: CommentTips
indent?: boolean
- onReplyClick?: (comment: ContractComment) => void
+ onReplyClick?: () => void
}) {
const { contract, comment, tips, indent, onReplyClick } = props
const {
@@ -98,16 +94,19 @@ export function FeedComment(props: {
money = formatMoney(Math.abs(comment.betAmount))
}
- const [highlighted, setHighlighted] = useState(false)
const router = useRouter()
+ const highlighted = router.asPath.endsWith(`#${comment.id}`)
+ const commentRef = useRef(null)
+
useEffect(() => {
- if (router.asPath.endsWith(`#${comment.id}`)) {
- setHighlighted(true)
+ if (highlighted && commentRef.current != null) {
+ commentRef.current.scrollIntoView(true)
}
- }, [comment.id, router.asPath])
+ }, [highlighted])
return (